1package patchutil
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bluekeyes/go-gitdiff/gitdiff"
8 "tangled.org/core/appview/filetree"
9 "tangled.org/core/types"
10)
11
12type InterdiffResult struct {
13 Files []*InterdiffFile
14}
15
16func (i *InterdiffResult) Stats() types.DiffStat {
17 var ins, del int64
18 for _, s := range i.ChangedFiles() {
19 stat := s.Stats()
20 ins += stat.Insertions
21 del += stat.Deletions
22 }
23 return types.DiffStat{
24 Insertions: ins,
25 Deletions: del,
26 FilesChanged: len(i.Files),
27 }
28}
29
30func (i *InterdiffResult) ChangedFiles() []types.DiffFileRenderer {
31 drs := make([]types.DiffFileRenderer, len(i.Files))
32 for i, s := range i.Files {
33 drs[i] = s
34 }
35 return drs
36}
37
38func (i *InterdiffResult) FileTree() *filetree.FileTreeNode {
39 fs := make([]string, len(i.Files))
40 for i, s := range i.Files {
41 fs[i] = s.Name
42 }
43 return filetree.FileTree(fs)
44}
45
46func (i *InterdiffResult) String() string {
47 var b strings.Builder
48 for _, f := range i.Files {
49 b.WriteString(f.String())
50 b.WriteString("\n")
51 }
52
53 return b.String()
54}
55
56type InterdiffFile struct {
57 *gitdiff.File
58 Name string
59 Status InterdiffFileStatus
60}
61
62func (s *InterdiffFile) Id() string {
63 return s.Name
64}
65
66func (s *InterdiffFile) Split() types.SplitDiff {
67 fragments := make([]types.SplitFragment, len(s.TextFragments))
68
69 for i, fragment := range s.TextFragments {
70 leftLines, rightLines := types.SeparateLines(fragment)
71
72 fragments[i] = types.SplitFragment{
73 Header: fragment.Header(),
74 LeftLines: leftLines,
75 RightLines: rightLines,
76 }
77 }
78
79 return types.SplitDiff{
80 Name: s.Id(),
81 TextFragments: fragments,
82 }
83}
84
85func (s *InterdiffFile) CanRender() string {
86 if s.Status.IsUnchanged() {
87 return "This file has not been changed."
88 } else if s.Status.IsRebased() {
89 return "This patch was likely rebased, as context lines do not match."
90 } else if s.Status.IsError() {
91 return "Failed to calculate interdiff for this file."
92 } else {
93 return ""
94 }
95}
96
97func (s *InterdiffFile) Names() types.DiffFileName {
98 var n types.DiffFileName
99 n.New = s.Name
100 return n
101}
102
103func (s *InterdiffFile) Stats() types.DiffFileStat {
104 var ins, del int64
105
106 if s.File != nil {
107 for _, f := range s.TextFragments {
108 ins += f.LinesAdded
109 del += f.LinesDeleted
110 }
111 }
112
113 return types.DiffFileStat{
114 Insertions: ins,
115 Deletions: del,
116 }
117}
118
119func (s *InterdiffFile) String() string {
120 var b strings.Builder
121 b.WriteString(s.Status.String())
122 b.WriteString(" ")
123
124 if s.File != nil {
125 b.WriteString(bestName(s.File))
126 b.WriteString("\n")
127 b.WriteString(s.File.String())
128 }
129
130 return b.String()
131}
132
133type InterdiffFileStatus struct {
134 StatusKind StatusKind
135 Error error
136}
137
138func (s *InterdiffFileStatus) String() string {
139 kind := s.StatusKind.String()
140 if s.Error != nil {
141 return fmt.Sprintf("%s [%s]", kind, s.Error.Error())
142 } else {
143 return kind
144 }
145}
146
147func (s *InterdiffFileStatus) IsOk() bool {
148 return s.StatusKind == StatusOk
149}
150
151func (s *InterdiffFileStatus) IsUnchanged() bool {
152 return s.StatusKind == StatusUnchanged
153}
154
155func (s *InterdiffFileStatus) IsOnlyInOne() bool {
156 return s.StatusKind == StatusOnlyInOne
157}
158
159func (s *InterdiffFileStatus) IsOnlyInTwo() bool {
160 return s.StatusKind == StatusOnlyInTwo
161}
162
163func (s *InterdiffFileStatus) IsRebased() bool {
164 return s.StatusKind == StatusRebased
165}
166
167func (s *InterdiffFileStatus) IsError() bool {
168 return s.StatusKind == StatusError
169}
170
171type StatusKind int
172
173func (k StatusKind) String() string {
174 switch k {
175 case StatusOnlyInOne:
176 return "only in one"
177 case StatusOnlyInTwo:
178 return "only in two"
179 case StatusUnchanged:
180 return "unchanged"
181 case StatusRebased:
182 return "rebased"
183 case StatusError:
184 return "error"
185 default:
186 return "changed"
187 }
188}
189
190const (
191 StatusOk StatusKind = iota
192 StatusOnlyInOne
193 StatusOnlyInTwo
194 StatusUnchanged
195 StatusRebased
196 StatusError
197)
198
199func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {
200 re1 := CreatePreImage(f1)
201 re2 := CreatePreImage(f2)
202
203 interdiffFile := InterdiffFile{
204 Name: bestName(f1),
205 }
206
207 merged, err := re1.Merge(&re2)
208 if err != nil {
209 interdiffFile.Status = InterdiffFileStatus{
210 StatusKind: StatusRebased,
211 Error: err,
212 }
213 return &interdiffFile
214 }
215
216 rev1, err := merged.Apply(f1)
217 if err != nil {
218 interdiffFile.Status = InterdiffFileStatus{
219 StatusKind: StatusError,
220 Error: err,
221 }
222 return &interdiffFile
223 }
224
225 rev2, err := merged.Apply(f2)
226 if err != nil {
227 interdiffFile.Status = InterdiffFileStatus{
228 StatusKind: StatusError,
229 Error: err,
230 }
231 return &interdiffFile
232 }
233
234 diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))
235 if err != nil {
236 interdiffFile.Status = InterdiffFileStatus{
237 StatusKind: StatusError,
238 Error: err,
239 }
240 return &interdiffFile
241 }
242
243 parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
244 if err != nil {
245 interdiffFile.Status = InterdiffFileStatus{
246 StatusKind: StatusError,
247 Error: err,
248 }
249 return &interdiffFile
250 }
251
252 if len(parsed) != 1 {
253 // files are identical?
254 interdiffFile.Status = InterdiffFileStatus{
255 StatusKind: StatusUnchanged,
256 }
257 return &interdiffFile
258 }
259
260 if interdiffFile.Status.StatusKind == StatusOk {
261 interdiffFile.File = parsed[0]
262 }
263
264 return &interdiffFile
265}
266
267func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult {
268 fileToIdx1 := make(map[string]int)
269 fileToIdx2 := make(map[string]int)
270 visited := make(map[string]struct{})
271 var result InterdiffResult
272
273 for idx, f := range patch1 {
274 fileToIdx1[bestName(f)] = idx
275 }
276
277 for idx, f := range patch2 {
278 fileToIdx2[bestName(f)] = idx
279 }
280
281 for _, f1 := range patch1 {
282 var interdiffFile *InterdiffFile
283
284 fileName := bestName(f1)
285 if idx, ok := fileToIdx2[fileName]; ok {
286 f2 := patch2[idx]
287
288 // we have f1 and f2, calculate interdiff
289 interdiffFile = interdiffFiles(f1, f2)
290 } else {
291 // only in patch 1, this change would have to be "inverted" to dissapear
292 // from patch 2, so we reverseDiff(f1)
293 reverseDiff(f1)
294
295 interdiffFile = &InterdiffFile{
296 File: f1,
297 Name: fileName,
298 Status: InterdiffFileStatus{
299 StatusKind: StatusOnlyInOne,
300 },
301 }
302 }
303
304 result.Files = append(result.Files, interdiffFile)
305 visited[fileName] = struct{}{}
306 }
307
308 // for all files in patch2 that remain unvisited; we can just add them into the output
309 for _, f2 := range patch2 {
310 fileName := bestName(f2)
311 if _, ok := visited[fileName]; ok {
312 continue
313 }
314
315 result.Files = append(result.Files, &InterdiffFile{
316 File: f2,
317 Name: fileName,
318 Status: InterdiffFileStatus{
319 StatusKind: StatusOnlyInTwo,
320 },
321 })
322 }
323
324 return &result
325}