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  }