13 "github.com/git-lfs/git-lfs/git"
14 "github.com/git-lfs/git-lfs/tools"
15 "github.com/spf13/cobra"
19 prefixBlocklist = []string{
23 trackLockableFlag bool
24 trackNotLockableFlag bool
25 trackVerboseLoggingFlag bool
27 trackNoModifyAttrsFlag bool
30 func trackCommand(cmd *cobra.Command, args []string) {
33 if cfg.LocalGitDir() == "" {
34 Print("Not a git repository.")
38 if cfg.LocalWorkingDir() == "" {
39 Print("This operation must be run in a work tree.")
43 if !cfg.Os.Bool("GIT_LFS_TRACK_NO_INSTALL_HOOKS", false) {
52 knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir())
53 lineEnd := getAttributeLineEnding(knownPatterns)
54 if len(lineEnd) == 0 {
55 lineEnd = gitLineEnding(cfg.Git)
58 wd, _ := tools.Getwd()
59 wd = tools.ResolveSymlinks(wd)
60 relpath, err := filepath.Rel(cfg.LocalWorkingDir(), wd)
62 Exit("Current directory %q outside of git working directory %q.", wd, cfg.LocalWorkingDir())
65 changedAttribLines := make(map[string]string)
66 var readOnlyPatterns []string
67 var writeablePatterns []string
69 for _, unsanitizedPattern := range args {
70 pattern := trimCurrentPrefix(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)
83 // Generate the new / changed attrib line for merging
84 encodedArg := escapeAttrPattern(pattern)
86 if trackLockableFlag { // no need to test trackNotLockableFlag, if we got here we're disabling
87 lockableArg = " " + git.LockableAttrib
90 changedAttribLines[pattern] = fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text%v%s", encodedArg, lockableArg, lineEnd)
92 if trackLockableFlag {
93 readOnlyPatterns = append(readOnlyPatterns, pattern)
95 writeablePatterns = append(writeablePatterns, pattern)
98 Print("Tracking %q", unescapeAttrPattern(encodedArg))
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
106 attribContents []byte
107 attributesFile *os.File
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")
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)
119 Print("Error opening .gitattributes file")
122 defer attributesFile.Close()
124 if len(attribContents) > 0 {
125 scanner := bufio.NewScanner(bytes.NewReader(attribContents))
127 line := scanner.Text()
128 fields := strings.Fields(line)
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)
140 // Write line unchanged (replace newline)
141 attributesFile.WriteString(line + lineEnd)
145 // Our method of writing also made sure there's always a newline at end
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)
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
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)
171 gittracked, err := git.GetTrackedFiles(pattern)
173 Exit("Error getting tracked files for %q: %s", pattern, err)
176 if trackVerboseLoggingFlag {
177 Print("Found %d files previously added to Git matching pattern: %s", len(gittracked), pattern)
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
187 if matchedBlocklist {
191 for _, f := range gittracked {
192 if trackVerboseLoggingFlag || trackDryRunFlag {
193 Print("Git LFS: touching %q", f)
196 if !trackDryRunFlag {
198 err := os.Chtimes(f, now, now)
200 LoggedError(err, "Error marking %q modified: %s", f, err)
207 // now flip read-only mode based on lockable / not lockable changes
208 lockClient := newLockClient()
209 err = lockClient.FixFileWriteFlagsInDir(relpath, readOnlyPatterns, writeablePatterns)
211 LoggedError(err, "Error changing lockable file permissions: %s", err)
215 func listPatterns() {
216 knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir())
217 if len(knownPatterns) < 1 {
221 Print("Listing tracked patterns")
222 for _, t := range knownPatterns {
224 Print(" %s [lockable] (%s)", t.Path, t.Source)
226 Print(" %s (%s)", t.Path, t.Source)
231 func getAttributeLineEnding(attribs []git.AttributePath) string {
232 for _, a := range attribs {
233 if a.Source.Path == ".gitattributes" {
234 return a.Source.LineEnding
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)
245 for _, p := range prefixBlocklist {
246 if strings.HasPrefix(base, p) {
255 trackEscapePatterns = map[string]string{
261 func escapeAttrPattern(unescaped string) string {
262 var escaped string = strings.Replace(unescaped, `\`, "/", -1)
264 for from, to := range trackEscapePatterns {
265 escaped = strings.Replace(escaped, from, to, -1)
271 func unescapeAttrPattern(escaped string) string {
272 var unescaped string = escaped
274 for to, from := range trackEscapePatterns {
275 unescaped = strings.Replace(unescaped, from, to, -1)
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")