Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 6.2 KiB
1// Numbers are the short aliases people type: `#42` instead of a 26-character
2// ULID.
3//
4// A number is not stored. It is the 1-based rank of the entity's creation commit
5// in the metadata branch's history, counted over issues and change requests
6// together, the way one forge counts both in a single sequence. Three things
7// follow from deriving it rather than allocating it:
8//
9// - No counter and no ledger. Nothing is written when an entity is created, so
10// there is no second serialized commit per issue and no number to lose.
11// - Existing repositories need no backfill. Every issue and request that is
12// already in history gets its number the first time anyone asks.
13// - The answer is the same on every clone and every instance serving the same
14// history, because it is a function of the commits, not of a server's state.
15//
16// The ULID stays the identity: paths, front matter, `closes:` lists, backlinks
17// and the audit log are untouched. A number is a view of the history, so an
18// admin rewind that rewrites that history renumbers what came after it. That is
19// the one cost of not storing a copy, and it is why documentation calls a ULID
20// the durable reference and `#42` a convenience.
21package metadata
22
23import (
24 "regexp"
25 "sort"
26 "strconv"
27 "strings"
28
29 "rodent.software/vole/core/git"
30)
31
32// Kinds of numbered entity. A number is ambiguous without one, because issues
33// and change requests share the sequence.
34const (
35 KindIssue = "issue"
36 KindRequest = "request"
37)
38
39var (
40 // numberIssuePathRE and numberRequestPathRE match the tool that creates an
41 // entity, not every file under it: a comment added to an existing issue in the
42 // same commit must not take a number, and an approval never held one.
43 numberIssuePathRE = regexp.MustCompile(`^issues/([0-9A-HJKMNP-TV-Z]{26})/issue\.md$`)
44 numberRequestPathRE = regexp.MustCompile("^" + regexp.QuoteMeta(ChangesDir) + `/([0-9A-HJKMNP-TV-Z]{26})/change\.md$`)
45 numberRE = regexp.MustCompile(`^[0-9]{1,9}$`)
46)
47
48// Numbers maps entity ids to their creation rank and back.
49type Numbers struct {
50 byID map[string]int
51 byNo map[int]string
52 kind map[string]string
53}
54
55func newNumbers() *Numbers {
56 return &Numbers{
57 byID: map[string]int{},
58 byNo: map[int]string{},
59 kind: map[string]string{},
60 }
61}
62
63// LoadNumbers reads the numbering of a metadata branch. It reads history rather
64// than the current tree, so an entity that has since been deleted keeps the
65// number it was given and no later entity takes it.
66func LoadNumbers(g *git.Repo, ref string) (*Numbers, error) {
67 commit, err := g.RevParse(ref)
68 if err != nil {
69 return nil, err
70 }
71 return LoadNumbersAt(g, commit)
72}
73
74// LoadNumbersAt numbers the entities a commit's history added. It takes the
75// commit rather than a ref so the index can resolve the metadata branch once.
76func LoadNumbersAt(g *git.Repo, commit string) (*Numbers, error) {
77 if commit == "" {
78 return newNumbers(), nil
79 }
80 groups, err := g.AddedPathsPerCommit(commit, IssuesDir, ChangesDir)
81 if err != nil {
82 return nil, err
83 }
84 return RankAdditions(groups), nil
85}
86
87// RankAdditions numbers the entities a history added, oldest commit first. It is
88// the pure half of LoadNumbers, so the rules can be tested without a repository.
89//
90// Within one commit the order is the ULID's, which is creation order because a
91// ULID starts with its timestamp. A path that is deleted and later re-added keeps
92// its first number: an alias someone already quoted must not come to mean
93// something else.
94func RankAdditions(commits [][]string) *Numbers {
95 n := newNumbers()
96 next := 1
97 for _, e := range Additions(commits) {
98 if _, seen := n.byID[e.ID]; seen {
99 continue
100 }
101 n.byID[e.ID] = next
102 n.byNo[next] = e.ID
103 n.kind[e.ID] = e.Kind
104 next++
105 }
106 return n
107}
108
109// Entity is one thing the numbering sequence counts.
110type Entity struct {
111 ID string
112 Kind string
113}
114
115// Additions turns per-commit path lists into the entities those commits created,
116// oldest commit first and ULID-ordered within one commit. It is the half of
117// RankAdditions that an incremental refresh reuses: a range of new commits can
118// be ranked without replaying the whole history.
119func Additions(commits [][]string) []Entity {
120 var out []Entity
121 for _, paths := range commits {
122 var batch []Entity
123 for _, path := range paths {
124 if m := numberIssuePathRE.FindStringSubmatch(path); m != nil {
125 batch = append(batch, Entity{ID: m[1], Kind: KindIssue})
126 continue
127 }
128 if m := numberRequestPathRE.FindStringSubmatch(path); m != nil {
129 batch = append(batch, Entity{ID: m[1], Kind: KindRequest})
130 }
131 }
132 sort.Slice(batch, func(i, j int) bool { return batch[i].ID < batch[j].ID })
133 out = append(out, batch...)
134 }
135 return out
136}
137
138// Of returns the number an entity was given.
139func (n *Numbers) Of(id string) (int, bool) {
140 if n == nil {
141 return 0, false
142 }
143 num, ok := n.byID[id]
144 return num, ok
145}
146
147// At returns the entity a number names, and which kind it is.
148func (n *Numbers) At(number int) (id, kind string, ok bool) {
149 if n == nil {
150 return "", "", false
151 }
152 id, ok = n.byNo[number]
153 if !ok {
154 return "", "", false
155 }
156 return id, n.kind[id], true
157}
158
159// Kind returns the kind of a numbered entity, or "" when it has no number.
160func (n *Numbers) Kind(id string) string {
161 if n == nil {
162 return ""
163 }
164 return n.kind[id]
165}
166
167// Len is how many entities have a number.
168func (n *Numbers) Len() int {
169 if n == nil {
170 return 0
171 }
172 return len(n.byID)
173}
174
175// TrimReference removes the decoration a typed reference carries: surrounding
176// space and the leading `#` of `#42`. Every id-taking surface trims it, so the way
177// someone writes a reference in prose is the way they may pass it on the command
178// line.
179func TrimReference(given string) string {
180 return strings.TrimPrefix(strings.TrimSpace(given), "#")
181}
182
183// ParseNumber reports whether a typed id is a number rather than a ULID or a
184// ULID prefix. A leading `#` is accepted. Length is the discriminator: a ULID is
185// 26 characters, so a shorter run of digits cannot be one and is read as an
186// alias. Nothing a person types is ambiguous between the two.
187func ParseNumber(given string) (int, bool) {
188 if !numberRE.MatchString(TrimReference(given)) {
189 return 0, false
190 }
191 num, err := strconv.Atoi(TrimReference(given))
192 if err != nil || num < 1 {
193 return 0, false
194 }
195 return num, true
196}
197