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.
Split repository settings into tabs
e189a23e457b6aa01a35a4586586713cdc2d426d · Keenan Nemetz · 23h ago · parent 87ebcc5a
Split repository settings into tabs One page held four audiences and ten forms. Each part is now its own tab: General for the repository's own fields, Access for the instance-admin rules, Secrets for the values jobs read, and Danger for rename and delete. A tab is offered only to the accounts that may use it, and a viewer with one tab gets no row at all. The Access and Secrets tabs, and the failures on each, keep their own URLs so a redirect and an error land where the form was; the Access and Secrets fragments survive as deep links.
server/admin_web_test.go +21−11
| @@ -19,9 +19,9 @@ func TestRepositoryAccessSettings(t *testing.T) { | |||
| 19 | 19 | f.addWriter(t) | |
| 20 | 20 | sess, cookie := f.browserSession(t, f.ownerID) | |
| 21 | 21 | ||
| 22 | // Access rules are instance-admin settings, so they live on the repository's | ||
| 23 | // own Settings page rather than in a separate admin surface. | ||
| 24 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings", cookie) | ||
| 22 | // Access rules are instance-admin settings, so they are a tab on the | ||
| 23 | // repository's own Settings page rather than a separate admin surface. | ||
| 24 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?tab=access", cookie) | ||
| 25 | 25 | if resp.StatusCode != 200 { | |
| 26 | 26 | t.Fatalf("settings page = %d:\n%s", resp.StatusCode, body) | |
| 27 | 27 | } | |
| @@ -50,10 +50,10 @@ func TestRepositoryAccessSettings(t *testing.T) { | |||
| 50 | 50 | "runner:" + f.writerID: {"on"}, | |
| 51 | 51 | } | |
| 52 | 52 | resp, _ = f.browserPost(t, "/r/"+f.repo+"/settings/access", cookie, form) | |
| 53 | if resp.StatusCode != 303 || resp.Header.Get("Location") != "/r/"+f.repo+"/settings?saved=access#access" { | ||
| 53 | if resp.StatusCode != 303 || resp.Header.Get("Location") != "/r/"+f.repo+"/settings?tab=access&saved=access#access" { | ||
| 54 | 54 | t.Fatalf("save = %d %q", resp.StatusCode, resp.Header.Get("Location")) | |
| 55 | 55 | } | |
| 56 | if resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?saved=access", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Access rules saved") { | ||
| 56 | if resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?tab=access&saved=access", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Access rules saved") { | ||
| 57 | 57 | t.Fatalf("saved access page = %d:\n%s", resp.StatusCode, body) | |
| 58 | 58 | } | |
| 59 | 59 | ||
| @@ -198,13 +198,23 @@ func TestRepositoryAccessRequiresInstanceAdmin(t *testing.T) { | |||
| 198 | 198 | if _, err := f.srv.Identity.SetAdmin(f.writerID, true, "test"); err != nil { | |
| 199 | 199 | t.Fatal(err) | |
| 200 | 200 | } | |
| 201 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings", writerCookie) | ||
| 201 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?tab=access", writerCookie) | ||
| 202 | 202 | if resp.StatusCode != 200 || !strings.Contains(body, `id="access"`) { | |
| 203 | 203 | t.Fatalf("instance-admin settings = %d:\n%s", resp.StatusCode, body) | |
| 204 | 204 | } | |
| 205 | for _, want := range []string{"Danger zone", "Default branch"} { | ||
| 206 | if !strings.Contains(body, want) { | ||
| 207 | t.Fatalf("instance admin does not see %q:\n%s", want, body) | ||
| 205 | // The repository's own fields and the destructive operations are separate | ||
| 206 | // tabs, so they are read from those URLs too. | ||
| 207 | resp, general := f.browserGet(t, "/r/"+f.repo+"/settings", writerCookie) | ||
| 208 | if resp.StatusCode != 200 || !strings.Contains(general, "Default branch") { | ||
| 209 | t.Fatalf("instance-admin general tab = %d:\n%s", resp.StatusCode, general) | ||
| 210 | } | ||
| 211 | resp, danger := f.browserGet(t, "/r/"+f.repo+"/settings?tab=danger", writerCookie) | ||
| 212 | if resp.StatusCode != 200 { | ||
| 213 | t.Fatalf("instance-admin danger tab = %d:\n%s", resp.StatusCode, danger) | ||
| 214 | } | ||
| 215 | for _, want := range []string{"Rename repository", "Delete repository"} { | ||
| 216 | if !strings.Contains(danger, want) { | ||
| 217 | t.Fatalf("instance admin does not see %q:\n%s", want, danger) | ||
| 208 | 218 | } | |
| 209 | 219 | } | |
| 210 | 220 | if _, home := f.browserGet(t, "/", writerCookie); !strings.Contains(home, `href="/admin"`) { | |
| @@ -305,7 +315,7 @@ func TestRepositoryAccessListsOnlyExplicitAccountsAndCanAddOne(t *testing.T) { | |||
| 305 | 315 | } | |
| 306 | 316 | sess, cookie := f.browserSession(t, f.ownerID) | |
| 307 | 317 | ||
| 308 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings", cookie) | ||
| 318 | resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?tab=access", cookie) | ||
| 309 | 319 | if resp.StatusCode != 200 { | |
| 310 | 320 | t.Fatalf("settings = %d:\n%s", resp.StatusCode, body) | |
| 311 | 321 | } | |
| @@ -338,7 +348,7 @@ func TestRepositoryAccessListsOnlyExplicitAccountsAndCanAddOne(t *testing.T) { | |||
| 338 | 348 | if acl.Entries["carol"] != access.Contribute.String() { | |
| 339 | 349 | t.Fatalf("carol entry = %q, want contribute", acl.Entries["carol"]) | |
| 340 | 350 | } | |
| 341 | _, body = f.browserGet(t, "/r/"+f.repo+"/settings", cookie) | ||
| 351 | _, body = f.browserGet(t, "/r/"+f.repo+"/settings?tab=access", cookie) | ||
| 342 | 352 | if !strings.Contains(body, `name="role:carol"`) { | |
| 343 | 353 | t.Fatalf("carol is not in the access table after being added:\n%s", body) | |
| 344 | 354 | } | |