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.
Let the CLI do what the browser does for the same account
7e0b5950c190caf11089e1aaac10f18678074fed · Keenan Nemetz · 6d ago · parent 6e1fca51
Let the CLI do what the browser does for the same account A contributor could file an issue, comment, manage the item it wrote, and open a change from its own branch — but only in the browser, because a push to `refs/heads/vole` required `maintain`. The role was not the reason: the hook had no per-path authorization for that branch, so the role gate stood in for one. The per-path checker is now applied to every session, not only to run credentials. It asks the ACL the questions the browser's forms ask — CanDiscuss, CanOpenRequest, CanTriage, CanApprove — so a contributor's key can write what a contributor may write, and a maintainer's key is unchanged. A non-maintainer's new file must name that account as its author, which is what the browser does for it; a maintainer keeps writing an imported issue under its original author, because that is a trust decision rather than a capability. Creating the issues branch still needs a maintainer, as it does from the browser, and an approval still cannot approve its author's own change — the tree validator already refused that, and now every transport gets the same answer. The one rule that moved is self-approval for a maintainer's push, which is now refused earlier rather than only at the landing gate. With the branch no longer bundled into `maintain`, CONTRIBUTING and the guides stop telling contributors to use the web. Both transports are one authorization model. Part of #168, stacked on #167, which built the checker this generalises.
server/hook/hook_test.go +7−1
| @@ -153,7 +153,6 @@ func TestContributorCanPushOnlyTheirNamespace(t *testing.T) { | |||
| 153 | 153 | "refs/heads/main", | |
| 154 | 154 | "refs/heads/contrib/someone-else/topic", | |
| 155 | 155 | "refs/heads/contrib/contributor", | |
| 156 | "refs/heads/vole", | ||
| 157 | 156 | "refs/tags/contrib/contributor/v1", | |
| 158 | 157 | } { | |
| 159 | 158 | code, out := f.run("contributor", ref, 0) | |
| @@ -164,6 +163,13 @@ func TestContributorCanPushOnlyTheirNamespace(t *testing.T) { | |||
| 164 | 163 | t.Fatalf("%s: unclear rejection: %s", ref, out) | |
| 165 | 164 | } | |
| 166 | 165 | } | |
| 166 | // The issues branch is not a namespace rule: a contributor may write it. This push | ||
| 167 | // would create it, which is a maintainer's act, and that is the refusal it gets. | ||
| 168 | if code, out := f.run("contributor", "refs/heads/vole", 0); code == 0 { | ||
| 169 | t.Fatal("a contributor created the issues branch") | ||
| 170 | } else if !strings.Contains(out, "creating the issues branch needs a maintainer") { | ||
| 171 | t.Fatalf("unclear rejection: %s", out) | ||
| 172 | } | ||
| 167 | 173 | } | |
| 168 | 174 | ||
| 169 | 175 | func TestInstanceRoleAppliesOnlyToPublicReposAndAdminEverywhere(t *testing.T) { | |