github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/lfs/gitscanner_refs.go (about)

     1  package lfs
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  )
    12  
    13  var z40 = regexp.MustCompile(`\^?0{40}`)
    14  
    15  type lockableNameSet struct {
    16  	opt *ScanRefsOptions
    17  	set GitScannerSet
    18  }
    19  
    20  // Determines if the given blob sha matches a locked file.
    21  func (s *lockableNameSet) Check(blobSha string) (string, bool) {
    22  	if s == nil || s.opt == nil || s.set == nil {
    23  		return "", false
    24  	}
    25  
    26  	name, ok := s.opt.GetName(blobSha)
    27  	if !ok {
    28  		return name, ok
    29  	}
    30  
    31  	if s.set.Contains(name) {
    32  		return name, true
    33  	}
    34  	return name, false
    35  }
    36  
    37  func noopFoundLockable(name string) {}
    38  
    39  // scanRefsToChan takes a ref and returns a channel of WrappedPointer objects
    40  // for all Git LFS pointers it finds for that ref.
    41  // Reports unique oids once only, not multiple times if >1 file uses the same content
    42  func scanRefsToChan(scanner *GitScanner, pointerCb GitScannerFoundPointer, refLeft, refRight string, opt *ScanRefsOptions) error {
    43  	if opt == nil {
    44  		panic("no scan ref options")
    45  	}
    46  
    47  	revs, err := revListShas(refLeft, refRight, opt)
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	lockableSet := &lockableNameSet{opt: opt, set: scanner.PotentialLockables}
    53  	smallShas, batchLockableCh, err := catFileBatchCheck(revs, lockableSet)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	lockableCb := scanner.FoundLockable
    59  	if lockableCb == nil {
    60  		lockableCb = noopFoundLockable
    61  	}
    62  
    63  	go func(cb GitScannerFoundLockable, ch chan string) {
    64  		for name := range ch {
    65  			cb(name)
    66  		}
    67  	}(lockableCb, batchLockableCh)
    68  
    69  	pointers, checkLockableCh, err := catFileBatch(smallShas, lockableSet)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	for p := range pointers.Results {
    75  		if name, ok := opt.GetName(p.Sha1); ok {
    76  			p.Name = name
    77  		}
    78  		pointerCb(p, nil)
    79  	}
    80  
    81  	for lockableName := range checkLockableCh {
    82  		lockableCb(lockableName)
    83  	}
    84  
    85  	if err := pointers.Wait(); err != nil {
    86  		pointerCb(nil, err)
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // revListShas uses git rev-list to return the list of object sha1s
    93  // for the given ref. If all is true, ref is ignored. It returns a
    94  // channel from which sha1 strings can be read.
    95  func revListShas(refLeft, refRight string, opt *ScanRefsOptions) (*StringChannelWrapper, error) {
    96  	refArgs := []string{"rev-list", "--objects"}
    97  	var stdin []string
    98  	switch opt.ScanMode {
    99  	case ScanRefsMode:
   100  		if opt.SkipDeletedBlobs {
   101  			refArgs = append(refArgs, "--no-walk")
   102  		} else {
   103  			refArgs = append(refArgs, "--do-walk")
   104  		}
   105  
   106  		refArgs = append(refArgs, refLeft)
   107  		if refRight != "" && !z40.MatchString(refRight) {
   108  			refArgs = append(refArgs, refRight)
   109  		}
   110  	case ScanAllMode:
   111  		refArgs = append(refArgs, "--all")
   112  	case ScanLeftToRemoteMode:
   113  		args, commits := revListArgsRefVsRemote(refLeft, opt.RemoteName, opt.skippedRefs)
   114  		refArgs = append(refArgs, args...)
   115  		if len(commits) > 0 {
   116  			stdin = commits
   117  		}
   118  	default:
   119  		return nil, errors.New("scanner: unknown scan type: " + strconv.Itoa(int(opt.ScanMode)))
   120  	}
   121  
   122  	// Use "--" at the end of the command to disambiguate arguments as refs,
   123  	// so Git doesn't complain about ambiguity if you happen to also have a
   124  	// file named "master".
   125  	refArgs = append(refArgs, "--")
   126  
   127  	cmd, err := startCommand("git", refArgs...)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if len(stdin) > 0 {
   133  		cmd.Stdin.Write([]byte(strings.Join(stdin, "\n")))
   134  	}
   135  
   136  	cmd.Stdin.Close()
   137  
   138  	revs := make(chan string, chanBufSize)
   139  	errchan := make(chan error, 5) // may be multiple errors
   140  
   141  	go func() {
   142  		scanner := bufio.NewScanner(cmd.Stdout)
   143  		for scanner.Scan() {
   144  			line := strings.TrimSpace(scanner.Text())
   145  			if len(line) < 40 {
   146  				continue
   147  			}
   148  
   149  			sha1 := line[0:40]
   150  			if len(line) > 40 {
   151  				opt.SetName(sha1, line[41:len(line)])
   152  			}
   153  			revs <- sha1
   154  		}
   155  
   156  		stderr, _ := ioutil.ReadAll(cmd.Stderr)
   157  		err := cmd.Wait()
   158  		if err != nil {
   159  			errchan <- fmt.Errorf("Error in git rev-list --objects: %v %v", err, string(stderr))
   160  		} else {
   161  			// Special case detection of ambiguous refs; lower level commands like
   162  			// git rev-list do not return non-zero exit codes in this case, just warn
   163  			ambiguousRegex := regexp.MustCompile(`warning: refname (.*) is ambiguous`)
   164  			if match := ambiguousRegex.FindStringSubmatch(string(stderr)); match != nil {
   165  				// Promote to fatal & exit
   166  				errchan <- fmt.Errorf("Error: ref %s is ambiguous", match[1])
   167  			}
   168  		}
   169  		close(revs)
   170  		close(errchan)
   171  	}()
   172  
   173  	return NewStringChannelWrapper(revs, errchan), nil
   174  }
   175  
   176  // Get additional arguments needed to limit 'git rev-list' to just the changes
   177  // in refTo that are also not on remoteName.
   178  //
   179  // Returns a slice of string command arguments, and a slice of string git
   180  // commits to pass to `git rev-list` via STDIN.
   181  func revListArgsRefVsRemote(refTo, remoteName string, skippedRefs []string) ([]string, []string) {
   182  	if len(skippedRefs) < 1 {
   183  		// Safe to use cached
   184  		return []string{refTo, "--not", "--remotes=" + remoteName}, nil
   185  	}
   186  
   187  	// Use only the non-missing refs as 'from' points
   188  	commits := make([]string, 1, len(skippedRefs)+1)
   189  	commits[0] = refTo
   190  	return []string{"--stdin"}, append(commits, skippedRefs...)
   191  }