forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}