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.
Require a linked issue when a change request opens
0789f22ea633bf995417893c0052abb6a36d5520 · Keenan Nemetz · 9d ago · parent 1265ea55
Require a linked issue when a change request opens Add a per-repository policy for teams that want every proposed update to trace back to an issue. The browser marks Closes as required, and the pre-receive hook applies the same rule to requests added through the CLI or a hand-written metadata commit. Existing requests stay editable and landable.
server/hook/hook_test.go +60
| @@ -7,12 +7,14 @@ import ( | |||
| 7 | 7 | "path/filepath" | |
| 8 | 8 | "strings" | |
| 9 | 9 | "testing" | |
| 10 | "time" | ||
| 10 | 11 | ||
| 11 | 12 | "github.com/rodent-software/vole/ci/runs" | |
| 12 | 13 | "github.com/rodent-software/vole/core/git" | |
| 13 | 14 | "github.com/rodent-software/vole/forge/access" | |
| 14 | 15 | "github.com/rodent-software/vole/forge/identity" | |
| 15 | 16 | "github.com/rodent-software/vole/forge/repo" | |
| 17 | "github.com/rodent-software/vole/metadata" | ||
| 16 | 18 | ) | |
| 17 | 19 | ||
| 18 | 20 | type fixture struct { | |
| @@ -339,3 +341,61 @@ func TestPostReceiveSurvivesAnUnwritableLog(t *testing.T) { | |||
| 339 | 341 | t.Fatalf("an unwritable audit log failed the push (exit %d)", code) | |
| 340 | 342 | } | |
| 341 | 343 | } | |
| 344 | |||
| 345 | // A repository can require a linked issue when a change is opened. The rule is | ||
| 346 | // checked for requests a push adds; turning it on must not block edits to the | ||
| 347 | // requests that were already there. | ||
| 348 | func TestNewChangeRequestRequiresALinkedIssueWhenConfigured(t *testing.T) { | ||
| 349 | t.Parallel() | ||
| 350 | f := newFixture(t, "hello\n") | ||
| 351 | f.setACL(func(a *access.ACL) { a.RequireLinkedIssue = true }) | ||
| 352 | |||
| 353 | g := f.m.Git() | ||
| 354 | now := time.Now().UTC() | ||
| 355 | iss, err := metadata.NewIssue("A bug", "details\n", "owner", nil, nil, now) | ||
| 356 | if err != nil { | ||
| 357 | t.Fatal(err) | ||
| 358 | } | ||
| 359 | if err := metadata.CreateIssue(g, iss, "owner"); err != nil { | ||
| 360 | t.Fatal(err) | ||
| 361 | } | ||
| 362 | issueTip, err := g.RevParse(metadata.Ref) | ||
| 363 | if err != nil { | ||
| 364 | t.Fatal(err) | ||
| 365 | } | ||
| 366 | |||
| 367 | linked, err := metadata.NewRequest("Linked change", "body\n", "owner", "main", f.head, now) | ||
| 368 | if err != nil { | ||
| 369 | t.Fatal(err) | ||
| 370 | } | ||
| 371 | linked.Closes = []string{iss.ID} | ||
| 372 | if err := metadata.CreateRequest(g, linked, "owner"); err != nil { | ||
| 373 | t.Fatal(err) | ||
| 374 | } | ||
| 375 | linkedTip, err := g.RevParse(metadata.Ref) | ||
| 376 | if err != nil { | ||
| 377 | t.Fatal(err) | ||
| 378 | } | ||
| 379 | if code, out := f.runRef(Config{Actor: "owner"}, issueTip, linkedTip, metadata.Ref); code != 0 { | ||
| 380 | t.Fatalf("a linked request was rejected: %s", out) | ||
| 381 | } | ||
| 382 | |||
| 383 | unlinked, err := metadata.NewRequest("Unlinked change", "body\n", "owner", "main", f.head, now) | ||
| 384 | if err != nil { | ||
| 385 | t.Fatal(err) | ||
| 386 | } | ||
| 387 | if err := metadata.CreateRequest(g, unlinked, "owner"); err != nil { | ||
| 388 | t.Fatal(err) | ||
| 389 | } | ||
| 390 | unlinkedTip, err := g.RevParse(metadata.Ref) | ||
| 391 | if err != nil { | ||
| 392 | t.Fatal(err) | ||
| 393 | } | ||
| 394 | code, out := f.runRef(Config{Actor: "owner"}, linkedTip, unlinkedTip, metadata.Ref) | ||
| 395 | if code == 0 { | ||
| 396 | t.Fatal("an unlinked request was accepted") | ||
| 397 | } | ||
| 398 | if !strings.Contains(out, "requires at least one linked issue") { | ||
| 399 | t.Fatalf("the refusal does not explain the rule: %s", out) | ||
| 400 | } | ||
| 401 | } | ||