1package git
2
3import (
4 "bufio"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "strings"
10 "time"
11
12 "tangled.sh/tangled.sh/core/api/tangled"
13
14 "github.com/go-git/go-git/v5/plumbing"
15)
16
17type PostReceiveLine struct {
18 OldSha plumbing.Hash // old sha of reference being updated
19 NewSha plumbing.Hash // new sha of reference being updated
20 Ref string // the reference being updated
21}
22
23func ParsePostReceive(buf io.Reader) ([]PostReceiveLine, error) {
24 scanner := bufio.NewScanner(buf)
25 var lines []PostReceiveLine
26 for scanner.Scan() {
27 line := scanner.Text()
28 parts := strings.SplitN(line, " ", 3)
29 if len(parts) != 3 {
30 continue
31 }
32
33 oldSha := parts[0]
34 newSha := parts[1]
35 ref := parts[2]
36
37 lines = append(lines, PostReceiveLine{
38 OldSha: plumbing.NewHash(oldSha),
39 NewSha: plumbing.NewHash(newSha),
40 Ref: ref,
41 })
42 }
43
44 if err := scanner.Err(); err != nil {
45 return nil, err
46 }
47
48 return lines, nil
49}
50
51type RefUpdateMeta struct {
52 CommitCount CommitCount
53 IsDefaultRef bool
54 LangBreakdown LangBreakdown
55}
56
57type CommitCount struct {
58 ByEmail map[string]int
59}
60
61func (g *GitRepo) RefUpdateMeta(line PostReceiveLine) (RefUpdateMeta, error) {
62 var errs error
63
64 commitCount, err := g.newCommitCount(line)
65 errors.Join(errs, err)
66
67 isDefaultRef, err := g.isDefaultBranch(line)
68 errors.Join(errs, err)
69
70 ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
71 defer cancel()
72 breakdown, err := g.AnalyzeLanguages(ctx)
73 errors.Join(errs, err)
74
75 return RefUpdateMeta{
76 CommitCount: commitCount,
77 IsDefaultRef: isDefaultRef,
78 LangBreakdown: breakdown,
79 }, errs
80}
81
82func (g *GitRepo) newCommitCount(line PostReceiveLine) (CommitCount, error) {
83 byEmail := make(map[string]int)
84 commitCount := CommitCount{
85 ByEmail: byEmail,
86 }
87
88 if line.NewSha.IsZero() {
89 return commitCount, nil
90 }
91
92 args := []string{fmt.Sprintf("--max-count=%d", 100)}
93
94 if line.OldSha.IsZero() {
95 // git rev-list <newsha> ^other-branches --not ^this-branch
96 args = append(args, line.NewSha.String())
97
98 branches, _ := g.Branches()
99 for _, b := range branches {
100 if !strings.Contains(line.Ref, b.Name) {
101 args = append(args, fmt.Sprintf("^%s", b.Name))
102 }
103 }
104
105 args = append(args, "--not")
106 args = append(args, fmt.Sprintf("^%s", line.Ref))
107 } else {
108 // git rev-list <oldsha>..<newsha>
109 args = append(args, fmt.Sprintf("%s..%s", line.OldSha.String(), line.NewSha.String()))
110 }
111
112 output, err := g.revList(args...)
113 if err != nil {
114 return commitCount, fmt.Errorf("failed to run rev-list: %w", err)
115 }
116
117 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
118 if len(lines) == 1 && lines[0] == "" {
119 return commitCount, nil
120 }
121
122 for _, item := range lines {
123 obj, err := g.r.CommitObject(plumbing.NewHash(item))
124 if err != nil {
125 continue
126 }
127 commitCount.ByEmail[obj.Author.Email] += 1
128 }
129
130 return commitCount, nil
131}
132
133func (g *GitRepo) isDefaultBranch(line PostReceiveLine) (bool, error) {
134 defaultBranch, err := g.FindMainBranch()
135 if err != nil {
136 return false, err
137 }
138
139 refName := plumbing.ReferenceName(line.Ref)
140 if refName.IsBranch() {
141 return defaultBranch == refName.Short(), nil
142 }
143
144 return false, err
145}
146
147func (m RefUpdateMeta) AsRecord() tangled.GitRefUpdate_Meta {
148 var byEmail []*tangled.GitRefUpdate_IndividualEmailCommitCount
149 for e, v := range m.CommitCount.ByEmail {
150 byEmail = append(byEmail, &tangled.GitRefUpdate_IndividualEmailCommitCount{
151 Email: e,
152 Count: int64(v),
153 })
154 }
155
156 var langs []*tangled.GitRefUpdate_IndividualLanguageSize
157 for lang, size := range m.LangBreakdown {
158 langs = append(langs, &tangled.GitRefUpdate_IndividualLanguageSize{
159 Lang: lang,
160 Size: size,
161 })
162 }
163
164 return tangled.GitRefUpdate_Meta{
165 CommitCount: &tangled.GitRefUpdate_CommitCountBreakdown{
166 ByEmail: byEmail,
167 },
168 IsDefaultRef: m.IsDefaultRef,
169 LangBreakdown: &tangled.GitRefUpdate_LangBreakdown{
170 Inputs: langs,
171 },
172 }
173}