github.com/2lambda123/git-lfs@v2.5.2+incompatible/commands/command_track.go (about) 1 package commands 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/git-lfs/git-lfs/git" 14 "github.com/git-lfs/git-lfs/tools" 15 "github.com/spf13/cobra" 16 ) 17 18 var ( 19 prefixBlocklist = []string{ 20 ".git", ".lfs", 21 } 22 23 trackLockableFlag bool 24 trackNotLockableFlag bool 25 trackVerboseLoggingFlag bool 26 trackDryRunFlag bool 27 trackNoModifyAttrsFlag bool 28 ) 29 30 func trackCommand(cmd *cobra.Command, args []string) { 31 requireGitVersion() 32 33 if cfg.LocalGitDir() == "" { 34 Print("Not a git repository.") 35 os.Exit(128) 36 } 37 38 if cfg.LocalWorkingDir() == "" { 39 Print("This operation must be run in a work tree.") 40 os.Exit(128) 41 } 42 43 if !cfg.Os.Bool("GIT_LFS_TRACK_NO_INSTALL_HOOKS", false) { 44 installHooks(false) 45 } 46 47 if len(args) == 0 { 48 listPatterns() 49 return 50 } 51 52 // Intentionally do _not_ consider global- and system-level 53 // .gitattributes here. 54 knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir()) 55 lineEnd := getAttributeLineEnding(knownPatterns) 56 if len(lineEnd) == 0 { 57 lineEnd = gitLineEnding(cfg.Git) 58 } 59 60 wd, _ := tools.Getwd() 61 wd = tools.ResolveSymlinks(wd) 62 relpath, err := filepath.Rel(cfg.LocalWorkingDir(), wd) 63 if err != nil { 64 Exit("Current directory %q outside of git working directory %q.", wd, cfg.LocalWorkingDir()) 65 } 66 67 changedAttribLines := make(map[string]string) 68 var readOnlyPatterns []string 69 var writeablePatterns []string 70 ArgsLoop: 71 for _, unsanitizedPattern := range args { 72 pattern := trimCurrentPrefix(cleanRootPath(unsanitizedPattern)) 73 if !trackNoModifyAttrsFlag { 74 for _, known := range knownPatterns { 75 if unescapeAttrPattern(known.Path) == filepath.Join(relpath, pattern) && 76 ((trackLockableFlag && known.Lockable) || // enabling lockable & already lockable (no change) 77 (trackNotLockableFlag && !known.Lockable) || // disabling lockable & not lockable (no change) 78 (!trackLockableFlag && !trackNotLockableFlag)) { // leave lockable as-is in all cases 79 Print("%q already supported", pattern) 80 continue ArgsLoop 81 } 82 } 83 } 84 85 // Generate the new / changed attrib line for merging 86 encodedArg := escapeAttrPattern(pattern) 87 lockableArg := "" 88 if trackLockableFlag { // no need to test trackNotLockableFlag, if we got here we're disabling 89 lockableArg = " " + git.LockableAttrib 90 } 91 92 changedAttribLines[pattern] = fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text%v%s", encodedArg, lockableArg, lineEnd) 93 94 if trackLockableFlag { 95 readOnlyPatterns = append(readOnlyPatterns, pattern) 96 } else { 97 writeablePatterns = append(writeablePatterns, pattern) 98 } 99 100 Print("Tracking %q", unescapeAttrPattern(encodedArg)) 101 } 102 103 // Now read the whole local attributes file and iterate over the contents, 104 // replacing any lines where the values have changed, and appending new lines 105 // change this: 106 107 var ( 108 attribContents []byte 109 attributesFile *os.File 110 ) 111 if !trackNoModifyAttrsFlag { 112 attribContents, err = ioutil.ReadFile(".gitattributes") 113 // it's fine for file to not exist 114 if err != nil && !os.IsNotExist(err) { 115 Print("Error reading .gitattributes file") 116 return 117 } 118 // Re-generate the file with merge of old contents and new (to deal with changes) 119 attributesFile, err = os.OpenFile(".gitattributes", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0660) 120 if err != nil { 121 Print("Error opening .gitattributes file") 122 return 123 } 124 defer attributesFile.Close() 125 126 if len(attribContents) > 0 { 127 scanner := bufio.NewScanner(bytes.NewReader(attribContents)) 128 for scanner.Scan() { 129 line := scanner.Text() 130 fields := strings.Fields(line) 131 if len(fields) < 1 { 132 continue 133 } 134 135 pattern := unescapeAttrPattern(fields[0]) 136 if newline, ok := changedAttribLines[pattern]; ok { 137 // Replace this line (newline already embedded) 138 attributesFile.WriteString(newline) 139 // Remove from map so we know we don't have to add it to the end 140 delete(changedAttribLines, pattern) 141 } else { 142 // Write line unchanged (replace newline) 143 attributesFile.WriteString(line + lineEnd) 144 } 145 } 146 147 // Our method of writing also made sure there's always a newline at end 148 } 149 } 150 151 // Any items left in the map, write new lines at the end of the file 152 // Note this is only new patterns, not ones which changed locking flags 153 for pattern, newline := range changedAttribLines { 154 if !trackNoModifyAttrsFlag { 155 // Newline already embedded 156 attributesFile.WriteString(newline) 157 } 158 159 // Also, for any new patterns we've added, make sure any existing git 160 // tracked files have their timestamp updated so they will now show as 161 // modifed note this is relative to current dir which is how we write 162 // .gitattributes deliberately not done in parallel as a chan because 163 // we'll be marking modified 164 // 165 // NOTE: `git ls-files` does not do well with leading slashes. 166 // Since all `git-lfs track` calls are relative to the root of 167 // the repository, the leading slash is simply removed for its 168 // implicit counterpart. 169 if trackVerboseLoggingFlag { 170 Print("Searching for files matching pattern: %s", pattern) 171 } 172 173 gittracked, err := git.GetTrackedFiles(pattern) 174 if err != nil { 175 Exit("Error getting tracked files for %q: %s", pattern, err) 176 } 177 178 if trackVerboseLoggingFlag { 179 Print("Found %d files previously added to Git matching pattern: %s", len(gittracked), pattern) 180 } 181 182 var matchedBlocklist bool 183 for _, f := range gittracked { 184 if forbidden := blocklistItem(f); forbidden != "" { 185 Print("Pattern %s matches forbidden file %s. If you would like to track %s, modify .gitattributes manually.", pattern, f, f) 186 matchedBlocklist = true 187 } 188 } 189 if matchedBlocklist { 190 continue 191 } 192 193 for _, f := range gittracked { 194 if trackVerboseLoggingFlag || trackDryRunFlag { 195 Print("Git LFS: touching %q", f) 196 } 197 198 if !trackDryRunFlag { 199 now := time.Now() 200 err := os.Chtimes(f, now, now) 201 if err != nil { 202 LoggedError(err, "Error marking %q modified: %s", f, err) 203 continue 204 } 205 } 206 } 207 } 208 209 // now flip read-only mode based on lockable / not lockable changes 210 lockClient := newLockClient() 211 err = lockClient.FixFileWriteFlagsInDir(relpath, readOnlyPatterns, writeablePatterns) 212 if err != nil { 213 LoggedError(err, "Error changing lockable file permissions: %s", err) 214 } 215 } 216 217 func listPatterns() { 218 knownPatterns := getAllKnownPatterns() 219 if len(knownPatterns) < 1 { 220 return 221 } 222 223 Print("Listing tracked patterns") 224 for _, t := range knownPatterns { 225 if t.Lockable { 226 Print(" %s [lockable] (%s)", t.Path, t.Source) 227 } else { 228 Print(" %s (%s)", t.Path, t.Source) 229 } 230 } 231 } 232 233 func getAllKnownPatterns() []git.AttributePath { 234 knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir()) 235 knownPatterns = append(knownPatterns, git.GetRootAttributePaths(cfg.Git)...) 236 knownPatterns = append(knownPatterns, git.GetSystemAttributePaths(cfg.Os)...) 237 238 return knownPatterns 239 } 240 241 func getAttributeLineEnding(attribs []git.AttributePath) string { 242 for _, a := range attribs { 243 if a.Source.Path == ".gitattributes" { 244 return a.Source.LineEnding 245 } 246 } 247 return "" 248 } 249 250 // blocklistItem returns the name of the blocklist item preventing the given 251 // file-name from being tracked, or an empty string, if there is none. 252 func blocklistItem(name string) string { 253 base := filepath.Base(name) 254 255 for _, p := range prefixBlocklist { 256 if strings.HasPrefix(base, p) { 257 return p 258 } 259 } 260 261 return "" 262 } 263 264 var ( 265 trackEscapePatterns = map[string]string{ 266 " ": "[[:space:]]", 267 "#": "\\#", 268 } 269 ) 270 271 func escapeAttrPattern(unescaped string) string { 272 var escaped string = strings.Replace(unescaped, `\`, "/", -1) 273 274 for from, to := range trackEscapePatterns { 275 escaped = strings.Replace(escaped, from, to, -1) 276 } 277 278 return escaped 279 } 280 281 func unescapeAttrPattern(escaped string) string { 282 var unescaped string = escaped 283 284 for to, from := range trackEscapePatterns { 285 unescaped = strings.Replace(unescaped, from, to, -1) 286 } 287 288 return unescaped 289 } 290 291 func init() { 292 RegisterCommand("track", trackCommand, func(cmd *cobra.Command) { 293 cmd.Flags().BoolVarP(&trackLockableFlag, "lockable", "l", false, "make pattern lockable, i.e. read-only unless locked") 294 cmd.Flags().BoolVarP(&trackNotLockableFlag, "not-lockable", "", false, "remove lockable attribute from pattern") 295 cmd.Flags().BoolVarP(&trackVerboseLoggingFlag, "verbose", "v", false, "log which files are being tracked and modified") 296 cmd.Flags().BoolVarP(&trackDryRunFlag, "dry-run", "d", false, "preview results of running `git lfs track`") 297 cmd.Flags().BoolVarP(&trackNoModifyAttrsFlag, "no-modify-attrs", "", false, "skip modifying .gitattributes file") 298 }) 299 }