1package patchutil
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "regexp"
8 "strings"
9
10 "github.com/bluekeyes/go-gitdiff/gitdiff"
11)
12
13type FormatPatch struct {
14 Files []*gitdiff.File
15 *gitdiff.PatchHeader
16 Raw string
17}
18
19func (f FormatPatch) ChangeId() (string, error) {
20 if vals, ok := f.RawHeaders["Change-Id"]; ok && len(vals) == 1 {
21 return vals[0], nil
22 }
23 return "", fmt.Errorf("no change-id found")
24}
25
26func ExtractPatches(formatPatch string) ([]FormatPatch, error) {
27 patches := splitFormatPatch(formatPatch)
28
29 result := []FormatPatch{}
30
31 for _, patch := range patches {
32 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch))
33 if err != nil {
34 return nil, fmt.Errorf("failed to parse patch: %w", err)
35 }
36
37 header, err := gitdiff.ParsePatchHeader(headerStr)
38 if err != nil {
39 return nil, fmt.Errorf("failed to parse patch header: %w", err)
40 }
41
42 result = append(result, FormatPatch{
43 Files: files,
44 PatchHeader: header,
45 Raw: patch,
46 })
47 }
48
49 return result, nil
50}
51
52// IsPatchValid checks if the given patch string is valid.
53// It performs very basic sniffing for either git-diff or git-format-patch
54// header lines. For format patches, it attempts to extract and validate each one.
55func IsPatchValid(patch string) bool {
56 if len(patch) == 0 {
57 return false
58 }
59
60 lines := strings.Split(patch, "\n")
61 if len(lines) < 2 {
62 return false
63 }
64
65 firstLine := strings.TrimSpace(lines[0])
66
67 // check if it's a git diff
68 if strings.HasPrefix(firstLine, "diff ") ||
69 strings.HasPrefix(firstLine, "--- ") ||
70 strings.HasPrefix(firstLine, "Index: ") ||
71 strings.HasPrefix(firstLine, "+++ ") ||
72 strings.HasPrefix(firstLine, "@@ ") {
73 return true
74 }
75
76 // check if it's format-patch
77 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
78 strings.HasPrefix(firstLine, "From: ") {
79 // ExtractPatches already runs it through gitdiff.Parse so if that errors,
80 // it's safe to say it's broken.
81 patches, err := ExtractPatches(patch)
82 if err != nil {
83 return false
84 }
85 return len(patches) > 0
86 }
87
88 return false
89}
90
91func IsFormatPatch(patch string) bool {
92 lines := strings.Split(patch, "\n")
93 if len(lines) < 2 {
94 return false
95 }
96
97 firstLine := strings.TrimSpace(lines[0])
98 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
99 return true
100 }
101
102 headerCount := 0
103 for i := range min(10, len(lines)) {
104 line := strings.TrimSpace(lines[i])
105 if strings.HasPrefix(line, "From: ") ||
106 strings.HasPrefix(line, "Date: ") ||
107 strings.HasPrefix(line, "Subject: ") ||
108 strings.HasPrefix(line, "commit ") {
109 headerCount++
110 }
111 }
112
113 return headerCount >= 2
114}
115
116func splitFormatPatch(patchText string) []string {
117 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
118
119 indexes := re.FindAllStringIndex(patchText, -1)
120
121 if len(indexes) == 0 {
122 return []string{}
123 }
124
125 patches := make([]string, len(indexes))
126
127 for i := range indexes {
128 startPos := indexes[i][0]
129 endPos := len(patchText)
130
131 if i < len(indexes)-1 {
132 endPos = indexes[i+1][0]
133 }
134
135 patches[i] = strings.TrimSpace(patchText[startPos:endPos])
136 }
137 return patches
138}
139
140func bestName(file *gitdiff.File) string {
141 if file.IsDelete {
142 return file.OldName
143 } else {
144 return file.NewName
145 }
146}
147
148// in-place reverse of a diff
149func reverseDiff(file *gitdiff.File) {
150 file.OldName, file.NewName = file.NewName, file.OldName
151 file.OldMode, file.NewMode = file.NewMode, file.OldMode
152 file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment
153
154 for _, fragment := range file.TextFragments {
155 // swap postions
156 fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition
157 fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines
158 fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded
159
160 for i := range fragment.Lines {
161 switch fragment.Lines[i].Op {
162 case gitdiff.OpAdd:
163 fragment.Lines[i].Op = gitdiff.OpDelete
164 case gitdiff.OpDelete:
165 fragment.Lines[i].Op = gitdiff.OpAdd
166 default:
167 // do nothing
168 }
169 }
170 }
171}
172
173func Unified(oldText, oldFile, newText, newFile string) (string, error) {
174 oldTemp, err := os.CreateTemp("", "old_*")
175 if err != nil {
176 return "", fmt.Errorf("failed to create temp file for oldText: %w", err)
177 }
178 defer os.Remove(oldTemp.Name())
179 if _, err := oldTemp.WriteString(oldText); err != nil {
180 return "", fmt.Errorf("failed to write to old temp file: %w", err)
181 }
182 oldTemp.Close()
183
184 newTemp, err := os.CreateTemp("", "new_*")
185 if err != nil {
186 return "", fmt.Errorf("failed to create temp file for newText: %w", err)
187 }
188 defer os.Remove(newTemp.Name())
189 if _, err := newTemp.WriteString(newText); err != nil {
190 return "", fmt.Errorf("failed to write to new temp file: %w", err)
191 }
192 newTemp.Close()
193
194 cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())
195 output, err := cmd.CombinedOutput()
196
197 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
198 return string(output), nil
199 }
200 if err != nil {
201 return "", fmt.Errorf("diff command failed: %w", err)
202 }
203
204 return string(output), nil
205}