133728e barerepo 1mo
1
package thread
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"hash/fnv"
133728e barerepo 1mo
7
"sort"
133728e barerepo 1mo
8
"strconv"
133728e barerepo 1mo
9
"strings"
133728e barerepo 1mo
10
"sync"
133728e barerepo 1mo
11
"time"
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
16
// maxRetries bounds the swap on the notes ref, where contention is two simultaneous comments.
133728e barerepo 1mo
17
const maxRetries = 20
133728e barerepo 1mo
19
// stripes serialise this process's writers. A map keyed by repository and thread would only grow.
133728e barerepo 1mo
20
var stripes [64]sync.Mutex
133728e barerepo 1mo
22
// lockFor picks the mutex guarding one thread's ref.
133728e barerepo 1mo
23
func lockFor(dir string, n int) *sync.Mutex {
133728e barerepo 1mo
24
h := fnv.New32a()
133728e barerepo 1mo
25
h.Write([]byte(dir + "/" + strconv.Itoa(n)))
133728e barerepo 1mo
26
return &stripes[h.Sum32()%uint32(len(stripes))]
133728e barerepo 1mo
29
// Write swaps the note ref, so a losing writer reads again rather than dropping a comment.
133728e barerepo 1mo
30
func Write(ctx context.Context, dir string, n int, message string, mutate func(t *Tree)) error {
133728e barerepo 1mo
31
// One writer at a time here, so the swap below only ever loses to another process.
133728e barerepo 1mo
32
mu := lockFor(dir, n)
133728e barerepo 1mo
33
mu.Lock()
133728e barerepo 1mo
34
defer mu.Unlock()
133728e barerepo 1mo
35
for attempt := 0; attempt < maxRetries; attempt++ {
133728e barerepo 1mo
36
old, tree, err := read(ctx, dir, n)
133728e barerepo 1mo
37
if err != nil {
133728e barerepo 1mo
38
return err
133728e barerepo 1mo
40
mutate(tree)
133728e barerepo 1mo
42
treeSHA, err := tree.write(ctx, dir)
133728e barerepo 1mo
43
if err != nil {
133728e barerepo 1mo
44
return err
133728e barerepo 1mo
46
args := []string{"commit-tree", treeSHA, "-m", message}
133728e barerepo 1mo
47
if old != "" {
133728e barerepo 1mo
48
args = append(args, "-p", old)
133728e barerepo 1mo
50
commit, err := gitx.RunStdin(ctx, dir, "", args...)
133728e barerepo 1mo
51
if err != nil {
133728e barerepo 1mo
52
return err
133728e barerepo 1mo
54
commit = strings.TrimSpace(commit)
133728e barerepo 1mo
56
if _, err := gitx.Run(ctx, dir, "update-ref", Ref(n), commit, old); err == nil {
133728e barerepo 1mo
57
return nil
133728e barerepo 1mo
59
// Somebody wrote between the read and the write, so read again with their record in.
133728e barerepo 1mo
61
return fmt.Errorf("could not write thread %d after %d tries", n, maxRetries)
133728e barerepo 1mo
64
// Tree is one note tree: a meta blob and a note per annotated object.
133728e barerepo 1mo
65
type Tree struct {
133728e barerepo 1mo
66
Meta Meta
133728e barerepo 1mo
67
Notes map[string]string // object sha -> note body
133728e barerepo 1mo
68
order []string // the order paths were first seen, so trees are stable
133728e barerepo 1mo
69
// blobs keeps each entry's hash, so an untouched note costs no git process to rewrite.
133728e barerepo 1mo
70
blobs map[string]string
133728e barerepo 1mo
71
dirty map[string]bool
133728e barerepo 1mo
72
// metaBlob and metaWas let an unchanged meta blob be reused.
133728e barerepo 1mo
73
metaBlob string
133728e barerepo 1mo
74
metaWas string
133728e barerepo 1mo
77
// Append adds a comment to the note on object.
133728e barerepo 1mo
78
func (t *Tree) Append(object string, c Comment) {
133728e barerepo 1mo
79
body := t.Notes[object]
133728e barerepo 1mo
80
if strings.TrimSpace(body) != "" {
133728e barerepo 1mo
81
body = strings.TrimRight(body, "\n") + "\n" + recordSep + "\n"
133728e barerepo 1mo
82
} else {
133728e barerepo 1mo
83
body = ""
133728e barerepo 1mo
85
t.set(object, body+c.Render())
133728e barerepo 1mo
88
func (t *Tree) set(path, body string) {
133728e barerepo 1mo
89
if t.Notes == nil {
133728e barerepo 1mo
90
t.Notes = map[string]string{}
133728e barerepo 1mo
92
if _, seen := t.Notes[path]; !seen {
133728e barerepo 1mo
93
t.order = append(t.order, path)
133728e barerepo 1mo
95
t.Notes[path] = body
133728e barerepo 1mo
96
if t.dirty == nil {
133728e barerepo 1mo
97
t.dirty = map[string]bool{}
133728e barerepo 1mo
99
t.dirty[path] = true
133728e barerepo 1mo
102
// keep records the hash of an entry this write does not touch.
133728e barerepo 1mo
103
func (t *Tree) keep(path, body, blob string) {
133728e barerepo 1mo
104
if t.Notes == nil {
133728e barerepo 1mo
105
t.Notes = map[string]string{}
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
if t.blobs == nil {
133728e barerepo 1mo
108
t.blobs = map[string]string{}
133728e barerepo 1mo
109
}
133728e barerepo 1mo
110
if _, seen := t.Notes[path]; !seen {
133728e barerepo 1mo
111
t.order = append(t.order, path)
133728e barerepo 1mo
112
}
133728e barerepo 1mo
113
t.Notes[path] = body
133728e barerepo 1mo
114
t.blobs[path] = blob
133728e barerepo 1mo
117
// read loads the current note tree, or an empty one, in two processes whatever the note count.
133728e barerepo 1mo
118
func read(ctx context.Context, dir string, n int) (commit string, t *Tree, err error) {
133728e barerepo 1mo
119
t = &Tree{Meta: Meta{State: Open}}
133728e barerepo 1mo
120
// A file read in a bare repository, falling back to git, which a worktree needs to be right.
133728e barerepo 1mo
121
commit, err = gitx.ResolveRefOrAsk(ctx, dir, Ref(n))
133728e barerepo 1mo
122
if err != nil {
133728e barerepo 1mo
123
return "", t, nil // no thread yet
133728e barerepo 1mo
124
}
133728e barerepo 1mo
126
// The tree of the commit this write will name as its parent, not of the ref, which can move.
133728e barerepo 1mo
127
head, err := gitx.Batch(ctx, dir, []string{commit + "^{tree}"})
133728e barerepo 1mo
128
if err != nil {
133728e barerepo 1mo
129
return "", nil, err
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
root := head[commit+"^{tree}"]
133728e barerepo 1mo
132
if root == nil {
133728e barerepo 1mo
133
return "", nil, fmt.Errorf("thread %d has no tree at %s", n, commit)
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
entries := gitx.TreeEntries(root.Body)
133728e barerepo 1mo
136
// Sorted, because git answers a tree in name order and the entry order decides nothing else.
133728e barerepo 1mo
137
paths := make([]string, 0, len(entries))
133728e barerepo 1mo
138
for path := range entries {
133728e barerepo 1mo
139
paths = append(paths, path)
133728e barerepo 1mo
140
}
133728e barerepo 1mo
141
sort.Strings(paths)
133728e barerepo 1mo
142
specs := make([]string, 0, len(paths))
133728e barerepo 1mo
143
for _, path := range paths {
133728e barerepo 1mo
144
specs = append(specs, entries[path])
133728e barerepo 1mo
145
}
133728e barerepo 1mo
146
blobs, err := gitx.Batch(ctx, dir, specs)
133728e barerepo 1mo
147
if err != nil {
133728e barerepo 1mo
148
return "", nil, err
133728e barerepo 1mo
149
}
133728e barerepo 1mo
150
for _, path := range paths {
133728e barerepo 1mo
151
sha := entries[path]
133728e barerepo 1mo
152
blob := blobs[sha]
133728e barerepo 1mo
153
if blob == nil || blob.Type != "blob" {
133728e barerepo 1mo
154
continue
133728e barerepo 1mo
155
}
133728e barerepo 1mo
156
if path == "meta" {
133728e barerepo 1mo
157
t.Meta = ParseMeta(blob.Body)
133728e barerepo 1mo
158
t.metaBlob = sha
133728e barerepo 1mo
159
t.metaWas = blob.Body
133728e barerepo 1mo
160
continue
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
t.keep(path, blob.Body, sha)
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
return commit, t, nil
133728e barerepo 1mo
167
// write builds the tree object.
133728e barerepo 1mo
168
func (t *Tree) write(ctx context.Context, dir string) (string, error) {
133728e barerepo 1mo
169
var entries strings.Builder
133728e barerepo 1mo
170
add := func(path, body, known string) error {
133728e barerepo 1mo
171
blob := known
133728e barerepo 1mo
172
if blob == "" {
133728e barerepo 1mo
173
out, err := gitx.RunStdin(ctx, dir, body, "hash-object", "-w", "--stdin")
133728e barerepo 1mo
174
if err != nil {
133728e barerepo 1mo
175
return err
133728e barerepo 1mo
176
}
133728e barerepo 1mo
177
blob = strings.TrimSpace(out)
133728e barerepo 1mo
178
}
133728e barerepo 1mo
179
fmt.Fprintf(&entries, "100644 blob %s\t%s\n", blob, path)
133728e barerepo 1mo
180
return nil
133728e barerepo 1mo
181
}
133728e barerepo 1mo
182
// Only what this write changed is hashed again.
133728e barerepo 1mo
183
meta := t.Meta.Render()
133728e barerepo 1mo
184
known := ""
133728e barerepo 1mo
185
if t.metaBlob != "" && meta == t.metaWas {
133728e barerepo 1mo
186
known = t.metaBlob
133728e barerepo 1mo
187
}
133728e barerepo 1mo
188
if err := add("meta", meta, known); err != nil {
133728e barerepo 1mo
189
return "", err
133728e barerepo 1mo
190
}
133728e barerepo 1mo
191
for _, path := range t.order {
133728e barerepo 1mo
192
body := t.Notes[path]
133728e barerepo 1mo
193
if strings.TrimSpace(body) == "" {
133728e barerepo 1mo
194
continue
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
reuse := ""
133728e barerepo 1mo
197
if !t.dirty[path] {
133728e barerepo 1mo
198
reuse = t.blobs[path]
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
if err := add(path, body, reuse); err != nil {
133728e barerepo 1mo
201
return "", err
133728e barerepo 1mo
202
}
133728e barerepo 1mo
203
}
133728e barerepo 1mo
204
out, err := gitx.RunStdin(ctx, dir, entries.String(), "mktree")
133728e barerepo 1mo
205
if err != nil {
133728e barerepo 1mo
206
return "", err
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
return strings.TrimSpace(out), nil
133728e barerepo 1mo
211
// Create opens a thread, or returns an error if n already exists.
133728e barerepo 1mo
212
func Create(ctx context.Context, dir string, n int, m Meta, object string, first Comment) error {
133728e barerepo 1mo
213
if _, exists, err := ReadMeta(ctx, dir, n); err != nil {
133728e barerepo 1mo
214
return err
133728e barerepo 1mo
215
} else if exists {
133728e barerepo 1mo
216
return fmt.Errorf("thread %d already exists", n)
133728e barerepo 1mo
217
}
133728e barerepo 1mo
218
msg := fmt.Sprintf("open thread %d", n)
133728e barerepo 1mo
219
if m.Author != "" {
133728e barerepo 1mo
220
msg += " by " + m.Author
133728e barerepo 1mo
221
}
133728e barerepo 1mo
222
return Write(ctx, dir, n, msg, func(t *Tree) {