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 }