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