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. For format patches, it attempts to extract and validate each one.
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
56 // check if it's a git diff
57 if strings.HasPrefix(firstLine, "diff ") ||
58 strings.HasPrefix(firstLine, "--- ") ||
59 strings.HasPrefix(firstLine, "Index: ") ||
60 strings.HasPrefix(firstLine, "+++ ") ||
61 strings.HasPrefix(firstLine, "@@ ") {
62 return true
63 }
64
65 // check if it's format-patch
66 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") ||
67 strings.HasPrefix(firstLine, "From: ") {
68 // ExtractPatches already runs it through gitdiff.Parse so if that errors,
69 // it's safe to say it's broken.
70 patches, err := ExtractPatches(patch)
71 if err != nil {
72 return false
73 }
74 return len(patches) > 0
75 }
76
77 return false
78}
79
80func IsFormatPatch(patch string) bool {
81 lines := strings.Split(patch, "\n")
82 if len(lines) < 2 {
83 return false
84 }
85
86 firstLine := strings.TrimSpace(lines[0])
87 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") {
88 return true
89 }
90
91 headerCount := 0
92 for i := range min(10, len(lines)) {
93 line := strings.TrimSpace(lines[i])
94 if strings.HasPrefix(line, "From: ") ||
95 strings.HasPrefix(line, "Date: ") ||
96 strings.HasPrefix(line, "Subject: ") ||
97 strings.HasPrefix(line, "commit ") {
98 headerCount++
99 }
100 }
101
102 return headerCount >= 2
103}
104
105func splitFormatPatch(patchText string) []string {
106 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
107
108 indexes := re.FindAllStringIndex(patchText, -1)
109
110 if len(indexes) == 0 {
111 return []string{}
112 }
113
114 patches := make([]string, len(indexes))
115
116 for i := range indexes {
117 startPos := indexes[i][0]
118 endPos := len(patchText)
119
120 if i < len(indexes)-1 {
121 endPos = indexes[i+1][0]
122 }
123
124 patches[i] = strings.TrimSpace(patchText[startPos:endPos])
125 }
126 return patches
127}