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