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