git-lfs/lfs/gitscanner_refs.go

152 lines
3.9 KiB
Go
Raw Normal View History

2016-11-17 22:47:01 +00:00
package lfs
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"regexp"
"strconv"
"strings"
)
2016-11-18 18:51:15 +00:00
var z40 = regexp.MustCompile(`\^?0{40}`)
2016-11-17 22:47:01 +00:00
// scanRefsToChan takes a ref and returns a channel of WrappedPointer objects
// for all Git LFS pointers it finds for that ref.
// Reports unique oids once only, not multiple times if >1 file uses the same content
func scanRefsToChan(cb GitScannerCallback, refLeft, refRight string, opt *ScanRefsOptions) error {
2016-11-17 22:47:01 +00:00
if opt == nil {
panic("no scan ref options")
}
revs, err := revListShas(refLeft, refRight, opt)
if err != nil {
return err
2016-11-17 22:47:01 +00:00
}
smallShas, err := catFileBatchCheck(revs)
if err != nil {
return err
2016-11-17 22:47:01 +00:00
}
pointers, err := catFileBatch(smallShas)
if err != nil {
return err
2016-11-17 22:47:01 +00:00
}
for p := range pointers.Results {
if name, ok := opt.GetName(p.Sha1); ok {
p.Name = name
2016-11-17 22:47:01 +00:00
}
cb(p, nil)
}
if err := pointers.Wait(); err != nil {
cb(nil, err)
}
2016-11-17 22:47:01 +00:00
return nil
2016-11-17 22:47:01 +00:00
}
// revListShas uses git rev-list to return the list of object sha1s
// for the given ref. If all is true, ref is ignored. It returns a
// channel from which sha1 strings can be read.
func revListShas(refLeft, refRight string, opt *ScanRefsOptions) (*StringChannelWrapper, error) {
refArgs := []string{"rev-list", "--objects"}
var stdin []string
switch opt.ScanMode {
case ScanRefsMode:
if opt.SkipDeletedBlobs {
refArgs = append(refArgs, "--no-walk")
} else {
refArgs = append(refArgs, "--do-walk")
}
refArgs = append(refArgs, refLeft)
if refRight != "" && !z40.MatchString(refRight) {
refArgs = append(refArgs, refRight)
}
case ScanAllMode:
refArgs = append(refArgs, "--all")
case ScanLeftToRemoteMode:
args, commits := revListArgsRefVsRemote(refLeft, opt.RemoteName, opt.skippedRefs)
refArgs = append(refArgs, args...)
if len(commits) > 0 {
stdin = commits
}
default:
return nil, errors.New("scanner: unknown scan type: " + strconv.Itoa(int(opt.ScanMode)))
}
// Use "--" at the end of the command to disambiguate arguments as refs,
// so Git doesn't complain about ambiguity if you happen to also have a
// file named "master".
refArgs = append(refArgs, "--")
cmd, err := startCommand("git", refArgs...)
if err != nil {
return nil, err
}
if len(stdin) > 0 {
cmd.Stdin.Write([]byte(strings.Join(stdin, "\n")))
}
cmd.Stdin.Close()
revs := make(chan string, chanBufSize)
errchan := make(chan error, 5) // may be multiple errors
go func() {
scanner := bufio.NewScanner(cmd.Stdout)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) < 40 {
continue
}
sha1 := line[0:40]
if len(line) > 40 {
opt.SetName(sha1, line[41:len(line)])
}
revs <- sha1
}
stderr, _ := ioutil.ReadAll(cmd.Stderr)
err := cmd.Wait()
if err != nil {
errchan <- fmt.Errorf("Error in git rev-list --objects: %v %v", err, string(stderr))
} else {
// Special case detection of ambiguous refs; lower level commands like
// git rev-list do not return non-zero exit codes in this case, just warn
ambiguousRegex := regexp.MustCompile(`warning: refname (.*) is ambiguous`)
if match := ambiguousRegex.FindStringSubmatch(string(stderr)); match != nil {
// Promote to fatal & exit
errchan <- fmt.Errorf("Error: ref %s is ambiguous", match[1])
}
}
close(revs)
close(errchan)
}()
return NewStringChannelWrapper(revs, errchan), nil
}
// Get additional arguments needed to limit 'git rev-list' to just the changes
// in refTo that are also not on remoteName.
//
// Returns a slice of string command arguments, and a slice of string git
// commits to pass to `git rev-list` via STDIN.
func revListArgsRefVsRemote(refTo, remoteName string, skippedRefs []string) ([]string, []string) {
if len(skippedRefs) < 1 {
// Safe to use cached
return []string{refTo, "--not", "--remotes=" + remoteName}, nil
}
// Use only the non-missing refs as 'from' points
commits := make([]string, 1, len(skippedRefs)+1)
commits[0] = refTo
return []string{"--stdin"}, append(commits, skippedRefs...)
}