-
Notifications
You must be signed in to change notification settings - Fork 0
/
diff.go
257 lines (212 loc) · 5.95 KB
/
diff.go
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package git
import (
"bufio"
"strconv"
"strings"
"github.com/purpleclay/chomp"
"github.com/purpleclay/gitz/scan"
)
const (
// git diff header delimiter > @@ ... @@
hdrDelim = "@@"
// prefix for lines added
addPrefix = "+"
// prefix for lines removed
remPrefix = "-"
)
// DiffOption provides a way for setting specific options during a diff
// operation. Each supported option can customize the way the diff is
// executed against the current repository (working directory)
type DiffOption func(*diffOptions)
type diffOptions struct {
DiffPaths []string
}
// WithDiffPaths allows the diff to be targeted to specific files and
// folers within the current repository (working directory). Paths to
// files and folders are relative to the root of the repository. All
// leading and trailing whitepsace will be trimmed from the file paths,
// allowing empty paths to be ignored
func WithDiffPaths(paths ...string) DiffOption {
return func(opts *diffOptions) {
opts.DiffPaths = trim(paths...)
}
}
// FileDiff represents a snapshot containing all of the changes to
// a file within a repository (working directory)
type FileDiff struct {
// Path of the file within the repository (working directory)
Path string
// DiffChunk contains all of the identified changes within
// the file
Chunks []DiffChunk
}
// DiffChunk represents a snapshot of a single change (chunk) to
// a file within a repository (working directory)
type DiffChunk struct {
// Added optionally contains details of the text that has
// been added to a file as part of the current change
Added DiffChange
// Removed optionally contains details of the text that has
// been removed from a file as part of the current change
Removed DiffChange
}
// DiffChange captures details about an individual chunk
// within a git diff. It contains both the changed text and
// its exact position (and line count) within the file
type DiffChange struct {
// LineNo is the position within the file where the
// change starts
LineNo int
// Count is the number of lines that has changed
Count int
// Change contains the text that has changed
Change string
}
// Diff captures the changes made to files within the current repository (working
// directory). Options can be provided to customize how the current diff is
// determined. By default, all diffs (or changes) to files within the repository
// will be retrieved. The diff is generated using the following git options:
//
// git diff -U0 --no-color
func (c *Client) Diff(opts ...DiffOption) ([]FileDiff, error) {
options := &diffOptions{}
for _, opt := range opts {
opt(options)
}
var buf strings.Builder
buf.WriteString("git diff -U0 --no-color")
if len(options.DiffPaths) > 0 {
buf.WriteString(" -- ")
buf.WriteString(strings.Join(options.DiffPaths, " "))
}
out, err := c.exec(buf.String())
if err != nil {
return nil, err
}
return parseDiffs(out)
}
func parseDiffs(log string) ([]FileDiff, error) {
var diffs []FileDiff
scanner := bufio.NewScanner(strings.NewReader(log))
scanner.Split(scan.DiffLines())
for scanner.Scan() {
diff, err := parseDiff(scanner.Text())
if err != nil {
return nil, err
}
diffs = append(diffs, diff)
}
return diffs, nil
}
func parseDiff(diff string) (FileDiff, error) {
rem, path, err := diffPath()(diff)
if err != nil {
return FileDiff{}, err
}
rem, _, err = chomp.Until(hdrDelim)(rem)
if err != nil {
return FileDiff{}, err
}
chunks, err := diffChunks(rem)
if err != nil {
return FileDiff{}, err
}
return FileDiff{
Path: path,
Chunks: chunks,
}, nil
}
func diffPath() chomp.Combinator[string] {
return func(s string) (string, string, error) {
var rem string
var err error
if rem, _, err = chomp.Tag("diff --git ")(s); err != nil {
return rem, "", err
}
var path string
if rem, path, err = chomp.Until(" ")(rem); err != nil {
return rem, "", err
}
path = path[strings.Index(path, "/")+1:]
rem, _, err = chomp.Eol()(rem)
return rem, path, err
}
}
func diffChunks(in string) ([]DiffChunk, error) {
_, chunks, err := chomp.Map(chomp.Many(diffChunk()),
func(in []string) []DiffChunk {
var diffChunks []DiffChunk
for i := 0; i+5 < len(in); i += 6 {
chunk := DiffChunk{
Removed: DiffChange{
LineNo: mustInt(in[i]),
Count: mustInt(in[i+1]),
Change: in[i+4],
},
Added: DiffChange{
LineNo: mustInt(in[i+2]),
Count: mustInt(in[i+3]),
Change: in[i+5],
},
}
if chunk.Added.Count == 0 {
chunk.Added.Count = 1
}
if chunk.Removed.Count == 0 {
chunk.Removed.Count = 1
}
diffChunks = append(diffChunks, chunk)
}
return diffChunks
},
)(in)
return chunks, err
}
func mustInt(in string) int {
out, _ := strconv.Atoi(in)
return out
}
func diffChunk() chomp.Combinator[[]string] {
return func(s string) (string, []string, error) {
var rem string
var err error
var changes []string
rem, changes, err = chomp.Delimited(
chomp.Tag(hdrDelim+" "),
chomp.SepPair(diffChunkHeaderChange(remPrefix), chomp.Tag(" "), diffChunkHeaderChange(addPrefix)),
chomp.Eol(),
)(s)
if err != nil {
return rem, nil, err
}
var removed string
rem, removed, err = chomp.Map(
chomp.ManyN(chomp.Prefixed(chomp.Eol(), chomp.Tag(remPrefix)), 0),
func(in []string) string { return strings.Join(in, "\n") },
)(rem)
if err != nil {
return rem, nil, err
}
var added string
rem, added, err = chomp.Map(
chomp.ManyN(chomp.Prefixed(chomp.Eol(), chomp.Tag(addPrefix)), 0),
func(in []string) string { return strings.Join(in, "\n") },
)(rem)
if err != nil {
return rem, nil, err
}
return rem, append(changes, removed, added), nil
}
}
func diffChunkHeaderChange(prefix string) chomp.Combinator[[]string] {
return func(s string) (string, []string, error) {
rem, _, err := chomp.Tag(prefix)(s)
if err != nil {
return rem, nil, err
}
return chomp.All(
chomp.While(chomp.IsDigit),
chomp.Opt(chomp.Prefixed(chomp.While(chomp.IsDigit), chomp.Tag(","))),
)(rem)
}
}