vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 5: an object is served only through a repository that holds it
65b88b3b1940623b6f7fbc686b19e986497c9ff2 · Keenan Nemetz · 17d ago · parent bf1e027c
Phase 5: an object is served only through a repository that holds it The store was instance-wide and content-addressed, so an object was fetchable through any repository whose read rule you passed, provided you knew its sha256. That is now impossible, and the fix is structural rather than a check: the pool still holds the bytes once, and each repository has a hard link into a namespace of its own (data/lfs/repos/<repo>/…). A fetch reads only that namespace, so "which repository owns these bytes" stops being a database question that can go stale and becomes a fact about the filesystem — and gc reconciles the links from the refs, which keeps the promise that derived state can always be thrown away and rebuilt. The alternatives were worse. A (repo, oid) table would have made the read path depend on the index, so deleting data/index.db — which the roadmap promises loses nothing — would have stopped LFS fetches until a rebuild. Namespacing the storage itself would have cost the deduplication the store exists for. Hard links give both: one copy of the bytes, one directory entry per repository, and removing one repository's link leaves the other's data alone. Three consequences that had to be handled rather than assumed: - pre-receive checks pointers against the pushing repository, not the pool. A pointer to an object that exists only in another repository would otherwise pass the push and then 404 on clone. The client re-uploads, the pool deduplicates and the link lands, which is the transfer cost isolation implies. - The sweep walks per repository, because reachability is per repository: an object reachable in one and not another keeps the first link and loses the second while the bytes stay. So `vole gc` is also the repair — it links what a repository reaches and unlinks what it no longer does. - An instance upgrading from the shared pool has no namespaces at all, so every fetch would 404 until somebody read an upgrade note. server.New notices a store with no namespace and runs the link pass in the background once, using gc.Link — the half of gc that creates links and deletes nothing, so an interrupted pass leaves the store more reachable than it found it. A repository rename moves its namespace with it. Reference counting stays rejected, and this makes the argument stronger: the thing that repairs the store is the same walk that cleans it, and it cannot invent a reference that does not exist. Measured: TestAnObjectIsInvisibleToARepositoryThatDoesNotHoldIt and the server level TestAnObjectIsNotFetchableThroughAnotherRepository (404 over the LFS endpoint for an object the shared store demonstrably has); TestTwoRepositoriesShareOneCopyOfTheBytes proves os.SameFile, so isolation costs no deduplication; TestOneRepositoryLosingAPointerKeepsTheOthersLink and TestLinkRepairsAMissingNamespace pin the per-repository sweep and the repair; TestAnUnscopedStoreIsLinkedOnStartup covers the upgrade path. Phase 5 is now complete: locking and object scoping both landed.
internal/hook/lfs.go +4−4
| @@ -22,7 +22,7 @@ import ( | |||
| 22 | 22 | ||
| 23 | 23 | // lfsFindings scans the blobs of the tree a pushed ref will serve. problems | |
| 24 | 24 | // reject the push, warnings are printed and the push proceeds. | |
| 25 | func lfsFindings(g *gitx.Repo, new string, store *lfs.Store, warnBytes int64) (problems, warnings []string, err error) { | ||
| 25 | func lfsFindings(g *gitx.Repo, repo, new string, store *lfs.Store, warnBytes int64) (problems, warnings []string, err error) { | ||
| 26 | 26 | if store == nil { | |
| 27 | 27 | return nil, nil, fmt.Errorf("no LFS store configured") | |
| 28 | 28 | } | |
| @@ -47,7 +47,7 @@ func lfsFindings(g *gitx.Repo, new string, store *lfs.Store, warnBytes int64) (p | |||
| 47 | 47 | if !ok { | |
| 48 | 48 | continue | |
| 49 | 49 | } | |
| 50 | problems = append(problems, pointerProblem(store, p, b.Path)...) | ||
| 50 | problems = append(problems, pointerProblem(store, repo, p, b.Path)...) | ||
| 51 | 51 | continue | |
| 52 | 52 | } | |
| 53 | 53 | if warnBytes <= 0 || b.Size <= warnBytes { | |
| @@ -69,8 +69,8 @@ func lfsFindings(g *gitx.Repo, new string, store *lfs.Store, warnBytes int64) (p | |||
| 69 | 69 | return problems, warnings, nil | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | func pointerProblem(store *lfs.Store, p lfs.Pointer, path string) []string { | ||
| 73 | size, err := store.Stat(p.OID) | ||
| 72 | func pointerProblem(store *lfs.Store, repo string, p lfs.Pointer, path string) []string { | ||
| 73 | size, err := store.Stat(repo, p.OID) | ||
| 74 | 74 | if err != nil { | |
| 75 | 75 | return []string{fmt.Sprintf( | |
| 76 | 76 | "%s points at LFS object %s (%s), which is not on this server. "+ | |