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 · 59m ago 4b8c9e1d
history blame raw 7.7 KiB
1package forge
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "testing"
8 "time"
9
10 "rodent.software/vole/core/git"
11 "rodent.software/vole/core/lfs"
12)
13
14// repoWithPointer builds a bare repository whose main branch commits a pointer
15// naming oid, then a second commit that replaces it with plain text. The object
16// is therefore reachable only through history — which is still reachable.
17// repoWithoutPointer is a repository whose history never mentions an LFS pointer.
18func repoWithoutPointer(t *testing.T) string {
19 t.Helper()
20 dir := t.TempDir() + "/repo.git"
21 if err := git.InitBare(dir, "main"); err != nil {
22 t.Fatal(err)
23 }
24 g := git.Open(dir)
25 text, err := g.HashObject([]byte("nothing but text here\n"))
26 if err != nil {
27 t.Fatal(err)
28 }
29 tree, err := g.MkTree([]git.Entry{{Mode: "100644", Type: "blob", SHA: text, Name: "README.md"}})
30 if err != nil {
31 t.Fatal(err)
32 }
33 c, err := g.CommitTree(tree, "", "text only")
34 if err != nil {
35 t.Fatal(err)
36 }
37 if err := g.UpdateRef("refs/heads/main", c); err != nil {
38 t.Fatal(err)
39 }
40 return dir
41}
42
43func repoWithPointer(t *testing.T, oid string) string {
44 t.Helper()
45 dir := t.TempDir() + "/repo.git"
46 if err := git.InitBare(dir, "main"); err != nil {
47 t.Fatal(err)
48 }
49 g := git.Open(dir)
50 pointer, err := g.HashObject(lfs.Pointer{OID: oid, Size: 12}.Encode())
51 if err != nil {
52 t.Fatal(err)
53 }
54 text, err := g.HashObject([]byte("readme\n"))
55 if err != nil {
56 t.Fatal(err)
57 }
58 tree1, err := g.MkTree([]git.Entry{
59 {Mode: "100644", Type: "blob", SHA: pointer, Name: "artifact.bin"},
60 {Mode: "100644", Type: "blob", SHA: text, Name: "README.md"},
61 })
62 if err != nil {
63 t.Fatal(err)
64 }
65 c1, err := g.CommitTree(tree1, "", "add the artifact")
66 if err != nil {
67 t.Fatal(err)
68 }
69 tree2, err := g.MkTree([]git.Entry{
70 {Mode: "100644", Type: "blob", SHA: text, Name: "README.md"},
71 })
72 if err != nil {
73 t.Fatal(err)
74 }
75 c2, err := g.CommitTree(tree2, c1, "drop the artifact")
76 if err != nil {
77 t.Fatal(err)
78 }
79 if err := g.UpdateRef("refs/heads/main", c2); err != nil {
80 t.Fatal(err)
81 }
82 return dir
83}
84
85func storeWith(t *testing.T, blobs ...[]byte) (*lfs.Store, []string) {
86 t.Helper()
87 s := lfs.New(t.TempDir(), 1<<20)
88 var oids []string
89 for _, b := range blobs {
90 sum := sha256.Sum256(b)
91 oid := hex.EncodeToString(sum[:])
92 if _, _, err := s.Put("demo", oid, int64(len(b)), bytes.NewReader(b)); err != nil {
93 t.Fatal(err)
94 }
95 oids = append(oids, oid)
96 }
97 return s, oids
98}
99
100func TestReachableThroughHistorySurvivesAndGarbageDoesNot(t *testing.T) {
101 t.Parallel()
102 kept := []byte("the release artifact")
103 garbage := []byte("nobody points at this")
104 store, oids := storeWith(t, kept, garbage)
105 reachable, orphan := oids[0], oids[1]
106 dir := repoWithPointer(t, reachable)
107
108 res, err := GC(GCOptions{
109 Repos: []GCRepo{{Name: "demo", Dir: dir}},
110 Store: store,
111 Grace: 0,
112 Now: time.Now().Add(time.Hour),
113 })
114 if err != nil {
115 t.Fatal(err)
116 }
117 if !store.Has("demo", reachable) {
118 t.Fatal("swept an object a live ref reaches through history")
119 }
120 if store.Has("demo", orphan) {
121 t.Fatal("kept an object nothing references")
122 }
123 if res.Swept != 1 || res.Marked != 1 {
124 t.Fatalf("result %+v; want 1 marked, 1 swept", res)
125 }
126}
127
128func TestGraceProtectsAFreshUpload(t *testing.T) {
129 t.Parallel()
130 kept := []byte("the release artifact")
131 fresh := []byte("uploaded a moment ago")
132 store, oids := storeWith(t, kept, fresh)
133 dir := repoWithPointer(t, oids[0])
134
135 res, err := GC(GCOptions{Repos: []GCRepo{{Name: "demo", Dir: dir}}, Store: store, Grace: time.Hour})
136 if err != nil {
137 t.Fatal(err)
138 }
139 if !store.Has("demo", oids[1]) {
140 t.Fatal("an upload inside the grace period was swept")
141 }
142 if res.Within != 1 || res.Swept != 0 {
143 t.Fatalf("result %+v; want 1 within grace, 0 swept", res)
144 }
145 // The same store, an hour later: the grace has run out.
146 res, err = GC(GCOptions{
147 Repos: []GCRepo{{Name: "demo", Dir: dir}}, Store: store, Grace: time.Hour,
148 Now: time.Now().Add(2 * time.Hour),
149 })
150 if err != nil {
151 t.Fatal(err)
152 }
153 if store.Has("demo", oids[1]) || res.Swept != 1 {
154 t.Fatalf("result %+v; want the stale object swept", res)
155 }
156 if !store.Has("demo", oids[0]) {
157 t.Fatal("swept the reachable object along with the stale one")
158 }
159}
160
161func TestArchivedLogIsARoot(t *testing.T) {
162 t.Parallel()
163 log := []byte("$ go test ./...\nok\n")
164 store, oids := storeWith(t, log)
165
166 res, err := GC(GCOptions{
167 Repos: nil,
168 Store: store,
169 RunOIDs: map[string][]string{"demo": oids},
170 Grace: 0,
171 Now: time.Now().Add(time.Hour),
172 })
173 if err != nil {
174 t.Fatal(err)
175 }
176 if !store.Has("demo", oids[0]) {
177 t.Fatal("swept an archived CI log; no git ref reaches one, so it must be a root")
178 }
179 if res.Swept != 0 || res.Marked != 1 {
180 t.Fatalf("result %+v", res)
181 }
182}
183
184func TestDryRunRemovesNothing(t *testing.T) {
185 t.Parallel()
186 store, oids := storeWith(t, []byte("garbage"))
187 res, err := GC(GCOptions{Store: store, Grace: 0, DryRun: true, Now: time.Now().Add(time.Hour)})
188 if err != nil {
189 t.Fatal(err)
190 }
191 if !store.Has("demo", oids[0]) {
192 t.Fatal("--dry-run deleted an object")
193 }
194 if res.Swept != 1 {
195 t.Fatalf("dry run reported %d removals; want 1", res.Swept)
196 }
197}
198
199// The sweep is also the repair. Nothing else creates a repository's links, so an object
200// reachable from a ref but absent from its namespace is invisible to that repository until
201// a pass links it — which is the path an instance upgrading onto scoped storage takes.
202func TestLinkRepairsAMissingNamespace(t *testing.T) {
203 t.Parallel()
204 kept := []byte("the release artifact")
205 store, oids := storeWith(t, kept)
206 reachable := oids[0]
207 dir := repoWithPointer(t, reachable)
208
209 // Drop the link, which is what an unscoped store looks like from a repository's side.
210 if err := store.Unlink("demo", reachable); err != nil {
211 t.Fatal(err)
212 }
213 if store.Has("demo", reachable) {
214 t.Fatal("the object is still linked; the fixture proves nothing")
215 }
216
217 res, err := GCLink(GCOptions{
218 Repos: []GCRepo{{Name: "demo", Dir: dir}},
219 Store: store,
220 Grace: 0,
221 Now: time.Now().Add(time.Hour),
222 })
223 if err != nil {
224 t.Fatal(err)
225 }
226 if res.Linked != 1 {
227 t.Fatalf("linked %d object(s), want 1 (%+v)", res.Linked, res)
228 }
229 if !store.Has("demo", reachable) {
230 t.Fatal("the repair did not make the object reachable")
231 }
232 // A repair deletes nothing, however stale the store looks.
233 if res.Swept != 0 || res.Unlinked != 0 {
234 t.Fatalf("the repair pass changed more than links: %+v", res)
235 }
236 // And it is idempotent.
237 again, err := GCLink(GCOptions{Repos: []GCRepo{{Name: "demo", Dir: dir}}, Store: store, Now: time.Now()})
238 if err != nil {
239 t.Fatal(err)
240 }
241 if again.Linked != 0 {
242 t.Fatalf("a second repair linked %d more object(s)", again.Linked)
243 }
244}
245
246// One repository dropping its pointer must not unlink the other repository's view: the
247// bytes are shared, the reachability is not.
248func TestOneRepositoryLosingAPointerKeepsTheOthersLink(t *testing.T) {
249 t.Parallel()
250 shared := []byte("one artifact, two projects")
251 store, oids := storeWith(t, shared)
252 oid := oids[0]
253 // Give the second repository the same object, as a cross-repo upload would.
254 if _, _, err := store.Put("other", oid, int64(len(shared)), bytes.NewReader(shared)); err != nil {
255 t.Fatal(err)
256 }
257 dirA := repoWithPointer(t, oid) // demo reaches it
258 dirB := repoWithoutPointer(t) // other does not
259
260 res, err := GC(GCOptions{
261 Repos: []GCRepo{{Name: "demo", Dir: dirA}, {Name: "other", Dir: dirB}},
262 Store: store,
263 Grace: 0,
264 Now: time.Now().Add(time.Hour),
265 })
266 if err != nil {
267 t.Fatal(err)
268 }
269 if !store.Has("demo", oid) {
270 t.Fatalf("the repository that still points at the object lost it (%+v)", res)
271 }
272 if store.Has("other", oid) {
273 t.Fatalf("a repository that no longer points at the object kept its link (%+v)", res)
274 }
275 if res.Swept != 0 {
276 t.Fatalf("swept the shared bytes while one repository still reaches them: %+v", res)
277 }
278}
279