File: diff_index_scanner.go

package info (click to toggle)
git-lfs 3.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,808 kB
  • sloc: sh: 21,256; makefile: 507; ruby: 417
file content (214 lines) | stat: -rw-r--r-- 6,424 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package lfs

import (
	"bufio"
	"fmt"
	"strconv"
	"strings"

	"github.com/git-lfs/git-lfs/v3/errors"
	"github.com/git-lfs/git-lfs/v3/git"
	"github.com/git-lfs/git-lfs/v3/tr"
)

// 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 DiffIndexStatus rune

const (
	StatusAddition     DiffIndexStatus = 'A'
	StatusCopy         DiffIndexStatus = 'C'
	StatusDeletion     DiffIndexStatus = 'D'
	StatusModification DiffIndexStatus = 'M'
	StatusRename       DiffIndexStatus = 'R'
	StatusTypeChange   DiffIndexStatus = 'T'
	StatusUnmerged     DiffIndexStatus = 'U'
	StatusUnknown      DiffIndexStatus = 'X'
)

// String implements fmt.Stringer by returning a human-readable name for each
// status.
func (s DiffIndexStatus) 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>"
}

// Format implements fmt.Formatter. If printed as "%+d", "%+s", or "%+v", the
// status will be written out as an English word: i.e., "addition", "copy",
// "deletion", etc.
//
// If the '+' flag is not given, the shorthand will be used instead: 'A', 'C',
// and 'D', respectively.
//
// If any other format verb is given, this function will panic().
func (s DiffIndexStatus) Format(state fmt.State, c rune) {
	switch c {
	case 'd', 's', 'v':
		if state.Flag('+') {
			state.Write([]byte(s.String()))
		} else {
			state.Write([]byte{byte(rune(s))})
		}
	default:
		panic(tr.Tr.Get("cannot format %v for DiffIndexStatus", c))
	}
}

// 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 DiffIndexStatus
	// StatusScore is the optional "score" associated with a particular
	// status.
	StatusScore int
	// SrcName is the name of the file in its "src" state as it appears in
	// the index.
	SrcName string
	// DstName is the name of the file in its "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 "refresh" is given, the DiffIndexScanner will refresh the index.  This is
// probably what you want in all cases except fsck, where invoking a filtering
// operation would be undesirable due to the possibility of corruption. It can
// also be disabled where another operation will have refreshed the index.
//
// If "workingDir" is set, the DiffIndexScanner will be run in the given
// directory. Otherwise, the DiffIndexScanner will be run in the current
// working directory.
//
// 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, refresh bool, workingDir string) (*DiffIndexScanner, error) {
	scanner, err := git.DiffIndex(ref, cached, refresh, workingDir)
	if err != nil {
		return nil, err
	}
	return &DiffIndexScanner{
		from: scanner,
	}, nil
}

// 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())
	if s.err != nil {
		s.err = errors.Wrap(s.err, tr.Tr.Get("`git diff-index` scan"))
	}

	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) {
	// Format is:
	//   :100644 100644 c5b3d83a7542255ec7856487baa5e83d65b1624c 9e82ac1b514be060945392291b5b3108c22f6fe3 M foo.gif
	//   :<old mode> <new mode> <old sha1> <new sha1> <status>\t<file name>[\t<file name>]

	parts := strings.Split(line, "\t")
	if len(parts) < 2 {
		return nil, errors.Errorf(tr.Tr.Get("invalid line: %s", line))
	}

	desc := strings.Fields(parts[0])
	if len(desc) < 5 {
		return nil, errors.Errorf(tr.Tr.Get("invalid description: %s", parts[0]))
	}

	entry := &DiffIndexEntry{
		SrcMode: strings.TrimPrefix(desc[0], ":"),
		DstMode: desc[1],
		SrcSha:  desc[2],
		DstSha:  desc[3],
		Status:  DiffIndexStatus(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
}