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