git-lfs/lfs/diff_index_scanner.go
2017-03-15 12:17:53 -06:00

192 lines
5.1 KiB
Go

package lfs
import (
"bufio"
"strconv"
"strings"
"github.com/git-lfs/git-lfs/errors"
)
// Status represents the status of a file that appears in the output of `git
// diff-index`.
//
// More information about each of its valid instances can be found:
// https://git-scm.com/docs/git-diff-index
type Status rune
const (
StatusAddition Status = 'A'
StatusCopy Status = 'C'
StatusDeletion Status = 'D'
StatusModification Status = 'M'
StatusRename Status = 'R'
StatusTypeChange Status = 'T'
StatusUnmerged Status = 'U'
StatusUnknown Status = 'X'
)
// String implements fmt.Stringer by returning a huamn-readable name for each
// status.
func (s Status) String() string {
switch s {
case StatusAddition:
return "addition"
case StatusCopy:
return "copy"
case StatusDeletion:
return "deletion"
case StatusModification:
return "modification"
case StatusRename:
return "rename"
case StatusTypeChange:
return "change"
case StatusUnmerged:
return "unmerged"
case StatusUnknown:
return "unknown"
}
return "<unknown>"
}
// DiffIndexEntry holds information about a single item in the results of a `git
// diff-index` command.
type DiffIndexEntry struct {
// SrcMode is the file mode of the "src" file, stored as a string-based
// octal.
SrcMode string
// DstMode is the file mode of the "dst" file, stored as a string-based
// octal.
DstMode string
// SrcSha is the Git blob ID of the "src" file.
SrcSha string
// DstSha is the Git blob ID of the "dst" file.
DstSha string
// Status is the status of the file in the index.
Status Status
// StatusScore is the optional "score" assosicated with a particular
// status.
StatusScore int
// SrcName is the name of the file in it's "src" state as it appears in
// the index.
SrcName string
// DstName is the name of the file in it's "dst" state as it appears in
// the index.
DstName string
}
// DiffIndexScanner scans the output of the `git diff-index` command.
type DiffIndexScanner struct {
// next is the next entry scanned by the Scanner.
next *DiffIndexEntry
// err is any error that the Scanner encountered while scanning.
err error
// from is the underlying scanner, scanning the `git diff-index`
// command's stdout.
from *bufio.Scanner
}
// NewDiffIndexScanner initializes a new `DiffIndexScanner` scanning at the
// given ref, "ref".
//
// If "cache" is given, the DiffIndexScanner will scan for differences between
// the given ref and the index. If "cache" is _not_ given, DiffIndexScanner will
// scan for differences between the given ref and the currently checked out
// tree.
//
// If any error was encountered in starting the command or closing its `stdin`,
// that error will be returned immediately. Otherwise, a `*DiffIndexScanner`
// will be returned with a `nil` error.
func NewDiffIndexScanner(ref string, cached bool) (*DiffIndexScanner, *wrappedCmd, error) {
cmd, err := startCommand("git", diffIndexCmdArgs(ref, cached)...)
if err != nil {
return nil, cmd, errors.Wrap(err, "diff-index")
}
if err = cmd.Stdin.Close(); err != nil {
return nil, cmd, errors.Wrap(err, "diff-index: close")
}
return &DiffIndexScanner{
from: bufio.NewScanner(cmd.Stdout),
}, cmd, nil
}
// diffIndexCmdArgs returns a string slice containing the arguments necessary
// to run the diff-index command.
func diffIndexCmdArgs(ref string, cached bool) []string {
args := []string{"diff-index", "-M"}
if cached {
args = append(args, "--cached")
}
args = append(args, ref)
return args
}
// Scan advances the scan line and yields either a new value for Entry(), or an
// Err(). It returns true or false, whether or not it can continue scanning for
// more entries.
func (s *DiffIndexScanner) Scan() bool {
if !s.prepareScan() {
return false
}
s.next, s.err = s.scan(s.from.Text())
return s.err == nil
}
// Entry returns the last entry that was Scan()'d by the DiffIndexScanner.
func (s *DiffIndexScanner) Entry() *DiffIndexEntry { return s.next }
// Entry returns the last error that was encountered by the DiffIndexScanner.
func (s *DiffIndexScanner) Err() error { return s.err }
// prepareScan clears out the results from the last Scan() loop, and advances
// the internal scanner to fetch a new line of Text().
func (s *DiffIndexScanner) prepareScan() bool {
s.next, s.err = nil, nil
if !s.from.Scan() {
s.err = s.from.Err()
return false
}
return true
}
// scan parses the given line and returns a `*DiffIndexEntry` or an error,
// depending on whether or not the parse was successful.
func (s *DiffIndexScanner) scan(line string) (*DiffIndexEntry, error) {
parts := strings.Split(line, "\t")
if len(parts) < 2 {
return nil, errors.Errorf("invalid line: %s", line)
}
desc := strings.Fields(parts[0])
if len(desc) < 5 {
return nil, errors.Errorf("invalid description: %s", parts[0])
}
entry := &DiffIndexEntry{
SrcMode: strings.TrimPrefix(desc[0], ":"),
DstMode: desc[1],
SrcSha: desc[2],
DstSha: desc[3],
Status: Status(rune(desc[4][0])),
SrcName: parts[1],
}
if score, err := strconv.Atoi(desc[4][1:]); err != nil {
entry.StatusScore = score
}
if len(parts) > 2 {
entry.DstName = parts[2]
}
return entry, nil
}