1package patchutil
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7
8 "github.com/bluekeyes/go-gitdiff/gitdiff"
9)
10
11type FormatPatch struct {
12 Files []*gitdiff.File
13 *gitdiff.PatchHeader
14}
15
16func ExtractPatches(formatPatch string) ([]FormatPatch, error) {
17 patches := splitFormatPatch(formatPatch)
18
19 result := []FormatPatch{}
20
21 for _, patch := range patches {
22 files, 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 Files: files,
34 PatchHeader: header,
35 })
36 }
37
38 return result, nil
39}
40
41// IsPatchValid checks if the given patch string is valid.
42// It performs very basic sniffing for either git-diff or git-format-patch
43// header lines.
44func IsPatchValid(patch string) bool {
45 if len(patch) == 0 {
46 return false
47 }
48
49 lines := strings.Split(patch, "\n")
50 if len(lines) < 2 {
51 return false
52 }
53
54 firstLine := strings.TrimSpace(lines[0])
55 return strings.HasPrefix(firstLine, "diff ") ||
56 strings.HasPrefix(firstLine, "--- ") ||
57 strings.HasPrefix(firstLine, "Index: ") ||
58 strings.HasPrefix(firstLine, "+++ ") ||
59 strings.HasPrefix(firstLine, "@@ ") ||
60 strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
61 strings.HasPrefix(firstLine, "From: ")
62}
63
64func IsFormatPatch(patch string) bool {
65 lines := strings.Split(patch, "\n")
66 if len(lines) < 2 {
67 return false
68 }
69
70 firstLine := strings.TrimSpace(lines[0])
71 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
72 return true
73 }
74
75 headerCount := 0
76 for i := range min(10, len(lines)) {
77 line := strings.TrimSpace(lines[i])
78 if strings.HasPrefix(line, "From: ") ||
79 strings.HasPrefix(line, "Date: ") ||
80 strings.HasPrefix(line, "Subject: ") ||
81 strings.HasPrefix(line, "commit ") {
82 headerCount++
83 }
84 }
85
86 return headerCount >= 2
87}
88
89func splitFormatPatch(patchText string) []string {
90 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
91
92 indexes := re.FindAllStringIndex(patchText, -1)
93
94 if len(indexes) == 0 {
95 return []string{}
96 }
97
98 patches := make([]string, len(indexes))
99
100 for i := range indexes {
101 startPos := indexes[i][0]
102 endPos := len(patchText)
103
104 if i < len(indexes)-1 {
105 endPos = indexes[i+1][0]
106 }
107
108 patches[i] = strings.TrimSpace(patchText[startPos:endPos])
109 }
110 return patches
111}