diff --git a/patchutil/patchutil.go b/patchutil/patchutil.go new file mode 100644 index 00000000..999f4a7f --- /dev/null +++ b/patchutil/patchutil.go @@ -0,0 +1,121 @@ +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 +} diff --git a/patchutil/patchutil_test.go b/patchutil/patchutil_test.go new file mode 100644 index 00000000..d25f3503 --- /dev/null +++ b/patchutil/patchutil_test.go @@ -0,0 +1,210 @@ +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +Subject: Patch + +content +-- +2.48.1 + + +From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 +From: Author +Subject: Another patch + +content +-- +2.48.1 + `, + expected: []string{ + `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 +From: Author +Subject: Patch + +content +-- +2.48.1`, + `From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 +From: Author +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) + } + }) + } +}