File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/run/run.go
238 lines · 6.5kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package run stores builds as notes, so build history clones and survives a move. Chapter 16.
133728e barerepo 1mo
2
package run
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"encoding/json"
133728e barerepo 1mo
7
"fmt"
133728e barerepo 1mo
8
"sort"
133728e barerepo 1mo
9
"strings"
133728e barerepo 1mo
10
"time"
133728e barerepo 1mo
11
133728e barerepo 1mo
12
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
13
)
133728e barerepo 1mo
14
133728e barerepo 1mo
15
// Ref holds every run in one ref, so `git log --show-notes=runs` prints them beside commits.
133728e barerepo 1mo
16
const Ref = "refs/notes/runs"
133728e barerepo 1mo
17
133728e barerepo 1mo
18
// inlineLimit is 64kb, so one talkative build does not make every reader of the ref pay.
133728e barerepo 1mo
19
const inlineLimit = 64 << 10
133728e barerepo 1mo
20
133728e barerepo 1mo
21
// Record is one run of one commit.
133728e barerepo 1mo
22
type Record struct {
133728e barerepo 1mo
23
Runner string `json:"runner"`
133728e barerepo 1mo
24
Labels []string `json:"labels,omitempty"`
133728e barerepo 1mo
25
// Name is the workflow job that produced this, so a matrix of builds is readable. Chapter 15A.
133728e barerepo 1mo
26
Name string `json:"name,omitempty"`
133728e barerepo 1mo
27
// Ref is what was built, because a commit reaches a branch and a proposal both.
133728e barerepo 1mo
28
Ref string `json:"ref,omitempty"`
133728e barerepo 1mo
29
Started int64 `json:"started"`
133728e barerepo 1mo
30
Duration int `json:"duration"`
133728e barerepo 1mo
31
Exit int `json:"exit"`
133728e barerepo 1mo
32
// Output holds the log when it is small enough to sit in the note.
133728e barerepo 1mo
33
Output string `json:"output,omitempty"`
133728e barerepo 1mo
34
// Log names a blob holding the log when Output does not.
133728e barerepo 1mo
35
Log string `json:"log,omitempty"`
133728e barerepo 1mo
36
}
133728e barerepo 1mo
37
133728e barerepo 1mo
38
// Failed reports a run worth looking at, because chapter 19.1 says green is not news.
133728e barerepo 1mo
39
func (r Record) Failed() bool { return r.Exit != 0 }
133728e barerepo 1mo
40
133728e barerepo 1mo
41
func (r Record) StartedAt() time.Time { return time.Unix(r.Started, 0) }
133728e barerepo 1mo
42
133728e barerepo 1mo
43
// recordSep separates runs of the same commit, as in chapter 13.
133728e barerepo 1mo
44
const recordSep = "--"
133728e barerepo 1mo
45
133728e barerepo 1mo
46
const maxRetries = 20
133728e barerepo 1mo
47
133728e barerepo 1mo
48
// Append records a run against a commit.
133728e barerepo 1mo
49
func Append(ctx context.Context, dir, sha string, rec Record, log string) error {
133728e barerepo 1mo
50
if len(log) > inlineLimit {
133728e barerepo 1mo
51
blob, err := gitx.RunStdin(ctx, dir, log, "hash-object", "-w", "--stdin")
133728e barerepo 1mo
52
if err != nil {
133728e barerepo 1mo
53
return err
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
rec.Log = strings.TrimSpace(blob)
133728e barerepo 1mo
56
rec.Output = ""
133728e barerepo 1mo
57
} else {
133728e barerepo 1mo
58
rec.Output = log
133728e barerepo 1mo
59
rec.Log = ""
133728e barerepo 1mo
60
}
133728e barerepo 1mo
61
body, err := json.Marshal(rec)
133728e barerepo 1mo
62
if err != nil {
133728e barerepo 1mo
63
return err
133728e barerepo 1mo
64
}
133728e barerepo 1mo
65
133728e barerepo 1mo
66
for attempt := 0; attempt < maxRetries; attempt++ {
133728e barerepo 1mo
67
old, notes, blobOf, err := read(ctx, dir)
133728e barerepo 1mo
68
if err != nil {
133728e barerepo 1mo
69
return err
133728e barerepo 1mo
70
}
133728e barerepo 1mo
71
existing := notes[sha]
133728e barerepo 1mo
72
if strings.TrimSpace(existing) != "" {
133728e barerepo 1mo
73
existing = strings.TrimRight(existing, "\n") + "\n" + recordSep + "\n"
133728e barerepo 1mo
74
} else {
133728e barerepo 1mo
75
existing = ""
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
notes[sha] = existing + string(body) + "\n"
133728e barerepo 1mo
78
// This one changed, so it is the only note that has to be written.
133728e barerepo 1mo
79
delete(blobOf, sha)
133728e barerepo 1mo
80
133728e barerepo 1mo
81
tree, err := writeTree(ctx, dir, notes, blobOf)
133728e barerepo 1mo
82
if err != nil {
133728e barerepo 1mo
83
return err
133728e barerepo 1mo
84
}
133728e barerepo 1mo
85
args := []string{"commit-tree", tree, "-m", "run on " + short(sha)}
133728e barerepo 1mo
86
if old != "" {
133728e barerepo 1mo
87
args = append(args, "-p", old)
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
commit, err := gitx.RunStdin(ctx, dir, "", args...)
133728e barerepo 1mo
90
if err != nil {
133728e barerepo 1mo
91
return err
133728e barerepo 1mo
92
}
133728e barerepo 1mo
93
if _, err := gitx.Run(ctx, dir, "update-ref", Ref, strings.TrimSpace(commit), old); err == nil {
133728e barerepo 1mo
94
return nil
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
return fmt.Errorf("could not record the run after %d tries", maxRetries)
133728e barerepo 1mo
98
}
133728e barerepo 1mo
99
133728e barerepo 1mo
100
// For returns every run of one commit, newest first.
133728e barerepo 1mo
101
func For(ctx context.Context, dir, sha string) ([]Record, error) {
133728e barerepo 1mo
102
out, err := gitx.Run(ctx, dir, "notes", "--ref="+Ref, "show", sha)
133728e barerepo 1mo
103
if err != nil {
133728e barerepo 1mo
104
return nil, nil
133728e barerepo 1mo
105
}
133728e barerepo 1mo
106
return parse(out), nil
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
133728e barerepo 1mo
109
// Recent returns the newest runs across the repository.
133728e barerepo 1mo
110
func Recent(ctx context.Context, dir string, limit int) (map[string][]Record, error) {
133728e barerepo 1mo
111
_, notes, _, err := read(ctx, dir)
133728e barerepo 1mo
112
if err != nil {
133728e barerepo 1mo
113
return nil, err
133728e barerepo 1mo
114
}
133728e barerepo 1mo
115
out := map[string][]Record{}
133728e barerepo 1mo
116
for sha, body := range notes {
133728e barerepo 1mo
117
if recs := parse(body); len(recs) > 0 {
133728e barerepo 1mo
118
out[sha] = recs
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
}
133728e barerepo 1mo
121
if limit <= 0 {
133728e barerepo 1mo
122
return out, nil
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
return newest(out, limit), nil
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
133728e barerepo 1mo
127
// newest keeps the most recent runs across every commit, because a page shows rows and not commits.
133728e barerepo 1mo
128
func newest(byCommit map[string][]Record, limit int) map[string][]Record {
133728e barerepo 1mo
129
type row struct {
133728e barerepo 1mo
130
sha string
133728e barerepo 1mo
131
rec Record
133728e barerepo 1mo
132
}
133728e barerepo 1mo
133
all := make([]row, 0, len(byCommit))
133728e barerepo 1mo
134
for sha, recs := range byCommit {
133728e barerepo 1mo
135
for _, rec := range recs {
133728e barerepo 1mo
136
all = append(all, row{sha, rec})
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
}
133728e barerepo 1mo
139
if len(all) <= limit {
133728e barerepo 1mo
140
return byCommit
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
sort.SliceStable(all, func(i, j int) bool { return all[i].rec.Started > all[j].rec.Started })
133728e barerepo 1mo
143
out := map[string][]Record{}
133728e barerepo 1mo
144
for _, r := range all[:limit] {
133728e barerepo 1mo
145
out[r.sha] = append(out[r.sha], r.rec)
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
return out
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
133728e barerepo 1mo
150
func parse(note string) []Record {
133728e barerepo 1mo
151
var out []Record
133728e barerepo 1mo
152
for _, part := range strings.Split(note, "\n"+recordSep+"\n") {
133728e barerepo 1mo
153
part = strings.TrimSpace(part)
133728e barerepo 1mo
154
if part == "" {
133728e barerepo 1mo
155
continue
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
var r Record
133728e barerepo 1mo
158
if err := json.Unmarshal([]byte(part), &r); err == nil {
133728e barerepo 1mo
159
out = append(out, r)
133728e barerepo 1mo
160
}
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
sort.SliceStable(out, func(i, j int) bool { return out[i].Started > out[j].Started })
133728e barerepo 1mo
163
return out
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
133728e barerepo 1mo
166
// Log returns a run's output, reading the blob when it is not inline.
133728e barerepo 1mo
167
func Log(ctx context.Context, dir string, r Record) (string, error) {
133728e barerepo 1mo
168
if r.Log == "" {
133728e barerepo 1mo
169
return r.Output, nil
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
return gitx.Run(ctx, dir, "cat-file", "blob", r.Log)
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
133728e barerepo 1mo
174
// read gets every run in two processes, where a cat-file per note cost ten times that.
133728e barerepo 1mo
175
func read(ctx context.Context, dir string) (commit string, notes, blobOf map[string]string, err error) {
133728e barerepo 1mo
176
notes, blobOf = map[string]string{}, map[string]string{}
133728e barerepo 1mo
177
head, err := gitx.Batch(ctx, dir, []string{Ref, Ref + "^{tree}"})
133728e barerepo 1mo
178
if err != nil {
133728e barerepo 1mo
179
return "", nil, nil, err
133728e barerepo 1mo
180
}
133728e barerepo 1mo
181
if head[Ref] == nil || head[Ref+"^{tree}"] == nil {
133728e barerepo 1mo
182
return "", notes, blobOf, nil
133728e barerepo 1mo
183
}
133728e barerepo 1mo
184
commit = head[Ref].SHA
133728e barerepo 1mo
185
entries := gitx.TreeEntries(head[Ref+"^{tree}"].Body)
133728e barerepo 1mo
186
specs := make([]string, 0, len(entries))
133728e barerepo 1mo
187
for _, sha := range entries {
133728e barerepo 1mo
188
specs = append(specs, sha)
133728e barerepo 1mo
189
}
133728e barerepo 1mo
190
blobs, err := gitx.Batch(ctx, dir, specs)
133728e barerepo 1mo
191
if err != nil {
133728e barerepo 1mo
192
return "", nil, nil, err
133728e barerepo 1mo
193
}
133728e barerepo 1mo
194
for path, sha := range entries {
133728e barerepo 1mo
195
if obj := blobs[sha]; obj != nil {
133728e barerepo 1mo
196
notes[path] = obj.Body
133728e barerepo 1mo
197
blobOf[path] = sha
133728e barerepo 1mo
198
}
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
return commit, notes, blobOf, nil
133728e barerepo 1mo
201
}
133728e barerepo 1mo
202
133728e barerepo 1mo
203
func writeTree(ctx context.Context, dir string, notes, blobOf map[string]string) (string, error) {
133728e barerepo 1mo
204
paths := make([]string, 0, len(notes))
133728e barerepo 1mo
205
for p := range notes {
133728e barerepo 1mo
206
paths = append(paths, p)
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
sort.Strings(paths)
133728e barerepo 1mo
209
133728e barerepo 1mo
210
var entries strings.Builder
133728e barerepo 1mo
211
for _, path := range paths {
133728e barerepo 1mo
212
if strings.TrimSpace(notes[path]) == "" {
133728e barerepo 1mo
213
continue
133728e barerepo 1mo
214
}
133728e barerepo 1mo
215
// A note this call did not touch already has a blob, and its content is its name.
133728e barerepo 1mo
216
blob, ok := blobOf[path]
133728e barerepo 1mo
217
if !ok {
133728e barerepo 1mo
218
out, err := gitx.RunStdin(ctx, dir, notes[path], "hash-object", "-w", "--stdin")
133728e barerepo 1mo
219
if err != nil {
133728e barerepo 1mo
220
return "", err
133728e barerepo 1mo
221
}
221 of 238 lines · the whole file
history · rawbarerepo 0.1.0