forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package patchutil
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "os/exec"
8 "regexp"
9 "slices"
10 "strings"
11
12 "github.com/bluekeyes/go-gitdiff/gitdiff"
13 "tangled.org/core/types"
14)
15
16func ExtractPatches(formatPatch string) ([]types.FormatPatch, error) {
17 patches := splitFormatPatch(formatPatch)
18
19 result := []types.FormatPatch{}
20
21 for _, patch := range patches {
22 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch))
23 if err != nil {
24 return nil, fmt.Errorf("failed to parse patch: %w", err)
25 }
26
27 header, err := gitdiff.ParsePatchHeader(headerStr)
28 if err != nil {
29 return nil, fmt.Errorf("failed to parse patch header: %w", err)
30 }
31
32 result = append(result, types.FormatPatch{
33 Files: files,
34 PatchHeader: header,
35 Raw: patch,
36 })
37 }
38
39 return result, nil
40}
41
42// IsPatchValid checks if the given patch string is valid.
43// It performs very basic sniffing for either git-diff or git-format-patch
44// header lines. For format patches, it attempts to extract and validate each one.
45func IsPatchValid(patch string) bool {
46 if len(patch) == 0 {
47 return false
48 }
49
50 lines := strings.Split(patch, "\n")
51 if len(lines) < 2 {
52 return false
53 }
54
55 firstLine := strings.TrimSpace(lines[0])
56
57 // check if it's a git diff
58 if strings.HasPrefix(firstLine, "diff ") ||
59 strings.HasPrefix(firstLine, "--- ") ||
60 strings.HasPrefix(firstLine, "Index: ") ||
61 strings.HasPrefix(firstLine, "+++ ") ||
62 strings.HasPrefix(firstLine, "@@ ") {
63 return true
64 }
65
66 // check if it's format-patch
67 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
68 strings.HasPrefix(firstLine, "From: ") {
69 // ExtractPatches already runs it through gitdiff.Parse so if that errors,
70 // it's safe to say it's broken.
71 patches, err := ExtractPatches(patch)
72 if err != nil {
73 return false
74 }
75 return len(patches) > 0
76 }
77
78 return false
79}
80
81func IsFormatPatch(patch string) bool {
82 lines := strings.Split(patch, "\n")
83 if len(lines) < 2 {
84 return false
85 }
86
87 firstLine := strings.TrimSpace(lines[0])
88 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
89 return true
90 }
91
92 headerCount := 0
93 for i := range min(10, len(lines)) {
94 line := strings.TrimSpace(lines[i])
95 if strings.HasPrefix(line, "From: ") ||
96 strings.HasPrefix(line, "Date: ") ||
97 strings.HasPrefix(line, "Subject: ") ||
98 strings.HasPrefix(line, "commit ") {
99 headerCount++
100 }
101 }
102
103 return headerCount >= 2
104}
105
106func splitFormatPatch(patchText string) []string {
107 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
108
109 indexes := re.FindAllStringIndex(patchText, -1)
110
111 if len(indexes) == 0 {
112 return []string{}
113 }
114
115 patches := make([]string, len(indexes))
116
117 for i := range indexes {
118 startPos := indexes[i][0]
119 endPos := len(patchText)
120
121 if i < len(indexes)-1 {
122 endPos = indexes[i+1][0]
123 }
124
125 patches[i] = strings.TrimSpace(patchText[startPos:endPos])
126 }
127 return patches
128}
129
130func bestName(file *gitdiff.File) string {
131 if file.IsDelete {
132 return file.OldName
133 } else {
134 return file.NewName
135 }
136}
137
138// in-place reverse of a diff
139func reverseDiff(file *gitdiff.File) {
140 file.OldName, file.NewName = file.NewName, file.OldName
141 file.OldMode, file.NewMode = file.NewMode, file.OldMode
142 file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment
143
144 for _, fragment := range file.TextFragments {
145 // swap postions
146 fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition
147 fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines
148 fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded
149
150 for i := range fragment.Lines {
151 switch fragment.Lines[i].Op {
152 case gitdiff.OpAdd:
153 fragment.Lines[i].Op = gitdiff.OpDelete
154 case gitdiff.OpDelete:
155 fragment.Lines[i].Op = gitdiff.OpAdd
156 default:
157 // do nothing
158 }
159 }
160 }
161}
162
163func Unified(oldText, oldFile, newText, newFile string) (string, error) {
164 oldTemp, err := os.CreateTemp("", "old_*")
165 if err != nil {
166 return "", fmt.Errorf("failed to create temp file for oldText: %w", err)
167 }
168 defer os.Remove(oldTemp.Name())
169 if _, err := oldTemp.WriteString(oldText); err != nil {
170 return "", fmt.Errorf("failed to write to old temp file: %w", err)
171 }
172 oldTemp.Close()
173
174 newTemp, err := os.CreateTemp("", "new_*")
175 if err != nil {
176 return "", fmt.Errorf("failed to create temp file for newText: %w", err)
177 }
178 defer os.Remove(newTemp.Name())
179 if _, err := newTemp.WriteString(newText); err != nil {
180 return "", fmt.Errorf("failed to write to new temp file: %w", err)
181 }
182 newTemp.Close()
183
184 cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())
185 output, err := cmd.CombinedOutput()
186
187 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
188 return string(output), nil
189 }
190 if err != nil {
191 return "", fmt.Errorf("diff command failed: %w", err)
192 }
193
194 return string(output), nil
195}
196
197// are two patches identical
198func Equal(a, b []*gitdiff.File) bool {
199 return slices.EqualFunc(a, b, func(x, y *gitdiff.File) bool {
200 // same pointer
201 if x == y {
202 return true
203 }
204 if x == nil || y == nil {
205 return x == y
206 }
207
208 // compare file metadata
209 if x.OldName != y.OldName || x.NewName != y.NewName {
210 return false
211 }
212 if x.OldMode != y.OldMode || x.NewMode != y.NewMode {
213 return false
214 }
215 if x.IsNew != y.IsNew || x.IsDelete != y.IsDelete || x.IsCopy != y.IsCopy || x.IsRename != y.IsRename {
216 return false
217 }
218
219 if len(x.TextFragments) != len(y.TextFragments) {
220 return false
221 }
222
223 for i, xFrag := range x.TextFragments {
224 yFrag := y.TextFragments[i]
225
226 // Compare fragment headers
227 if xFrag.OldPosition != yFrag.OldPosition || xFrag.OldLines != yFrag.OldLines ||
228 xFrag.NewPosition != yFrag.NewPosition || xFrag.NewLines != yFrag.NewLines {
229 return false
230 }
231
232 // Compare fragment changes
233 if len(xFrag.Lines) != len(yFrag.Lines) {
234 return false
235 }
236
237 for j, xLine := range xFrag.Lines {
238 yLine := yFrag.Lines[j]
239 if xLine.Op != yLine.Op || xLine.Line != yLine.Line {
240 return false
241 }
242 }
243 }
244
245 return true
246 })
247}
248
249// sort patch files in alphabetical order
250func SortPatch(patch []*gitdiff.File) {
251 slices.SortFunc(patch, func(a, b *gitdiff.File) int {
252 return strings.Compare(bestName(a), bestName(b))
253 })
254}
255
256func AsDiff(patch string) ([]*gitdiff.File, error) {
257 // if format-patch; then extract each patch
258 var diffs []*gitdiff.File
259 if IsFormatPatch(patch) {
260 patches, err := ExtractPatches(patch)
261 if err != nil {
262 return nil, err
263 }
264 var ps [][]*gitdiff.File
265 for _, p := range patches {
266 ps = append(ps, p.Files)
267 }
268
269 diffs = CombineDiff(ps...)
270 } else {
271 d, _, err := gitdiff.Parse(strings.NewReader(patch))
272 if err != nil {
273 return nil, err
274 }
275 diffs = d
276 }
277
278 return diffs, nil
279}
280
281func AsNiceDiff(patch, targetBranch string) types.NiceDiff {
282 diffs, err := AsDiff(patch)
283 if err != nil {
284 log.Println(err)
285 }
286
287 nd := types.NiceDiff{}
288 nd.Commit.Parent = targetBranch
289
290 for _, d := range diffs {
291 ndiff := types.Diff{}
292 ndiff.Name.New = d.NewName
293 ndiff.Name.Old = d.OldName
294 ndiff.IsBinary = d.IsBinary
295 ndiff.IsNew = d.IsNew
296 ndiff.IsDelete = d.IsDelete
297 ndiff.IsCopy = d.IsCopy
298 ndiff.IsRename = d.IsRename
299
300 for _, tf := range d.TextFragments {
301 ndiff.TextFragments = append(ndiff.TextFragments, *tf)
302 for _, l := range tf.Lines {
303 switch l.Op {
304 case gitdiff.OpAdd:
305 nd.Stat.Insertions += 1
306 case gitdiff.OpDelete:
307 nd.Stat.Deletions += 1
308 }
309 }
310 }
311
312 nd.Diff = append(nd.Diff, ndiff)
313 }
314
315 nd.Stat.FilesChanged = len(diffs)
316
317 return nd
318}