forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package patchutil
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "regexp"
8 "slices"
9 "strings"
10
11 "github.com/bluekeyes/go-gitdiff/gitdiff"
12)
13
14type FormatPatch struct {
15 Files []*gitdiff.File
16 *gitdiff.PatchHeader
17 Raw string
18}
19
20func (f FormatPatch) ChangeId() (string, error) {
21 if vals, ok := f.RawHeaders["Change-Id"]; ok && len(vals) == 1 {
22 return vals[0], nil
23 }
24 return "", fmt.Errorf("no change-id found")
25}
26
27func ExtractPatches(formatPatch string) ([]FormatPatch, error) {
28 patches := splitFormatPatch(formatPatch)
29
30 result := []FormatPatch{}
31
32 for _, patch := range patches {
33 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch))
34 if err != nil {
35 return nil, fmt.Errorf("failed to parse patch: %w", err)
36 }
37
38 header, err := gitdiff.ParsePatchHeader(headerStr)
39 if err != nil {
40 return nil, fmt.Errorf("failed to parse patch header: %w", err)
41 }
42
43 result = append(result, FormatPatch{
44 Files: files,
45 PatchHeader: header,
46 Raw: patch,
47 })
48 }
49
50 return result, nil
51}
52
53// IsPatchValid checks if the given patch string is valid.
54// It performs very basic sniffing for either git-diff or git-format-patch
55// header lines. For format patches, it attempts to extract and validate each one.
56func IsPatchValid(patch string) bool {
57 if len(patch) == 0 {
58 return false
59 }
60
61 lines := strings.Split(patch, "\n")
62 if len(lines) < 2 {
63 return false
64 }
65
66 firstLine := strings.TrimSpace(lines[0])
67
68 // check if it's a git diff
69 if strings.HasPrefix(firstLine, "diff ") ||
70 strings.HasPrefix(firstLine, "--- ") ||
71 strings.HasPrefix(firstLine, "Index: ") ||
72 strings.HasPrefix(firstLine, "+++ ") ||
73 strings.HasPrefix(firstLine, "@@ ") {
74 return true
75 }
76
77 // check if it's format-patch
78 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
79 strings.HasPrefix(firstLine, "From: ") {
80 // ExtractPatches already runs it through gitdiff.Parse so if that errors,
81 // it's safe to say it's broken.
82 patches, err := ExtractPatches(patch)
83 if err != nil {
84 return false
85 }
86 return len(patches) > 0
87 }
88
89 return false
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}