vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Show what a merged change request landed
2841d922c15ce32fb600d992f9f5dc2d22c48f90 · Keenan Nemetz · 13d ago · parent c765ae2e
Show what a merged change request landed Landing moves the base to include the head, so the three-dot diff the Files tab reviews an open request with becomes empty by definition: the merge base of the base and the head is the head itself. A merged request therefore looked like it had never changed anything. The landing mark now records `merged_base`, the tip the base pointed at before it moved, and the Files tab diffs that against what landed: the head for a fast-forward, the merge commit otherwise, so conflict resolution is part of the review too. The record is written in the same commit as the mark, and a retry that finds the head already landed keeps the base it has instead of naming the moved tip. `merged_base` is optional on read because landings made before the field existed have none. Such a record falls back to the merge commit's first parent when the landing was a merge, and claims nothing when it was a fast-forward and the starting point is gone. The request page shows the file count and the review link for a merged request as well, with the merge named beside it, instead of replacing them with "Already merged".
metadata/requests_test.go +43−19
| @@ -207,12 +207,25 @@ func TestMergedStateNamesTheCommitThatLanded(t *testing.T) { | |||
| 207 | 207 | return m | |
| 208 | 208 | } | |
| 209 | 209 | ||
| 210 | if _, err := ParseRequests(merged("merged_head: " + sideSHA + "\nmerged_by: wilbur\nmerged_at: 2026-10-23T09:00:00Z\n")); err != nil { | ||
| 210 | verified := "merged_head: " + sideSHA + "\nmerged_by: wilbur\nmerged_at: 2026-10-23T09:00:00Z\n" | ||
| 211 | if _, err := ParseRequests(merged(verified)); err != nil { | ||
| 211 | 212 | t.Fatalf("a merged request with a commit was refused: %v", err) | |
| 212 | 213 | } | |
| 214 | // merged_base records what the base pointed at before the landing, which is what | ||
| 215 | // a merged request is diffed against. It is optional on read so records written | ||
| 216 | // before it existed still load, but the value has to be a commit. | ||
| 217 | withBase := merged(verified + "merged_base: " + headSHA + "\n") | ||
| 218 | list, err := ParseRequests(withBase) | ||
| 219 | if err != nil { | ||
| 220 | t.Fatalf("a merged request naming its base was refused: %v", err) | ||
| 221 | } | ||
| 222 | if list[0].MergedBase != headSHA { | ||
| 223 | t.Fatalf("merged_base = %q, want %q", list[0].MergedBase, headSHA) | ||
| 224 | } | ||
| 213 | 225 | for name, extra := range map[string]string{ | |
| 214 | 226 | "no merged_head": "merged_by: wilbur\nmerged_at: 2026-10-23T09:00:00Z\n", | |
| 215 | 227 | "short merged_head": "merged_head: " + headSHA[:12] + "\nmerged_by: wilbur\nmerged_at: 2026-10-23T09:00:00Z\n", | |
| 228 | "short merged_base": verified + "merged_base: " + headSHA[:12] + "\n", | ||
| 216 | 229 | "no merged_by": "merged_head: " + sideSHA + "\nmerged_at: 2026-10-23T09:00:00Z\n", | |
| 217 | 230 | "no merged_at": "merged_head: " + sideSHA + "\nmerged_by: wilbur\n", | |
| 218 | 231 | "merged_at is not TS": "merged_head: " + sideSHA + "\nmerged_by: wilbur\nmerged_at: yesterday\n", | |
| @@ -222,19 +235,24 @@ func TestMergedStateNamesTheCommitThatLanded(t *testing.T) { | |||
| 222 | 235 | } | |
| 223 | 236 | } | |
| 224 | 237 | // A request that is not merged may not carry the record either. | |
| 225 | pending := map[string]string{ | ||
| 226 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 227 | `title: "Land the retry"` + "\n" + | ||
| 228 | "state: open\n" + | ||
| 229 | "author: keenan\n" + | ||
| 230 | "base: main\n" + | ||
| 231 | "head: " + headSHA + "\n" + | ||
| 232 | "created: 2026-10-22T22:00:00Z\n" + | ||
| 233 | "merged_head: " + sideSHA + "\n" + | ||
| 234 | "---\n", | ||
| 235 | } | ||
| 236 | if _, err := ParseRequests(pending); err == nil { | ||
| 237 | t.Error("an open request with a merged_head was accepted") | ||
| 238 | for field, line := range map[string]string{ | ||
| 239 | "merged_head": "merged_head: " + sideSHA + "\n", | ||
| 240 | "merged_base": "merged_base: " + sideSHA + "\n", | ||
| 241 | } { | ||
| 242 | pending := map[string]string{ | ||
| 243 | RequestsDir + "/" + reqIDA + "/request.md": "---\n" + | ||
| 244 | `title: "Land the retry"` + "\n" + | ||
| 245 | "state: open\n" + | ||
| 246 | "author: keenan\n" + | ||
| 247 | "base: main\n" + | ||
| 248 | "head: " + headSHA + "\n" + | ||
| 249 | "created: 2026-10-22T22:00:00Z\n" + | ||
| 250 | line + | ||
| 251 | "---\n", | ||
| 252 | } | ||
| 253 | if _, err := ParseRequests(pending); err == nil { | ||
| 254 | t.Errorf("an open request with a %s was accepted", field) | ||
| 255 | } | ||
| 238 | 256 | } | |
| 239 | 257 | } | |
| 240 | 258 | ||
| @@ -340,12 +358,12 @@ func TestMarkMergedClosesLinkedIssues(t *testing.T) { | |||
| 340 | 358 | t.Fatal("a request closing a non-existent issue was written") | |
| 341 | 359 | } | |
| 342 | 360 | ||
| 343 | got, closed, err := MarkMerged(g, req.ID, headSHA, "reviewer", now) | ||
| 361 | got, closed, err := MarkMerged(g, req.ID, sideSHA, headSHA, "reviewer", now) | ||
| 344 | 362 | if err != nil { | |
| 345 | 363 | t.Fatalf("mark: %v", err) | |
| 346 | 364 | } | |
| 347 | if got.State != "merged" || got.MergedHead != headSHA { | ||
| 348 | t.Fatalf("request = %q/%q", got.State, got.MergedHead) | ||
| 365 | if got.State != "merged" || got.MergedHead != headSHA || got.MergedBase != sideSHA { | ||
| 366 | t.Fatalf("request = %q/%q/%q", got.State, got.MergedBase, got.MergedHead) | ||
| 349 | 367 | } | |
| 350 | 368 | if len(closed) != 1 || closed[0] != linked.ID { | |
| 351 | 369 | t.Fatalf("closed = %v, want [%s] — the already-closed issue is not rewritten", closed, linked.ID) | |
| @@ -360,9 +378,15 @@ func TestMarkMergedClosesLinkedIssues(t *testing.T) { | |||
| 360 | 378 | } | |
| 361 | 379 | } | |
| 362 | 380 | // Marking again is a no-op, so a retried landing does not re-close anything. | |
| 363 | if _, again, err := MarkMerged(g, req.ID, headSHA, "reviewer", now); err != nil || len(again) != 0 { | ||
| 381 | // A retry that cannot name the base keeps the one the record already has. | ||
| 382 | if _, again, err := MarkMerged(g, req.ID, "", headSHA, "reviewer", now); err != nil || len(again) != 0 { | ||
| 364 | 383 | t.Fatalf("second mark = %v, %v", again, err) | |
| 365 | 384 | } | |
| 385 | if again, err := GetRequest(g, req.ID); err != nil { | ||
| 386 | t.Fatal(err) | ||
| 387 | } else if again.MergedBase != sideSHA { | ||
| 388 | t.Fatalf("a retry without a base dropped it: merged_base = %q, want %q", again.MergedBase, sideSHA) | ||
| 389 | } | ||
| 366 | 390 | } | |
| 367 | 391 | ||
| 368 | 392 | // A link is often discovered after a request is opened, so it can be set later. The | |
| @@ -415,7 +439,7 @@ func TestSetRequestClosesEditsAnOpenRequest(t *testing.T) { | |||
| 415 | 439 | } | |
| 416 | 440 | } | |
| 417 | 441 | // Once merged, the list is a record of what happened, not a setting. | |
| 418 | if _, _, err := MarkMerged(g, req.ID, headSHA, "reviewer", now); err != nil { | ||
| 442 | if _, _, err := MarkMerged(g, req.ID, sideSHA, headSHA, "reviewer", now); err != nil { | ||
| 419 | 443 | t.Fatal(err) | |
| 420 | 444 | } | |
| 421 | 445 | if _, err := SetRequestCloses(g, req.ID, nil, "keenan"); err == nil { | |