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