github.com/git-lfs/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  }