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(refLeft, refRight string, opt *ScanRefsOptions) (*PointerChannelWrapper, error) {
|
|
|
|
if opt == nil {
|
|
|
|
panic("no scan ref options")
|
|
|
|
}
|
|
|
|
|
|
|
|
revs, err := revListShas(refLeft, refRight, opt)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
smallShas, err := catFileBatchCheck(revs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
pointers, err := catFileBatch(smallShas)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
retchan := make(chan *WrappedPointer, chanBufSize)
|
|
|
|
errchan := make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
for p := range pointers.Results {
|
|
|
|
if name, ok := opt.GetName(p.Sha1); ok {
|
|
|
|
p.Name = name
|
|
|
|
}
|
|
|
|
retchan <- p
|
|
|
|
}
|
|
|
|
err := pointers.Wait()
|
|
|
|
if err != nil {
|
|
|
|
errchan <- err
|
|
|
|
}
|
|
|
|
close(retchan)
|
|
|
|
close(errchan)
|
|
|
|
}()
|
|
|
|
|
|
|
|
return NewPointerChannelWrapper(retchan, errchan), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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...)
|
|
|
|
}
|