push.go
| 1 | package ci |
| 2 | |
| 3 | import ( |
| 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. |
| 12 | const PreambleMax = 4096 |
| 13 | |
| 14 | // CapWriter keeps only the first Limit bytes written to it. The git transports |
| 15 | // tee the push stream through it. |
| 16 | type CapWriter struct { |
| 17 | Limit int |
| 18 | buf []byte |
| 19 | } |
| 20 | |
| 21 | func (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. |
| 33 | func (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. |
| 41 | type 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. |
| 50 | func 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. |
| 78 | func (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 |