1package patchutil
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bluekeyes/go-gitdiff/gitdiff"
8)
9
10// original1 -> patch1 -> rev1
11// original2 -> patch2 -> rev2
12//
13// original2 must be equal to rev1, so we can merge them to get maximal context
14//
15// finally,
16// rev2' <- apply(patch2, merged)
17// combineddiff <- diff(rev2', original1)
18func combineFiles(file1, file2 *gitdiff.File) (*gitdiff.File, error) {
19 fileName := bestName(file1)
20
21 o1 := CreatePreImage(file1)
22 r1 := CreatePostImage(file1)
23 o2 := CreatePreImage(file2)
24
25 merged, err := r1.Merge(&o2)
26 if err != nil {
27 return nil, err
28 }
29
30 r2Prime, err := merged.Apply(file2)
31 if err != nil {
32 return nil, err
33 }
34
35 // produce combined diff
36 diff, err := Unified(o1.String(), fileName, r2Prime, fileName)
37 if err != nil {
38 return nil, err
39 }
40
41 parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
42
43 if len(parsed) != 1 {
44 // no diff? the second commit reverted the changes from the first
45 return nil, nil
46 }
47
48 return parsed[0], nil
49}
50
51// use empty lines for lines we are unaware of
52//
53// this raises an error only if the two patches were invalid or non-contiguous
54func mergeLines(old, new string) (string, error) {
55 var i, j int
56
57 // TODO: use strings.Lines
58 linesOld := strings.Split(old, "\n")
59 linesNew := strings.Split(new, "\n")
60
61 result := []string{}
62
63 for i < len(linesOld) || j < len(linesNew) {
64 if i >= len(linesOld) {
65 // rest of the file is populated from `new`
66 result = append(result, linesNew[j])
67 j++
68 continue
69 }
70
71 if j >= len(linesNew) {
72 // rest of the file is populated from `old`
73 result = append(result, linesOld[i])
74 i++
75 continue
76 }
77
78 oldLine := linesOld[i]
79 newLine := linesNew[j]
80
81 if oldLine != newLine && (oldLine != "" && newLine != "") {
82 // context mismatch
83 return "", fmt.Errorf("failed to merge files, found context mismatch at %d; oldLine: `%s`, newline: `%s`", i+1, oldLine, newLine)
84 }
85
86 if oldLine == newLine {
87 result = append(result, oldLine)
88 } else if oldLine == "" {
89 result = append(result, newLine)
90 } else if newLine == "" {
91 result = append(result, oldLine)
92 }
93 i++
94 j++
95 }
96
97 return strings.Join(result, "\n"), nil
98}
99
100func combineTwo(patch1, patch2 []*gitdiff.File) []*gitdiff.File {
101 fileToIdx1 := make(map[string]int)
102 fileToIdx2 := make(map[string]int)
103 visited := make(map[string]struct{})
104 var result []*gitdiff.File
105
106 for idx, f := range patch1 {
107 fileToIdx1[bestName(f)] = idx
108 }
109
110 for idx, f := range patch2 {
111 fileToIdx2[bestName(f)] = idx
112 }
113
114 for _, f1 := range patch1 {
115 fileName := bestName(f1)
116 if idx, ok := fileToIdx2[fileName]; ok {
117 f2 := patch2[idx]
118
119 // we have f1 and f2, combine them
120 combined, err := combineFiles(f1, f2)
121 if err != nil {
122 // fmt.Println(err)
123 }
124
125 // combined can be nil commit 2 reverted all changes from commit 1
126 if combined != nil {
127 result = append(result, combined)
128 }
129
130 } else {
131 // only in patch1; add as-is
132 result = append(result, f1)
133 }
134
135 visited[fileName] = struct{}{}
136 }
137
138 // for all files in patch2 that remain unvisited; we can just add them into the output
139 for _, f2 := range patch2 {
140 fileName := bestName(f2)
141 if _, ok := visited[fileName]; ok {
142 continue
143 }
144
145 result = append(result, f2)
146 }
147
148 return result
149}
150
151// pairwise combination from first to last patch
152func CombineDiff(patches ...[]*gitdiff.File) []*gitdiff.File {
153 if len(patches) == 0 {
154 return nil
155 }
156
157 if len(patches) == 1 {
158 return patches[0]
159 }
160
161 combined := combineTwo(patches[0], patches[1])
162
163 newPatches := [][]*gitdiff.File{}
164 newPatches = append(newPatches, combined)
165 for i, p := range patches {
166 if i >= 2 {
167 newPatches = append(newPatches, p)
168 }
169 }
170
171 return CombineDiff(newPatches...)
172}