github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/locking/lockable.go (about) 1 package locking 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "strings" 8 "sync" 9 10 "github.com/git-lfs/git-lfs/errors" 11 "github.com/git-lfs/git-lfs/filepathfilter" 12 "github.com/git-lfs/git-lfs/git" 13 "github.com/git-lfs/git-lfs/tools" 14 ) 15 16 // GetLockablePatterns returns a list of patterns in .gitattributes which are 17 // marked as lockable 18 func (c *Client) GetLockablePatterns() []string { 19 c.ensureLockablesLoaded() 20 return c.lockablePatterns 21 } 22 23 // getLockableFilter returns the internal filter used to check if a file is lockable 24 func (c *Client) getLockableFilter() *filepathfilter.Filter { 25 c.ensureLockablesLoaded() 26 return c.lockableFilter 27 } 28 29 func (c *Client) ensureLockablesLoaded() { 30 c.lockableMutex.Lock() 31 defer c.lockableMutex.Unlock() 32 33 // Only load once 34 if c.lockablePatterns == nil { 35 c.refreshLockablePatterns() 36 } 37 } 38 39 // Internal function to repopulate lockable patterns 40 // You must have locked the c.lockableMutex in the caller 41 func (c *Client) refreshLockablePatterns() { 42 43 paths := git.GetAttributePaths(c.LocalWorkingDir, c.LocalGitDir) 44 // Always make non-nil even if empty 45 c.lockablePatterns = make([]string, 0, len(paths)) 46 for _, p := range paths { 47 if p.Lockable { 48 c.lockablePatterns = append(c.lockablePatterns, p.Path) 49 } 50 } 51 c.lockableFilter = filepathfilter.New(c.lockablePatterns, nil) 52 } 53 54 // IsFileLockable returns whether a specific file path is marked as Lockable, 55 // ie has the 'lockable' attribute in .gitattributes 56 // Lockable patterns are cached once for performance, unless you call RefreshLockablePatterns 57 // path should be relative to repository root 58 func (c *Client) IsFileLockable(path string) bool { 59 return c.getLockableFilter().Allows(path) 60 } 61 62 // FixAllLockableFileWriteFlags recursively scans the repo looking for files which 63 // are lockable, and makes sure their write flags are set correctly based on 64 // whether they are currently locked or unlocked. 65 // Files which are unlocked are made read-only, files which are locked are made 66 // writeable. 67 // This function can be used after a clone or checkout to ensure that file 68 // state correctly reflects the locking state 69 func (c *Client) FixAllLockableFileWriteFlags() error { 70 return c.fixFileWriteFlags(c.LocalWorkingDir, c.LocalWorkingDir, c.getLockableFilter(), nil) 71 } 72 73 // FixFileWriteFlagsInDir scans dir (which can either be a relative dir 74 // from the root of the repo, or an absolute dir within the repo) looking for 75 // files to change permissions for. 76 // If lockablePatterns is non-nil, then any file matching those patterns will be 77 // checked to see if it is currently locked by the current committer, and if so 78 // it will be writeable, and if not locked it will be read-only. 79 // If unlockablePatterns is non-nil, then any file matching those patterns will 80 // be made writeable if it is not already. This can be used to reset files to 81 // writeable when their 'lockable' attribute is turned off. 82 func (c *Client) FixFileWriteFlagsInDir(dir string, lockablePatterns, unlockablePatterns []string) error { 83 84 // early-out if no patterns 85 if len(lockablePatterns) == 0 && len(unlockablePatterns) == 0 { 86 return nil 87 } 88 89 absPath := dir 90 if !filepath.IsAbs(dir) { 91 absPath = filepath.Join(c.LocalWorkingDir, dir) 92 } 93 stat, err := os.Stat(absPath) 94 if err != nil { 95 return err 96 } 97 if !stat.IsDir() { 98 return fmt.Errorf("%q is not a valid directory", dir) 99 } 100 101 var lockableFilter *filepathfilter.Filter 102 var unlockableFilter *filepathfilter.Filter 103 if lockablePatterns != nil { 104 lockableFilter = filepathfilter.New(lockablePatterns, nil) 105 } 106 if unlockablePatterns != nil { 107 unlockableFilter = filepathfilter.New(unlockablePatterns, nil) 108 } 109 110 return c.fixFileWriteFlags(absPath, c.LocalWorkingDir, lockableFilter, unlockableFilter) 111 } 112 113 // Internal implementation of fixing file write flags with precompiled filters 114 func (c *Client) fixFileWriteFlags(absPath, workingDir string, lockable, unlockable *filepathfilter.Filter) error { 115 116 var errs []error 117 var errMux sync.Mutex 118 119 addErr := func(err error) { 120 errMux.Lock() 121 defer errMux.Unlock() 122 123 errs = append(errs, err) 124 } 125 126 tools.FastWalkGitRepo(absPath, func(parentDir string, fi os.FileInfo, err error) { 127 if err != nil { 128 addErr(err) 129 return 130 } 131 // Skip dirs, we only need to check files 132 if fi.IsDir() { 133 return 134 } 135 abschild := filepath.Join(parentDir, fi.Name()) 136 137 // This is a file, get relative to repo root 138 relpath, err := filepath.Rel(workingDir, abschild) 139 if err != nil { 140 addErr(err) 141 return 142 } 143 144 err = c.fixSingleFileWriteFlags(relpath, lockable, unlockable) 145 if err != nil { 146 addErr(err) 147 } 148 149 }) 150 return errors.Combine(errs) 151 } 152 153 // FixLockableFileWriteFlags checks each file in the provided list, and for 154 // those which are lockable, makes sure their write flags are set correctly 155 // based on whether they are currently locked or unlocked. Files which are 156 // unlocked are made read-only, files which are locked are made writeable. 157 // Files which are not lockable are ignored. 158 // This function can be used after a clone or checkout to ensure that file 159 // state correctly reflects the locking state, and is more efficient than 160 // FixAllLockableFileWriteFlags when you know which files changed 161 func (c *Client) FixLockableFileWriteFlags(files []string) error { 162 // early-out if no lockable patterns 163 if len(c.GetLockablePatterns()) == 0 { 164 return nil 165 } 166 167 var errs []error 168 for _, f := range files { 169 err := c.fixSingleFileWriteFlags(f, c.getLockableFilter(), nil) 170 if err != nil { 171 errs = append(errs, err) 172 } 173 } 174 175 return errors.Combine(errs) 176 } 177 178 // fixSingleFileWriteFlags fixes write flags on a single file 179 // If lockablePatterns is non-nil, then any file matching those patterns will be 180 // checked to see if it is currently locked by the current committer, and if so 181 // it will be writeable, and if not locked it will be read-only. 182 // If unlockablePatterns is non-nil, then any file matching those patterns will 183 // be made writeable if it is not already. This can be used to reset files to 184 // writeable when their 'lockable' attribute is turned off. 185 func (c *Client) fixSingleFileWriteFlags(file string, lockable, unlockable *filepathfilter.Filter) error { 186 // Convert to git-style forward slash separators if necessary 187 // Necessary to match attributes 188 if filepath.Separator == '\\' { 189 file = strings.Replace(file, "\\", "/", -1) 190 } 191 if lockable != nil && lockable.Allows(file) { 192 // Lockable files are writeable only if they're currently locked 193 err := tools.SetFileWriteFlag(file, c.IsFileLockedByCurrentCommitter(file)) 194 // Ignore not exist errors 195 if err != nil && !os.IsNotExist(err) { 196 return err 197 } 198 } else if unlockable != nil && unlockable.Allows(file) { 199 // Unlockable files are always writeable 200 // We only check files which match the incoming patterns to avoid 201 // checking every file in the system all the time, and only do it 202 // when a file has had its lockable attribute removed 203 err := tools.SetFileWriteFlag(file, true) 204 if err != nil && !os.IsNotExist(err) { 205 return err 206 } 207 } 208 return nil 209 }