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