Commit f0187c7c7e

f0187c7c7ed3ca9ed68c8f8d3f0321562aec3d22

parent: 934524888e

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-17 16:26 UTC

web: a merge request without a merge or close stamp makes no byline claim

Ref #218
internal/httpd/mrpage_test.go +15 −3
@@ -63,7 +63,8 @@ func testMR(state string) store.MR {
6363}
6464
6565// The header states what happened to the MR: who opened, merged, or closed
66// it, and when.
66// it, and when. A merge or close with no stamp (imports, pre-0029 merges)
67// names the state without claiming a byline it cannot back up.
6768func TestMRHeaderByState(t *testing.T) {
6869 open := renderMR(t, testMR("open"), nil, nil)
6970 if !strings.Contains(open, "opened by") {
@@ -90,11 +91,22 @@ func TestMRHeaderByState(t *testing.T) {
9091 }
9192
9293 // Imports and pre-0029 merges carry no stamp; the wording drops the
93 // claim rather than inventing a time.
94 // byline claim rather than naming nobody or inventing a time.
9495 out = renderMR(t, testMR("merged"), nil, nil)
95 if strings.Contains(out, "opened by") || strings.Contains(out, " on 20") {
96 if strings.Contains(out, "opened by") || strings.Contains(out, " on 20") || strings.Contains(out, "merged by") {
9697 t.Errorf("unstamped merged header:\n%s", out)
9798 }
99 if !strings.Contains(out, "merged") {
100 t.Errorf("unstamped merged header drops the state entirely:\n%s", out)
101 }
102
103 out = renderMR(t, testMR("closed"), nil, nil)
104 if strings.Contains(out, "opened by") || strings.Contains(out, " on 20") || strings.Contains(out, "closed without merging by") {
105 t.Errorf("unstamped closed header:\n%s", out)
106 }
107 if !strings.Contains(out, "without merging") {
108 t.Errorf("unstamped closed header drops the state entirely:\n%s", out)
109 }
98110}
99111
100112// Approvals and checks carry their times in the aside, so reading the MR
internal/web/templates/mr.html +4 −1
@@ -3,7 +3,10 @@
33{{$base := printf "/%s/%s/mrs/%d" .Repo.OwnerName .Repo.Name .MR.Number}}
44<h1 class="issuetitle">{{.MR.Title}} <span class="issuenumber">!{{.MR.Number}}</span></h1>
55<p class="issuemeta"><span class="chip chip-{{.MR.State}}">{{if eq .MR.State "source_gone"}}source gone{{else}}{{.MR.State}}{{end}}</span>{{if .MR.Draft}} <span class="chip chip-neutral">draft</span>{{end}}
6 {{if eq .MR.State "merged"}}merged by <a href="/{{.MR.MergedBy}}">{{.MR.MergedBy}}</a> on {{when .MR.MergedAt}}{{else if eq .MR.State "closed"}}closed without merging by <a href="/{{.MR.ClosedBy}}">{{.MR.ClosedBy}}</a> on {{when .MR.ClosedAt}}{{else}}opened by <a href="/{{.MR.Author}}">{{.MR.Author}}</a> on {{when .MR.CreatedAt}}{{end}}
6 {{if and (eq .MR.State "merged") .MR.MergedBy}}merged by <a href="/{{.MR.MergedBy}}">{{.MR.MergedBy}}</a>{{with .MR.MergedAt}} on {{when .}}{{end}}
7 {{else if and (eq .MR.State "closed") .MR.ClosedBy}}closed without merging by <a href="/{{.MR.ClosedBy}}">{{.MR.ClosedBy}}</a>{{with .MR.ClosedAt}} on {{when .}}{{end}}
8 {{else if or (eq .MR.State "merged") (eq .MR.State "closed")}}{{if eq .MR.State "merged"}}merged{{else}}closed without merging{{end}}
9 {{else}}opened by <a href="/{{.MR.Author}}">{{.MR.Author}}</a> on {{when .MR.CreatedAt}}{{end}}
710 · <code>{{.MR.SourceRef}}</code> into <code>{{.MR.TargetRef}}</code></p>
811{{with field . "StackedOn"}}<p class="meta">Stacked on <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{.Number}}">!{{.Number}} {{.Title}}</a>: merges into its branch until that lands, then onto its target.</p>{{end}}
912{{with field . "Stacked"}}<p class="meta">Builds on this: {{range $i, $k := .}}{{if $i}}, {{end}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{$k.Number}}">!{{$k.Number}} {{$k.Title}}</a>{{end}}. Merging with squash or rebase is refused while they are open.</p>{{end}}