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.
Roadmap: order the LFS slices and drop the stale Remaining block
671f2aa75e988725c1bfeea1279526dd01a8a2b6 · Keenan Nemetz · 16d ago · parent 65b88b3b
Roadmap: order the LFS slices and drop the stale Remaining block Slice 5 (locking) had landed after slice 6, and the "Remaining" heading still held a parenthetical about artifact uploads that shipped in Phase 7 slice 6.
docs/roadmap.md +39−44
| @@ -647,11 +647,46 @@ reachable from every ref in every repository — plus archived logs, which no re | |||
| 647 | 647 | — and removes the rest. Anything younger than the grace period (default 1h) is kept, | |
| 648 | 648 | because an upload that has not been pushed yet is not garbage. | |
| 649 | 649 | ||
| 650 | Remaining: | ||
| 650 | ✅ **Slice 5 — file locking.** `git lfs lock <path>` works over the SSH transfer, so a | ||
| 651 | binary that git cannot merge can be reserved by one person before they edit it: a push | ||
| 652 | that changes a locked path is refused, naming the holder, and the lock is released when | ||
| 653 | its holder says so. This is what makes a repository of design files usable by more than | ||
| 654 | one person, and it is the reason a studio would pick a forge with it over one without. | ||
| 655 | |||
| 656 | The HTTP locking API is the usual path for git-lfs, and it is a *write* surface — which | ||
| 657 | this instance does not have, because its web is anonymous and read-only. So locking rides | ||
| 658 | the SSH transfer protocol instead (`lock`, `list-lock`, `unlock`, advertised as the | ||
| 659 | `locking` capability), which means the key that pushed is the key that locks: no second | ||
| 660 | endpoint, no token, nothing new to authenticate. Locks live in `data/users.db` beside the | ||
| 661 | roster and the access rules — the file that holds what git cannot rebuild — because a lock | ||
| 662 | resurrected from history would be exactly wrong: the point of a lock is that it is | ||
| 663 | released. They are keyed by account id, so a rename changes nothing. | ||
| 664 | |||
| 665 | The interesting decision is *where the refusal lives*. git-lfs enforces locks on the | ||
| 666 | client: it asks the server what is held and declines to send the push. That is a courtesy, | ||
| 667 | and a plain `git push` walks straight past it — so the rule also sits in `pre-receive`, at | ||
| 668 | the one place this forge enforces everything else, where it cannot be turned off by a | ||
| 669 | client setting. What it guards is narrow on purpose: only the paths the push actually | ||
| 670 | writes, so reading, branching and merging untouched history are all unaffected. An admin | ||
| 671 | may override, audited, for the same reason a force-push is allowed: otherwise a lock whose | ||
| 672 | holder has left makes a path unpushable forever. | ||
| 673 | |||
| 674 | `/r/<repo>/locks` answers "who is holding what", which the protocol otherwise hides | ||
| 675 | completely — the next person to push is refused with no way to find out why without asking | ||
| 676 | somebody. | ||
| 677 | (Measured: `TestALockRefusesAPushUntilItIsReleased` drives it through the binary over SSH — | ||
| 678 | lock, refused push naming the holder, unlock, the same push landing; `TestLocksAre- | ||
| 679 | AdvertisedOnlyWhenOffered` pins that the capability follows the lock table, since a client | ||
| 680 | that reads `locking` will offer a command that must work; the store's own tests cover path | ||
| 681 | normalization (one file, one lock, however it was typed), paging, owner partitioning and | ||
| 682 | the audited override.) | ||
| 683 | |||
| 684 | **Known gap, stated rather than assumed:** git-lfs is not installed in this environment, so | ||
| 685 | the protocol was implemented against `docs/proposals/ssh_adapter.md` and verified with a | ||
| 686 | client written to that document, not with a shipped client. The framing, the 201/409 | ||
| 687 | argument blocks and the five-line lock-spec records are the spec's; whether a real | ||
| 688 | `git lfs lock` agrees is the first thing to check on a machine that has one. | ||
| 651 | 689 | ||
| 652 | - (Artifact uploads from workflows landed in Phase 7 as run artifacts: a job declares | ||
| 653 | `artifacts:`, the runner publishes the matches to the object store, and the run page | ||
| 654 | serves them. See Phase 7 slice 6.) | ||
| 655 | 690 | ✅ **Slice 6 — objects are scoped to the repository that owns them.** The store was | |
| 656 | 691 | instance-wide and content-addressed, so an object was fetchable through *any* repository | |
| 657 | 692 | whose read rule you passed, provided you knew its sha256. That is now impossible, and the | |
| @@ -692,46 +727,6 @@ slow — and `gc` is an operator command, not a hot path. The link pass makes th | |||
| 692 | 727 | stronger: the thing that repairs the store is the same walk that cleans it, and it cannot | |
| 693 | 728 | invent a reference that does not exist. | |
| 694 | 729 | ||
| 695 | ✅ **Slice 5 — file locking.** `git lfs lock <path>` works over the SSH transfer, so a | ||
| 696 | binary that git cannot merge can be reserved by one person before they edit it: a push | ||
| 697 | that changes a locked path is refused, naming the holder, and the lock is released when | ||
| 698 | its holder says so. This is what makes a repository of design files usable by more than | ||
| 699 | one person, and it is the reason a studio would pick a forge with it over one without. | ||
| 700 | |||
| 701 | The HTTP locking API is the usual path for git-lfs, and it is a *write* surface — which | ||
| 702 | this instance does not have, because its web is anonymous and read-only. So locking rides | ||
| 703 | the SSH transfer protocol instead (`lock`, `list-lock`, `unlock`, advertised as the | ||
| 704 | `locking` capability), which means the key that pushed is the key that locks: no second | ||
| 705 | endpoint, no token, nothing new to authenticate. Locks live in `data/users.db` beside the | ||
| 706 | roster and the access rules — the file that holds what git cannot rebuild — because a lock | ||
| 707 | resurrected from history would be exactly wrong: the point of a lock is that it is | ||
| 708 | released. They are keyed by account id, so a rename changes nothing. | ||
| 709 | |||
| 710 | The interesting decision is *where the refusal lives*. git-lfs enforces locks on the | ||
| 711 | client: it asks the server what is held and declines to send the push. That is a courtesy, | ||
| 712 | and a plain `git push` walks straight past it — so the rule also sits in `pre-receive`, at | ||
| 713 | the one place this forge enforces everything else, where it cannot be turned off by a | ||
| 714 | client setting. What it guards is narrow on purpose: only the paths the push actually | ||
| 715 | writes, so reading, branching and merging untouched history are all unaffected. An admin | ||
| 716 | may override, audited, for the same reason a force-push is allowed: otherwise a lock whose | ||
| 717 | holder has left makes a path unpushable forever. | ||
| 718 | |||
| 719 | `/r/<repo>/locks` answers "who is holding what", which the protocol otherwise hides | ||
| 720 | completely — the next person to push is refused with no way to find out why without asking | ||
| 721 | somebody. | ||
| 722 | (Measured: `TestALockRefusesAPushUntilItIsReleased` drives it through the binary over SSH — | ||
| 723 | lock, refused push naming the holder, unlock, the same push landing; `TestLocksAre- | ||
| 724 | AdvertisedOnlyWhenOffered` pins that the capability follows the lock table, since a client | ||
| 725 | that reads `locking` will offer a command that must work; the store's own tests cover path | ||
| 726 | normalization (one file, one lock, however it was typed), paging, owner partitioning and | ||
| 727 | the audited override.) | ||
| 728 | |||
| 729 | **Known gap, stated rather than assumed:** git-lfs is not installed in this environment, so | ||
| 730 | the protocol was implemented against `docs/proposals/ssh_adapter.md` and verified with a | ||
| 731 | client written to that document, not with a shipped client. The framing, the 201/409 | ||
| 732 | argument blocks and the five-line lock-spec records are the spec's; whether a real | ||
| 733 | `git lfs lock` agrees is the first thing to check on a machine that has one. | ||
| 734 | |||
| 735 | 730 | **Exit criteria:** a clone of an LFS repository pulls real objects through the proxy | |
| 736 | 731 | (measured: a 256 KiB artifact pushed over SSH and pulled back byte-identical via | |
| 737 | 732 | `http://…/git/<repo>.git`, with the sha256 checked on both ends); a pointer pushed | |