forked from tangled.org/core
this repo has no description

patchutil: move patch-related shared code to top-level package

Changed files
+331
patchutil
+121
patchutil/patchutil.go
···
+
package patchutil
+
+
import (
+
"fmt"
+
"regexp"
+
"strings"
+
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
+
)
+
+
type FormatPatch struct {
+
*gitdiff.PatchHeader
+
Patch string
+
}
+
+
func ExtractPatches(formatPatch string) ([]FormatPatch, error) {
+
patches := splitFormatPatch(formatPatch)
+
+
result := []FormatPatch{}
+
+
for _, patch := range patches {
+
_, headerStr, err := gitdiff.Parse(strings.NewReader(patch))
+
if err != nil {
+
return nil, fmt.Errorf("failed to parse patch: %w", err)
+
}
+
+
header, err := gitdiff.ParsePatchHeader(headerStr)
+
if err != nil {
+
return nil, fmt.Errorf("failed to parse patch header: %w", err)
+
}
+
+
result = append(result, FormatPatch{
+
PatchHeader: header,
+
Patch: patch,
+
})
+
}
+
+
return result, nil
+
}
+
+
// Very basic validation to check if it looks like a diff/patch
+
// A valid patch usually starts with diff or --- lines or git format-patch header
+
func IsPatchValid(patch string) bool {
+
// Basic validation to check if it looks like a diff/patch
+
// A valid patch usually starts with diff or --- lines
+
if len(patch) == 0 {
+
return false
+
}
+
+
lines := strings.Split(patch, "\n")
+
if len(lines) < 2 {
+
return false
+
}
+
+
// Check for common patch format markers
+
firstLine := strings.TrimSpace(lines[0])
+
return strings.HasPrefix(firstLine, "diff ") ||
+
strings.HasPrefix(firstLine, "--- ") ||
+
strings.HasPrefix(firstLine, "Index: ") ||
+
strings.HasPrefix(firstLine, "+++ ") ||
+
strings.HasPrefix(firstLine, "@@ ") ||
+
strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
+
strings.HasPrefix(firstLine, "From: ")
+
}
+
+
func IsFormatPatch(patch string) bool {
+
lines := strings.Split(patch, "\n")
+
if len(lines) < 2 {
+
return false
+
}
+
+
firstLine := strings.TrimSpace(lines[0])
+
if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
+
return true
+
}
+
+
headerCount := 0
+
for i := range min(10, len(lines)) {
+
line := strings.TrimSpace(lines[i])
+
if strings.HasPrefix(line, "From: ") ||
+
strings.HasPrefix(line, "Date: ") ||
+
strings.HasPrefix(line, "Subject: ") ||
+
strings.HasPrefix(line, "commit ") {
+
headerCount++
+
}
+
if strings.HasPrefix(line, "diff --git ") {
+
return true
+
}
+
}
+
+
return headerCount >= 2
+
}
+
+
func splitFormatPatch(patchText string) []string {
+
// The pattern to match is "From " followed by a commit hash and the rest of that line
+
re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
+
+
// Find all starting positions of patches
+
indexes := re.FindAllStringIndex(patchText, -1)
+
+
if len(indexes) == 0 {
+
// No patches found
+
return []string{}
+
}
+
+
patches := make([]string, len(indexes))
+
+
for i := range indexes {
+
startPos := indexes[i][0]
+
endPos := len(patchText)
+
+
// If there's a next patch, set end position to the start of the next patch
+
if i < len(indexes)-1 {
+
endPos = indexes[i+1][0]
+
}
+
+
// Extract the patch and trim any whitespace
+
patches[i] = strings.TrimSpace(patchText[startPos:endPos])
+
}
+
return patches
+
}
+210
patchutil/patchutil_test.go
···
+
package patchutil
+
+
import (
+
"reflect"
+
"testing"
+
)
+
+
func TestSplitPatches(t *testing.T) {
+
tests := []struct {
+
name string
+
input string
+
expected []string
+
}{
+
{
+
name: "Empty input",
+
input: "",
+
expected: []string{},
+
},
+
{
+
name: "No valid patches",
+
input: "This is not a patch\nJust some random text",
+
expected: []string{},
+
},
+
{
+
name: "Single patch",
+
input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
+
Subject: [PATCH] Example patch
+
+
diff --git a/file.txt b/file.txt
+
index 123456..789012 100644
+
--- a/file.txt
+
+++ b/file.txt
+
@@ -1 +1 @@
+
-old content
+
+new content
+
--
+
2.48.1`,
+
expected: []string{
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
+
Subject: [PATCH] Example patch
+
+
diff --git a/file.txt b/file.txt
+
index 123456..789012 100644
+
--- a/file.txt
+
+++ b/file.txt
+
@@ -1 +1 @@
+
-old content
+
+new content
+
--
+
2.48.1`,
+
},
+
},
+
{
+
name: "Two patches",
+
input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
+
Subject: [PATCH 1/2] First patch
+
+
diff --git a/file1.txt b/file1.txt
+
index 123456..789012 100644
+
--- a/file1.txt
+
+++ b/file1.txt
+
@@ -1 +1 @@
+
-old content
+
+new content
+
--
+
2.48.1
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:03:11 +0300
+
Subject: [PATCH 2/2] Second patch
+
+
diff --git a/file2.txt b/file2.txt
+
index abcdef..ghijkl 100644
+
--- a/file2.txt
+
+++ b/file2.txt
+
@@ -1 +1 @@
+
-foo bar
+
+baz qux
+
--
+
2.48.1`,
+
expected: []string{
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:01:00 +0300
+
Subject: [PATCH 1/2] First patch
+
+
diff --git a/file1.txt b/file1.txt
+
index 123456..789012 100644
+
--- a/file1.txt
+
+++ b/file1.txt
+
@@ -1 +1 @@
+
-old content
+
+new content
+
--
+
2.48.1`,
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Date: Wed, 16 Apr 2025 11:03:11 +0300
+
Subject: [PATCH 2/2] Second patch
+
+
diff --git a/file2.txt b/file2.txt
+
index abcdef..ghijkl 100644
+
--- a/file2.txt
+
+++ b/file2.txt
+
@@ -1 +1 @@
+
-foo bar
+
+baz qux
+
--
+
2.48.1`,
+
},
+
},
+
{
+
name: "Patches with additional text between them",
+
input: `Some text before the patches
+
+
From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: [PATCH] First patch
+
+
diff content here
+
--
+
2.48.1
+
+
Some text between patches
+
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: [PATCH] Second patch
+
+
more diff content
+
--
+
2.48.1
+
+
Text after patches`,
+
expected: []string{
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: [PATCH] First patch
+
+
diff content here
+
--
+
2.48.1
+
+
Some text between patches`,
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: [PATCH] Second patch
+
+
more diff content
+
--
+
2.48.1
+
+
Text after patches`,
+
},
+
},
+
{
+
name: "Patches with whitespace padding",
+
input: `
+
+
From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: Patch
+
+
content
+
--
+
2.48.1
+
+
+
From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: Another patch
+
+
content
+
--
+
2.48.1
+
`,
+
expected: []string{
+
`From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: Patch
+
+
content
+
--
+
2.48.1`,
+
`From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
+
From: Author <author@example.com>
+
Subject: Another patch
+
+
content
+
--
+
2.48.1`,
+
},
+
},
+
}
+
+
for _, tt := range tests {
+
t.Run(tt.name, func(t *testing.T) {
+
result := splitFormatPatch(tt.input)
+
if !reflect.DeepEqual(result, tt.expected) {
+
t.Errorf("splitPatches() = %v, want %v", result, tt.expected)
+
}
+
})
+
}
+
}