repos / git-pr

a self-hosted git collaboration server
git clone https://github.com/picosh/git-pr.git

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}