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 result = append(result, combined)
126 } else {
127 // only in patch1; add as-is
128 result = append(result, f1)
129 }
130
131 visited[fileName] = struct{}{}
132 }
133
134 // for all files in patch2 that remain unvisited; we can just add them into the output
135 for _, f2 := range patch2 {
136 fileName := bestName(f2)
137 if _, ok := visited[fileName]; ok {
138 continue
139 }
140
141 result = append(result, f2)
142 }
143
144 return result
145}
146
147// pairwise combination from first to last patch
148func CombineDiff(patches ...[]*gitdiff.File) []*gitdiff.File {
149 if len(patches) == 0 {
150 return nil
151 }
152
153 if len(patches) == 1 {
154 return patches[0]
155 }
156
157 combined := combineTwo(patches[0], patches[1])
158
159 newPatches := [][]*gitdiff.File{}
160 newPatches = append(newPatches, combined)
161 for i, p := range patches {
162 if i >= 2 {
163 newPatches = append(newPatches, p)
164 }
165 }
166
167 return CombineDiff(newPatches...)
168}