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

     1  package lfs
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/git-lfs/git-lfs/errors"
    10  )
    11  
    12  // Status represents the status of a file that appears in the output of `git
    13  // diff-index`.
    14  //
    15  // More information about each of its valid instances can be found:
    16  // https://git-scm.com/docs/git-diff-index
    17  type DiffIndexStatus rune
    18  
    19  const (
    20  	StatusAddition     DiffIndexStatus = 'A'
    21  	StatusCopy         DiffIndexStatus = 'C'
    22  	StatusDeletion     DiffIndexStatus = 'D'
    23  	StatusModification DiffIndexStatus = 'M'
    24  	StatusRename       DiffIndexStatus = 'R'
    25  	StatusTypeChange   DiffIndexStatus = 'T'
    26  	StatusUnmerged     DiffIndexStatus = 'U'
    27  	StatusUnknown      DiffIndexStatus = 'X'
    28  )
    29  
    30  // String implements fmt.Stringer by returning a human-readable name for each
    31  // status.
    32  func (s DiffIndexStatus) String() string {
    33  	switch s {
    34  	case StatusAddition:
    35  		return "addition"
    36  	case StatusCopy:
    37  		return "copy"
    38  	case StatusDeletion:
    39  		return "deletion"
    40  	case StatusModification:
    41  		return "modification"
    42  	case StatusRename:
    43  		return "rename"
    44  	case StatusTypeChange:
    45  		return "change"
    46  	case StatusUnmerged:
    47  		return "unmerged"
    48  	case StatusUnknown:
    49  		return "unknown"
    50  	}
    51  	return "<unknown>"
    52  }
    53  
    54  // Format implements fmt.Formatter. If printed as "%+d", "%+s", or "%+v", the
    55  // status will be written out as an English word: i.e., "addition", "copy",
    56  // "deletion", etc.
    57  //
    58  // If the '+' flag is not given, the shorthand will be used instead: 'A', 'C',
    59  // and 'D', respectively.
    60  //
    61  // If any other format verb is given, this function will panic().
    62  func (s DiffIndexStatus) Format(state fmt.State, c rune) {
    63  	switch c {
    64  	case 'd', 's', 'v':
    65  		if state.Flag('+') {
    66  			state.Write([]byte(s.String()))
    67  		} else {
    68  			state.Write([]byte{byte(rune(s))})
    69  		}
    70  	default:
    71  		panic(fmt.Sprintf("cannot format %v for DiffIndexStatus", c))
    72  	}
    73  }
    74  
    75  // DiffIndexEntry holds information about a single item in the results of a `git
    76  // diff-index` command.
    77  type DiffIndexEntry struct {
    78  	// SrcMode is the file mode of the "src" file, stored as a string-based
    79  	// octal.
    80  	SrcMode string
    81  	// DstMode is the file mode of the "dst" file, stored as a string-based
    82  	// octal.
    83  	DstMode string
    84  	// SrcSha is the Git blob ID of the "src" file.
    85  	SrcSha string
    86  	// DstSha is the Git blob ID of the "dst" file.
    87  	DstSha string
    88  	// Status is the status of the file in the index.
    89  	Status DiffIndexStatus
    90  	// StatusScore is the optional "score" associated with a particular
    91  	// status.
    92  	StatusScore int
    93  	// SrcName is the name of the file in its "src" state as it appears in
    94  	// the index.
    95  	SrcName string
    96  	// DstName is the name of the file in its "dst" state as it appears in
    97  	// the index.
    98  	DstName string
    99  }
   100  
   101  // DiffIndexScanner scans the output of the `git diff-index` command.
   102  type DiffIndexScanner struct {
   103  	// next is the next entry scanned by the Scanner.
   104  	next *DiffIndexEntry
   105  	// err is any error that the Scanner encountered while scanning.
   106  	err error
   107  
   108  	// from is the underlying scanner, scanning the `git diff-index`
   109  	// command's stdout.
   110  	from *bufio.Scanner
   111  }
   112  
   113  // NewDiffIndexScanner initializes a new `DiffIndexScanner` scanning at the
   114  // given ref, "ref".
   115  //
   116  // If "cache" is given, the DiffIndexScanner will scan for differences between
   117  // the given ref and the index. If "cache" is _not_ given, DiffIndexScanner will
   118  // scan for differences between the given ref and the currently checked out
   119  // tree.
   120  //
   121  // If any error was encountered in starting the command or closing its `stdin`,
   122  // that error will be returned immediately. Otherwise, a `*DiffIndexScanner`
   123  // will be returned with a `nil` error.
   124  func NewDiffIndexScanner(ref string, cached bool) (*DiffIndexScanner, error) {
   125  	cmd, err := startCommand("git", diffIndexCmdArgs(ref, cached)...)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	if err = cmd.Stdin.Close(); err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	return &DiffIndexScanner{
   135  		from: bufio.NewScanner(cmd.Stdout),
   136  	}, nil
   137  }
   138  
   139  // diffIndexCmdArgs returns a string slice containing the arguments necessary
   140  // to run the diff-index command.
   141  func diffIndexCmdArgs(ref string, cached bool) []string {
   142  	args := []string{"diff-index", "-M"}
   143  	if cached {
   144  		args = append(args, "--cached")
   145  	}
   146  	args = append(args, ref)
   147  
   148  	return args
   149  }
   150  
   151  // Scan advances the scan line and yields either a new value for Entry(), or an
   152  // Err(). It returns true or false, whether or not it can continue scanning for
   153  // more entries.
   154  func (s *DiffIndexScanner) Scan() bool {
   155  	if !s.prepareScan() {
   156  		return false
   157  	}
   158  
   159  	s.next, s.err = s.scan(s.from.Text())
   160  	if s.err != nil {
   161  		s.err = errors.Wrap(s.err, "scan")
   162  	}
   163  
   164  	return s.err == nil
   165  }
   166  
   167  // Entry returns the last entry that was Scan()'d by the DiffIndexScanner.
   168  func (s *DiffIndexScanner) Entry() *DiffIndexEntry { return s.next }
   169  
   170  // Entry returns the last error that was encountered by the DiffIndexScanner.
   171  func (s *DiffIndexScanner) Err() error { return s.err }
   172  
   173  // prepareScan clears out the results from the last Scan() loop, and advances
   174  // the internal scanner to fetch a new line of Text().
   175  func (s *DiffIndexScanner) prepareScan() bool {
   176  	s.next, s.err = nil, nil
   177  	if !s.from.Scan() {
   178  		s.err = s.from.Err()
   179  		return false
   180  	}
   181  
   182  	return true
   183  }
   184  
   185  // scan parses the given line and returns a `*DiffIndexEntry` or an error,
   186  // depending on whether or not the parse was successful.
   187  func (s *DiffIndexScanner) scan(line string) (*DiffIndexEntry, error) {
   188  	// Format is:
   189  	//   :100644 100644 c5b3d83a7542255ec7856487baa5e83d65b1624c 9e82ac1b514be060945392291b5b3108c22f6fe3 M foo.gif
   190  	//   :<old mode> <new mode> <old sha1> <new sha1> <status>\t<file name>[\t<file name>]
   191  
   192  	parts := strings.Split(line, "\t")
   193  	if len(parts) < 2 {
   194  		return nil, errors.Errorf("invalid line: %s", line)
   195  	}
   196  
   197  	desc := strings.Fields(parts[0])
   198  	if len(desc) < 5 {
   199  		return nil, errors.Errorf("invalid description: %s", parts[0])
   200  	}
   201  
   202  	entry := &DiffIndexEntry{
   203  		SrcMode: strings.TrimPrefix(desc[0], ":"),
   204  		DstMode: desc[1],
   205  		SrcSha:  desc[2],
   206  		DstSha:  desc[3],
   207  		Status:  DiffIndexStatus(rune(desc[4][0])),
   208  		SrcName: parts[1],
   209  	}
   210  
   211  	if score, err := strconv.Atoi(desc[4][1:]); err != nil {
   212  		entry.StatusScore = score
   213  	}
   214  
   215  	if len(parts) > 2 {
   216  		entry.DstName = parts[2]
   217  	}
   218  
   219  	return entry, nil
   220  }