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