jolheiser
·
2025-08-21
util.go
1package git
2
3import (
4 "crypto/sha256"
5 "database/sql"
6 "encoding/hex"
7 "fmt"
8 "io"
9 "math/rand"
10 "regexp"
11 "strconv"
12 "strings"
13
14 "github.com/bluekeyes/go-gitdiff/gitdiff"
15 "github.com/charmbracelet/ssh"
16)
17
18var (
19 baseCommitRe = regexp.MustCompile(`base-commit: (.+)\s*`)
20 letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
21 startOfPatch = "From "
22 patchsetPrefix = "ps-"
23 prPrefix = "pr-"
24)
25
26// https://stackoverflow.com/a/22892986
27func randSeq(n int) string {
28 b := make([]rune, n)
29 for i := range b {
30 b[i] = letters[rand.Intn(len(letters))]
31 }
32 return strings.ToLower(string(b))
33}
34
35func truncateSha(sha string) string {
36 if len(sha) < 7 {
37 return sha
38 }
39 return sha[:7]
40}
41
42func GetAuthorizedKeys(pubkeys []string) ([]ssh.PublicKey, error) {
43 keys := []ssh.PublicKey{}
44 for _, pubkey := range pubkeys {
45 if strings.TrimSpace(pubkey) == "" {
46 continue
47 }
48 if strings.HasPrefix(pubkey, "#") {
49 continue
50 }
51 upk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
52 if err != nil {
53 return keys, err
54 }
55 keys = append(keys, upk)
56 }
57
58 return keys, nil
59}
60
61func getFormattedPatchsetID(id int64) string {
62 if id == 0 {
63 return ""
64 }
65 return fmt.Sprintf("%s%d", patchsetPrefix, id)
66}
67
68func getPrID(prID string) (int64, error) {
69 recID, err := strconv.Atoi(strings.Replace(prID, prPrefix, "", 1))
70 if err != nil {
71 return 0, err
72 }
73 return int64(recID), nil
74}
75
76func getPatchsetID(patchsetID string) (int64, error) {
77 psID, err := strconv.Atoi(strings.Replace(patchsetID, patchsetPrefix, "", 1))
78 if err != nil {
79 return 0, err
80 }
81 return int64(psID), nil
82}
83
84func splitPatchSet(patchset string) []string {
85 return strings.Split(patchset, "\n"+startOfPatch)
86}
87
88func findBaseCommit(patch string) string {
89 strs := baseCommitRe.FindStringSubmatch(patch)
90 baseCommit := ""
91 if len(strs) > 1 {
92 baseCommit = strs[1]
93 }
94 return baseCommit
95}
96
97func patchToDiff(patch io.Reader) (string, error) {
98 by, err := io.ReadAll(patch)
99 if err != nil {
100 return "", err
101 }
102 str := string(by)
103 idx := strings.Index(str, "diff --git")
104 if idx == -1 {
105 return "", fmt.Errorf("no diff found in patch")
106 }
107 trailIdx := strings.LastIndex(str, "-- \n")
108 if trailIdx >= 0 {
109 return str[idx:trailIdx], nil
110 }
111 return str[idx:], nil
112}
113
114func ParsePatch(patchRaw string) ([]*gitdiff.File, string, error) {
115 reader := strings.NewReader(patchRaw)
116 diffFiles, preamble, err := gitdiff.Parse(reader)
117 return diffFiles, preamble, err
118}
119
120func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
121 patches := []*Patch{}
122 buf := new(strings.Builder)
123 _, err := io.Copy(buf, patchset)
124 if err != nil {
125 return nil, err
126 }
127
128 patchesRaw := splitPatchSet(buf.String())
129 for idx, patchRaw := range patchesRaw {
130 patchStr := patchRaw
131 if idx > 0 {
132 patchStr = startOfPatch + patchRaw
133 }
134 diffFiles, preamble, err := ParsePatch(patchStr)
135 if err != nil {
136 return nil, err
137 }
138 header, err := gitdiff.ParsePatchHeader(preamble)
139 if err != nil {
140 return nil, err
141 }
142
143 baseCommit := findBaseCommit(patchRaw)
144 authorName := "Unknown"
145 authorEmail := ""
146 if header.Author != nil {
147 authorName = header.Author.Name
148 authorEmail = header.Author.Email
149 }
150
151 contentSha := calcContentSha(diffFiles, header)
152
153 patches = append(patches, &Patch{
154 AuthorName: authorName,
155 AuthorEmail: authorEmail,
156 AuthorDate: header.AuthorDate.UTC(),
157 Title: header.Title,
158 Body: header.Body,
159 BodyAppendix: header.BodyAppendix,
160 CommitSha: header.SHA,
161 ContentSha: contentSha,
162 RawText: patchStr,
163 BaseCommitSha: sql.NullString{String: baseCommit},
164 Files: diffFiles,
165 })
166 }
167
168 return patches, nil
169}
170
171// calcContentSha calculates a shasum containing the important content
172// changes related to a patch.
173// We cannot rely on patch.CommitSha because it includes the commit date
174// that will change when a user fetches and applies the patch locally.
175func calcContentSha(diffFiles []*gitdiff.File, header *gitdiff.PatchHeader) string {
176 authorName := ""
177 authorEmail := ""
178 if header.Author != nil {
179 authorName = header.Author.Name
180 authorEmail = header.Author.Email
181 }
182 content := fmt.Sprintf(
183 "%s\n%s\n%s\n%s\n",
184 header.Title,
185 header.Body,
186 authorName,
187 authorEmail,
188 )
189 for _, diff := range diffFiles {
190 // we need to ignore diffs with base commit because that depends
191 // on the client that is exporting the patch
192 foundBase := false
193 for _, text := range diff.TextFragments {
194 for _, line := range text.Lines {
195 base := findBaseCommit(line.Line)
196 if base != "" {
197 foundBase = true
198 }
199 }
200 }
201
202 if foundBase {
203 continue
204 }
205
206 dff := fmt.Sprintf(
207 "%s->%s %s..%s %s->%s\n",
208 diff.OldName, diff.NewName,
209 diff.OldOIDPrefix, diff.NewOIDPrefix,
210 diff.OldMode.String(), diff.NewMode.String(),
211 )
212 content += dff
213 }
214 sha := sha256.Sum256([]byte(content))
215 shaStr := hex.EncodeToString(sha[:])
216 return shaStr
217}