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.
Refuse a protected-ref pattern that can never match
93c7cae8ebb7d6d853dca476851e2bd0cbe10079 · Keenan Nemetz · 18h ago · parent 2c988ecd
Refuse a protected-ref pattern that can never match acl protect stored any string: refs/head/main read like a protected branch in acl show while the hook compared it against refs/heads/main and matched nothing, so approvals and required checks were off on the branch everything lands on. Validate the pattern in forge/access, where the rule is stored, and call it from both acl protect add|set and the repository settings form. Only the two shapes IsProtected understands are accepted, exact ref and one trailing wildcard, the ref part must be a name git could store, and a singular category is named as the typo it is.
server/admin_web_test.go +46
| @@ -125,6 +125,52 @@ func TestRepositoryAccessSettings(t *testing.T) { | |||
| 125 | 125 | } | |
| 126 | 126 | } | |
| 127 | 127 | ||
| 128 | // A pattern the hook can never match must be refused by the form, not stored and | ||
| 129 | // echoed back by the settings page as if the branch were protected. | ||
| 130 | func TestRepositoryAccessRefusesAPatternThatCannotMatch(t *testing.T) { | ||
| 131 | t.Parallel() | ||
| 132 | f := browserFixture(t) | ||
| 133 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 134 | t.Fatal(err) | ||
| 135 | } | ||
| 136 | sess, cookie := f.browserSession(t, f.ownerID) | ||
| 137 | |||
| 138 | form := url.Values{ | ||
| 139 | "csrf": {sess.CSRF}, | ||
| 140 | "default": {"contribute"}, | ||
| 141 | "approvals": {"1"}, | ||
| 142 | "protected": {"refs/head/main"}, | ||
| 143 | } | ||
| 144 | resp, body := f.browserPost(t, "/r/"+f.repo+"/settings/access", cookie, form) | ||
| 145 | if resp.StatusCode != 400 { | ||
| 146 | t.Fatalf("a pattern that cannot match was saved: %d", resp.StatusCode) | ||
| 147 | } | ||
| 148 | if !strings.Contains(body, "refs/heads/main") { | ||
| 149 | t.Fatalf("the refusal does not name the fix:\n%s", body) | ||
| 150 | } | ||
| 151 | m, err := f.srv.Repos.Open(f.repo) | ||
| 152 | if err != nil { | ||
| 153 | t.Fatal(err) | ||
| 154 | } | ||
| 155 | if acl, err := m.ACL(); err != nil { | ||
| 156 | t.Fatal(err) | ||
| 157 | } else if len(acl.Protected) != 0 { | ||
| 158 | t.Fatalf("the refusal stored %v", acl.Protected) | ||
| 159 | } | ||
| 160 | |||
| 161 | // A valid pattern still saves and round-trips through the page. | ||
| 162 | form.Set("protected", "refs/heads/main\nrefs/heads/release/*") | ||
| 163 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/settings/access", cookie, form) | ||
| 164 | if resp.StatusCode != 303 { | ||
| 165 | t.Fatalf("valid save = %d", resp.StatusCode) | ||
| 166 | } | ||
| 167 | if acl, err := m.ACL(); err != nil { | ||
| 168 | t.Fatal(err) | ||
| 169 | } else if got := strings.Join(acl.Protected, ","); got != "refs/heads/main,refs/heads/release/*" { | ||
| 170 | t.Fatalf("protected = %q", got) | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 128 | 174 | func TestInstanceAdminCanBrowsePrivateRepoWithoutACLEntry(t *testing.T) { | |
| 129 | 175 | t.Parallel() | |
| 130 | 176 | f := browserFixture(t) | |