3472f9c3e6773933d51ce761da68a3bc41a1d735
[scm/test.git] / commands / command_track.go
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 }