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

barerepo / server / internal/proposal/counter.go
131 lines · 4.1kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package proposal is chapter 12: a change pushed to a ref, numbered by the repository itself.
133728e barerepo 1mo
2
package proposal
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"fmt"
133728e barerepo 1mo
7
"strconv"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
11
)
133728e barerepo 1mo
12
133728e barerepo 1mo
13
// CounterRef is a ref, so update-ref makes allocation atomic and the number travels with the repo.
133728e barerepo 1mo
14
const CounterRef = "refs/meta/counter"
133728e barerepo 1mo
15
133728e barerepo 1mo
16
// maxRetries bounds the swap, where contention is two simultaneous pushes, not a herd.
133728e barerepo 1mo
17
const maxRetries = 20
133728e barerepo 1mo
18
133728e barerepo 1mo
19
// Allocate returns the next number, shared by proposals and threads, which are one object.
133728e barerepo 1mo
20
func Allocate(ctx context.Context, dir string) (int, error) {
133728e barerepo 1mo
21
for attempt := 0; attempt < maxRetries; attempt++ {
133728e barerepo 1mo
22
oldBlob, current, err := readCounter(ctx, dir)
133728e barerepo 1mo
23
if err != nil {
133728e barerepo 1mo
24
return 0, err
133728e barerepo 1mo
25
}
133728e barerepo 1mo
26
// Skip what is taken, because chapter 35.5 lets a push leave the counter behind.
133728e barerepo 1mo
27
next := current + 1
133728e barerepo 1mo
28
for taken(ctx, dir, next) {
133728e barerepo 1mo
29
next++
133728e barerepo 1mo
30
}
133728e barerepo 1mo
31
133728e barerepo 1mo
32
newBlob, err := gitx.RunStdin(ctx, dir, strconv.Itoa(next)+"\n", "hash-object", "-w", "--stdin")
133728e barerepo 1mo
33
if err != nil {
133728e barerepo 1mo
34
return 0, err
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
newBlob = strings.TrimSpace(newBlob)
133728e barerepo 1mo
37
133728e barerepo 1mo
38
args := []string{"update-ref", CounterRef, newBlob}
133728e barerepo 1mo
39
// An empty old value means the ref must not exist, which makes the first allocation safe.
133728e barerepo 1mo
40
args = append(args, oldBlob)
133728e barerepo 1mo
41
if _, err := gitx.Run(ctx, dir, args...); err == nil {
133728e barerepo 1mo
42
return next, nil
133728e barerepo 1mo
43
}
133728e barerepo 1mo
44
// Somebody else allocated between the read and the write. Read again.
133728e barerepo 1mo
45
}
133728e barerepo 1mo
46
return 0, fmt.Errorf("could not allocate a number after %d tries; too many pushes at once", maxRetries)
133728e barerepo 1mo
47
}
133728e barerepo 1mo
48
133728e barerepo 1mo
49
// readCounter returns the counter blob and value, and a repository with neither starts at zero.
133728e barerepo 1mo
50
func readCounter(ctx context.Context, dir string) (blob string, value int, err error) {
133728e barerepo 1mo
51
out, err := gitx.Run(ctx, dir, "rev-parse", "--verify", "--quiet", CounterRef)
133728e barerepo 1mo
52
if err != nil {
133728e barerepo 1mo
53
return "", 0, nil // no counter yet
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
blob = strings.TrimSpace(out)
133728e barerepo 1mo
56
133728e barerepo 1mo
57
body, err := gitx.Run(ctx, dir, "cat-file", "blob", blob)
133728e barerepo 1mo
58
if err != nil {
133728e barerepo 1mo
59
// Refuse rather than restart the numbering, which threads already refer to.
133728e barerepo 1mo
60
return "", 0, fmt.Errorf("%s is not readable: %w", CounterRef, err)
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
value, err = strconv.Atoi(strings.TrimSpace(body))
133728e barerepo 1mo
63
if err != nil {
133728e barerepo 1mo
64
return "", 0, fmt.Errorf("%s does not hold a number: %q", CounterRef, strings.TrimSpace(body))
133728e barerepo 1mo
65
}
133728e barerepo 1mo
66
return blob, value, nil
133728e barerepo 1mo
67
}
133728e barerepo 1mo
68
133728e barerepo 1mo
69
func Ref(n int) string { return "refs/proposals/" + strconv.Itoa(n) }
133728e barerepo 1mo
70
133728e barerepo 1mo
71
// RevisionRef retains old tips outside refs/proposals, where a file and directory would collide.
133728e barerepo 1mo
72
func RevisionRef(n, k int) string {
133728e barerepo 1mo
73
return fmt.Sprintf("refs/revisions/%d/%d", n, k)
133728e barerepo 1mo
74
}
133728e barerepo 1mo
75
133728e barerepo 1mo
76
// CurrentRevision is the number the content on screen will take when the next force-push retains it.
133728e barerepo 1mo
77
func CurrentRevision(dir string, n int) int {
133728e barerepo 1mo
78
refs, err := gitx.ListRefs(dir, RevisionPrefix(n))
133728e barerepo 1mo
79
if err != nil {
133728e barerepo 1mo
80
return 0
133728e barerepo 1mo
81
}
133728e barerepo 1mo
82
// The highest kept plus one, because pruning drops the low numbers and a count would reuse them.
133728e barerepo 1mo
83
high := 0
133728e barerepo 1mo
84
for name := range refs {
133728e barerepo 1mo
85
k, err := strconv.Atoi(strings.TrimPrefix(name, RevisionPrefix(n)+"/"))
133728e barerepo 1mo
86
if err == nil && k > high {
133728e barerepo 1mo
87
high = k
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
}
133728e barerepo 1mo
90
return high + 1
133728e barerepo 1mo
91
}
133728e barerepo 1mo
92
133728e barerepo 1mo
93
// RevisionPrefix is where one proposal's retained tips live, as a ref prefix and not a glob.
133728e barerepo 1mo
94
func RevisionPrefix(n int) string {
133728e barerepo 1mo
95
return fmt.Sprintf("refs/revisions/%d", n)
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
133728e barerepo 1mo
98
// RevisionGlob matches every retained revision of proposal n.
133728e barerepo 1mo
99
func RevisionGlob(n int) string {
133728e barerepo 1mo
100
return fmt.Sprintf("refs/revisions/%d/*", n)
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
133728e barerepo 1mo
103
// NewRef is the magic name. It is reserved and never created.
133728e barerepo 1mo
104
const NewRef = "refs/proposals/new"
133728e barerepo 1mo
105
133728e barerepo 1mo
106
// Number reads the proposal number out of a ref name, or returns 0.
133728e barerepo 1mo
107
func Number(ref string) int {
133728e barerepo 1mo
108
rest, ok := strings.CutPrefix(ref, "refs/proposals/")
133728e barerepo 1mo
109
if !ok {
133728e barerepo 1mo
110
return 0
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
// refs/proposals/47 and refs/proposals/47/rev/2 both belong to 47.
133728e barerepo 1mo
113
if i := strings.Index(rest, "/"); i >= 0 {
133728e barerepo 1mo
114
rest = rest[:i]
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
n, err := strconv.Atoi(rest)
133728e barerepo 1mo
117
if err != nil || n <= 0 {
133728e barerepo 1mo
118
return 0
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
return n
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
133728e barerepo 1mo
123
// taken reports whether a number already names a thread or a proposal.
133728e barerepo 1mo
124
func taken(ctx context.Context, dir string, n int) bool {
133728e barerepo 1mo
125
for _, ref := range []string{Ref(n), "refs/notes/threads/" + strconv.Itoa(n)} {
133728e barerepo 1mo
126
if _, err := gitx.Run(ctx, dir, "rev-parse", "--verify", "--quiet", ref); err == nil {
133728e barerepo 1mo
127
return true
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
}
133728e barerepo 1mo
130
return false
133728e barerepo 1mo
131
}
history · rawbarerepo 0.1.0