push.go
⎇
Raw
1package ci
2
3import (
4 "context"
5 "log"
6 "strconv"
7 "strings"
8)
9
10// PreambleMax is how much of a pushed stream a transport keeps so
11// TriggerForPush can read the ref updates. The pkt-line ref list always fits.
12const PreambleMax = 4096
13
14// CapWriter keeps only the first Limit bytes written to it. The git transports
15// tee the push stream through it.
16type CapWriter struct {
17 Limit int
18 buf []byte
19}
20
21func (w *CapWriter) Write(p []byte) (int, error) {
22 if n := w.Limit - len(w.buf); n > 0 {
23 if n > len(p) {
24 n = len(p)
25 }
26 w.buf = append(w.buf, p[:n]...)
27 }
28 return len(p), nil
29}
30
31// Bytes returns the captured prefix. A nil CapWriter returns nothing, so a
32// transport can pass one it never allocated.
33func (w *CapWriter) Bytes() []byte {
34 if w == nil {
35 return nil
36 }
37 return w.buf
38}
39
40// refUpdate is one "old new refname" line of a receive-pack request.
41type refUpdate struct {
42 OldSha string
43 NewSha string
44 Refname string
45}
46
47// parsePktLineRefUpdates reads the ref updates from the start of a
48// receive-pack stream. It stops at the flush packet or at truncated input,
49// so it works on the first few kilobytes alone.
50func parsePktLineRefUpdates(text string) []refUpdate {
51 var refs []refUpdate
52 for pos := 0; pos+4 <= len(text); {
53 length, err := strconv.ParseUint(text[pos:pos+4], 16, 32)
54 if err != nil || length == 0 {
55 break
56 }
57 if length < 4 || pos+int(length) > len(text) {
58 break
59 }
60 line := text[pos+4 : pos+int(length)]
61 // Capabilities follow a NUL byte on the first line.
62 if i := strings.IndexByte(line, 0); i >= 0 {
63 line = line[:i]
64 }
65 pos += int(length)
66
67 parts := strings.Split(strings.TrimSpace(line), " ")
68 if len(parts) >= 3 && parts[2] != "" {
69 refs = append(refs, refUpdate{OldSha: parts[0], NewSha: parts[1], Refname: parts[2]})
70 }
71 }
72 return refs
73}
74
75// TriggerForPush starts a run for every pushed branch or tag whose config at
76// the new commit asks for it. preamble is the captured start of the
77// receive-pack stream. Both git transports call this.
78func (r *Runner) TriggerForPush(ctx context.Context, repoName string, preamble []byte) {
79 for _, ref := range parsePktLineRefUpdates(string(preamble)) {
80 // A delete pushes the all-zero sha; there is nothing to build.
81 if ref.NewSha == "" || strings.Trim(ref.NewSha, "0") == "" {
82 continue
83 }
84 isBranch := strings.HasPrefix(ref.Refname, "refs/heads/")
85 isTag := strings.HasPrefix(ref.Refname, "refs/tags/")
86 if !isBranch && !isTag {
87 continue
88 }
89 cfg, err := r.ConfigAt(ctx, repoName, ref.NewSha)
90 if err != nil || cfg == nil {
91 continue
92 }
93 opts := TriggerOpts{CommitSha: ref.NewSha}
94 switch {
95 case isBranch:
96 branch := strings.TrimPrefix(ref.Refname, "refs/heads/")
97 if !shouldTriggerPush(cfg, branch) {
98 continue
99 }
100 opts.TriggerSource = "push"
101 opts.CommitBranch = branch
102 case cfg.On.Tag:
103 opts.TriggerSource = "tag"
104 opts.CommitTag = strings.TrimPrefix(ref.Refname, "refs/tags/")
105 default:
106 continue
107 }
108 if _, err := r.TriggerRun(ctx, repoName, opts); err != nil {
109 log.Printf("CI %s trigger failed for %s: %v", opts.TriggerSource, repoName, err)
110 }
111 }
112}
113