+121
patchutil/patchutil.go
+121
patchutil/patchutil.go
···
1
+
package patchutil
2
+
3
+
import (
4
+
"fmt"
5
+
"regexp"
6
+
"strings"
7
+
8
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
9
+
)
10
+
11
+
type FormatPatch struct {
12
+
*gitdiff.PatchHeader
13
+
Patch string
14
+
}
15
+
16
+
func ExtractPatches(formatPatch string) ([]FormatPatch, error) {
17
+
patches := splitFormatPatch(formatPatch)
18
+
19
+
result := []FormatPatch{}
20
+
21
+
for _, patch := range patches {
22
+
_, 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, FormatPatch{
33
+
PatchHeader: header,
34
+
Patch: patch,
35
+
})
36
+
}
37
+
38
+
return result, nil
39
+
}
40
+
41
+
// Very basic validation to check if it looks like a diff/patch
42
+
// A valid patch usually starts with diff or --- lines or git format-patch header
43
+
func IsPatchValid(patch string) bool {
44
+
// Basic validation to check if it looks like a diff/patch
45
+
// A valid patch usually starts with diff or --- lines
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
+
// Check for common patch format markers
56
+
firstLine := strings.TrimSpace(lines[0])
57
+
return strings.HasPrefix(firstLine, "diff ") ||
58
+
strings.HasPrefix(firstLine, "--- ") ||
59
+
strings.HasPrefix(firstLine, "Index: ") ||
60
+
strings.HasPrefix(firstLine, "+++ ") ||
61
+
strings.HasPrefix(firstLine, "@@ ") ||
62
+
strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
63
+
strings.HasPrefix(firstLine, "From: ")
64
+
}
65
+
66
+
func IsFormatPatch(patch string) bool {
67
+
lines := strings.Split(patch, "\n")
68
+
if len(lines) < 2 {
69
+
return false
70
+
}
71
+
72
+
firstLine := strings.TrimSpace(lines[0])
73
+
if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
74
+
return true
75
+
}
76
+
77
+
headerCount := 0
78
+
for i := range min(10, len(lines)) {
79
+
line := strings.TrimSpace(lines[i])
80
+
if strings.HasPrefix(line, "From: ") ||
81
+
strings.HasPrefix(line, "Date: ") ||
82
+
strings.HasPrefix(line, "Subject: ") ||
83
+
strings.HasPrefix(line, "commit ") {
84
+
headerCount++
85
+
}
86
+
if strings.HasPrefix(line, "diff --git ") {
87
+
return true
88
+
}
89
+
}
90
+
91
+
return headerCount >= 2
92
+
}
93
+
94
+
func splitFormatPatch(patchText string) []string {
95
+
// The pattern to match is "From " followed by a commit hash and the rest of that line
96
+
re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
97
+
98
+
// Find all starting positions of patches
99
+
indexes := re.FindAllStringIndex(patchText, -1)
100
+
101
+
if len(indexes) == 0 {
102
+
// No patches found
103
+
return []string{}
104
+
}
105
+
106
+
patches := make([]string, len(indexes))
107
+
108
+
for i := range indexes {
109
+
startPos := indexes[i][0]
110
+
endPos := len(patchText)
111
+
112
+
// If there's a next patch, set end position to the start of the next patch
113
+
if i < len(indexes)-1 {
114
+
endPos = indexes[i+1][0]
115
+
}
116
+
117
+
// Extract the patch and trim any whitespace
118
+
patches[i] = strings.TrimSpace(patchText[startPos:endPos])
119
+
}
120
+
return patches
121
+
}
+210
patchutil/patchutil_test.go
+210
patchutil/patchutil_test.go
···
1
+
package patchutil
2
+
3
+
import (
4
+
"reflect"
5
+
"testing"
6
+
)
7
+
8
+
func TestSplitPatches(t *testing.T) {
9
+
tests := []struct {
10
+
name string
11
+
input string
12
+
expected []string
13
+
}{
14
+
{
15
+
name: "Empty input",
16
+
input: "",
17
+
expected: []string{},
18
+
},
19
+
{
20
+
name: "No valid patches",
21
+
input: "This is not a patch\nJust some random text",
22
+
expected: []string{},
23
+
},
24
+
{
25
+
name: "Single patch",
26
+
input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
27
+
From: Author <author@example.com>
28
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
29
+
Subject: [PATCH] Example patch
30
+
31
+
diff --git a/file.txt b/file.txt
32
+
index 123456..789012 100644
33
+
--- a/file.txt
34
+
+++ b/file.txt
35
+
@@ -1 +1 @@
36
+
-old content
37
+
+new content
38
+
--
39
+
2.48.1`,
40
+
expected: []string{
41
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
42
+
From: Author <author@example.com>
43
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
44
+
Subject: [PATCH] Example patch
45
+
46
+
diff --git a/file.txt b/file.txt
47
+
index 123456..789012 100644
48
+
--- a/file.txt
49
+
+++ b/file.txt
50
+
@@ -1 +1 @@
51
+
-old content
52
+
+new content
53
+
--
54
+
2.48.1`,
55
+
},
56
+
},
57
+
{
58
+
name: "Two patches",
59
+
input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
60
+
From: Author <author@example.com>
61
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
62
+
Subject: [PATCH 1/2] First patch
63
+
64
+
diff --git a/file1.txt b/file1.txt
65
+
index 123456..789012 100644
66
+
--- a/file1.txt
67
+
+++ b/file1.txt
68
+
@@ -1 +1 @@
69
+
-old content
70
+
+new content
71
+
--
72
+
2.48.1
73
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
74
+
From: Author <author@example.com>
75
+
Date: Wed, 16 Apr 2025 11:03:11 +0300
76
+
Subject: [PATCH 2/2] Second patch
77
+
78
+
diff --git a/file2.txt b/file2.txt
79
+
index abcdef..ghijkl 100644
80
+
--- a/file2.txt
81
+
+++ b/file2.txt
82
+
@@ -1 +1 @@
83
+
-foo bar
84
+
+baz qux
85
+
--
86
+
2.48.1`,
87
+
expected: []string{
88
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
89
+
From: Author <author@example.com>
90
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
91
+
Subject: [PATCH 1/2] First patch
92
+
93
+
diff --git a/file1.txt b/file1.txt
94
+
index 123456..789012 100644
95
+
--- a/file1.txt
96
+
+++ b/file1.txt
97
+
@@ -1 +1 @@
98
+
-old content
99
+
+new content
100
+
--
101
+
2.48.1`,
102
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
103
+
From: Author <author@example.com>
104
+
Date: Wed, 16 Apr 2025 11:03:11 +0300
105
+
Subject: [PATCH 2/2] Second patch
106
+
107
+
diff --git a/file2.txt b/file2.txt
108
+
index abcdef..ghijkl 100644
109
+
--- a/file2.txt
110
+
+++ b/file2.txt
111
+
@@ -1 +1 @@
112
+
-foo bar
113
+
+baz qux
114
+
--
115
+
2.48.1`,
116
+
},
117
+
},
118
+
{
119
+
name: "Patches with additional text between them",
120
+
input: `Some text before the patches
121
+
122
+
From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
123
+
From: Author <author@example.com>
124
+
Subject: [PATCH] First patch
125
+
126
+
diff content here
127
+
--
128
+
2.48.1
129
+
130
+
Some text between patches
131
+
132
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
133
+
From: Author <author@example.com>
134
+
Subject: [PATCH] Second patch
135
+
136
+
more diff content
137
+
--
138
+
2.48.1
139
+
140
+
Text after patches`,
141
+
expected: []string{
142
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
143
+
From: Author <author@example.com>
144
+
Subject: [PATCH] First patch
145
+
146
+
diff content here
147
+
--
148
+
2.48.1
149
+
150
+
Some text between patches`,
151
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
152
+
From: Author <author@example.com>
153
+
Subject: [PATCH] Second patch
154
+
155
+
more diff content
156
+
--
157
+
2.48.1
158
+
159
+
Text after patches`,
160
+
},
161
+
},
162
+
{
163
+
name: "Patches with whitespace padding",
164
+
input: `
165
+
166
+
From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
167
+
From: Author <author@example.com>
168
+
Subject: Patch
169
+
170
+
content
171
+
--
172
+
2.48.1
173
+
174
+
175
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
176
+
From: Author <author@example.com>
177
+
Subject: Another patch
178
+
179
+
content
180
+
--
181
+
2.48.1
182
+
`,
183
+
expected: []string{
184
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
185
+
From: Author <author@example.com>
186
+
Subject: Patch
187
+
188
+
content
189
+
--
190
+
2.48.1`,
191
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
192
+
From: Author <author@example.com>
193
+
Subject: Another patch
194
+
195
+
content
196
+
--
197
+
2.48.1`,
198
+
},
199
+
},
200
+
}
201
+
202
+
for _, tt := range tests {
203
+
t.Run(tt.name, func(t *testing.T) {
204
+
result := splitFormatPatch(tt.input)
205
+
if !reflect.DeepEqual(result, tt.expected) {
206
+
t.Errorf("splitPatches() = %v, want %v", result, tt.expected)
207
+
}
208
+
})
209
+
}
210
+
}