1package patchutil
2
3import (
4 "bytes"
5 "fmt"
6 "strings"
7
8 "github.com/bluekeyes/go-gitdiff/gitdiff"
9)
10
11type Line struct {
12 LineNumber int64
13 Content string
14 IsUnknown bool
15}
16
17func NewLineAt(lineNumber int64, content string) Line {
18 return Line{
19 LineNumber: lineNumber,
20 Content: content,
21 IsUnknown: false,
22 }
23}
24
25type Image struct {
26 File string
27 Data []*Line
28}
29
30func (r *Image) String() string {
31 var i, j int64
32 var b strings.Builder
33 for {
34 i += 1
35
36 if int(j) >= (len(r.Data)) {
37 break
38 }
39
40 if r.Data[j].LineNumber == i {
41 // b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber))
42 b.WriteString(r.Data[j].Content)
43 j += 1
44 } else {
45 //b.WriteString(fmt.Sprintf("%d:\n", i))
46 b.WriteString("\n")
47 }
48 }
49
50 return b.String()
51}
52
53func (r *Image) AddLine(line *Line) {
54 r.Data = append(r.Data, line)
55}
56
57// rebuild the original file from a patch
58func CreatePreImage(file *gitdiff.File) Image {
59 rf := Image{
60 File: bestName(file),
61 }
62
63 for _, fragment := range file.TextFragments {
64 position := fragment.OldPosition
65 for _, line := range fragment.Lines {
66 switch line.Op {
67 case gitdiff.OpContext:
68 rl := NewLineAt(position, line.Line)
69 rf.Data = append(rf.Data, &rl)
70 position += 1
71 case gitdiff.OpDelete:
72 rl := NewLineAt(position, line.Line)
73 rf.Data = append(rf.Data, &rl)
74 position += 1
75 case gitdiff.OpAdd:
76 // do nothing here
77 }
78 }
79 }
80
81 return rf
82}
83
84// rebuild the revised file from a patch
85func CreatePostImage(file *gitdiff.File) Image {
86 rf := Image{
87 File: bestName(file),
88 }
89
90 for _, fragment := range file.TextFragments {
91 position := fragment.NewPosition
92 for _, line := range fragment.Lines {
93 switch line.Op {
94 case gitdiff.OpContext:
95 rl := NewLineAt(position, line.Line)
96 rf.Data = append(rf.Data, &rl)
97 position += 1
98 case gitdiff.OpAdd:
99 rl := NewLineAt(position, line.Line)
100 rf.Data = append(rf.Data, &rl)
101 position += 1
102 case gitdiff.OpDelete:
103 // do nothing here
104 }
105 }
106 }
107
108 return rf
109}
110
111type MergeError struct {
112 msg string
113 mismatchingLine int64
114}
115
116func (m MergeError) Error() string {
117 return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine)
118}
119
120// best effort merging of two reconstructed files
121func (this *Image) Merge(other *Image) (*Image, error) {
122 mergedFile := Image{}
123
124 var i, j int64
125
126 for int(i) < len(this.Data) || int(j) < len(other.Data) {
127 if int(i) >= len(this.Data) {
128 // first file is done; the rest of the lines from file 2 can go in
129 mergedFile.AddLine(other.Data[j])
130 j++
131 continue
132 }
133
134 if int(j) >= len(other.Data) {
135 // first file is done; the rest of the lines from file 2 can go in
136 mergedFile.AddLine(this.Data[i])
137 i++
138 continue
139 }
140
141 line1 := this.Data[i]
142 line2 := other.Data[j]
143
144 if line1.LineNumber == line2.LineNumber {
145 if line1.Content != line2.Content {
146 return nil, MergeError{
147 msg: "mismatching lines, this patch might have undergone rebase",
148 mismatchingLine: line1.LineNumber,
149 }
150 } else {
151 mergedFile.AddLine(line1)
152 }
153 i++
154 j++
155 } else if line1.LineNumber < line2.LineNumber {
156 mergedFile.AddLine(line1)
157 i++
158 } else {
159 mergedFile.AddLine(line2)
160 j++
161 }
162 }
163
164 return &mergedFile, nil
165}
166
167func (r *Image) Apply(patch *gitdiff.File) (string, error) {
168 original := r.String()
169 var buffer bytes.Buffer
170 reader := strings.NewReader(original)
171
172 err := gitdiff.Apply(&buffer, reader, patch)
173 if err != nil {
174 return "", err
175 }
176
177 return buffer.String(), nil
178}