A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit b7fc5924ee

b7fc5924eece9dee015ea517faf7c52fe4f171af

parent: 0483270185

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-25T15:54:06Z

List an MR's commits on the web page and in mr show

base..head, the same range the diff covers: linked rows with signature
badges on the web, commit:/JSON entries in mr show. Capped at 100 rows
on the web page.
e2e/mr_test.go +13
@@ -267,4 +267,17 @@ func TestMergeRequests(t *testing.T) {
267267 if !strings.Contains(body, "feature.txt") {
268268 t.Fatalf("merged MR web diff empty:\n%s", body)
269269 }
270 // The MR page lists the commits it carries, linked to commit pages.
271 if !strings.Contains(body, ">commits <") || !strings.Contains(body, "/alice/lib/commit/") {
272 t.Fatalf("mr commits section missing:\n%s", body)
273 }
274 // So does mr show, human and JSON.
275 showOut, _, code := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1")
276 if code != 0 || !strings.Contains(showOut, "commit: ") {
277 t.Fatalf("mr show missing commits: %d\n%s", code, showOut)
278 }
279 showJSON, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1", "--json")
280 if !strings.Contains(showJSON, `"commits":[`) || !strings.Contains(showJSON, `"subject"`) {
281 t.Fatalf("mr show json missing commits: %s", showJSON)
282 }
270283 }
internal/control/mr.go +32 −1
@@ -11,6 +11,7 @@ import (
1111 "gitbay.org/gitbay/internal/gitutil"
1212 "gitbay.org/gitbay/internal/policy"
1313 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/sig"
1415 "gitbay.org/gitbay/internal/store"
1516 )
1617
@@ -414,19 +415,49 @@ func runMRShow(c *Ctx, args []string) int {
414415 for _, r := range reviews {
415416 rs = append(rs, reviewOut{r.Reviewer, r.Verdict, r.Stale})
416417 }
418 // The commits this MR carries: base..head, the diff's range.
419 type commitOut struct {
420 SHA string `json:"sha"`
421 Subject string `json:"subject"`
422 }
423 var commits []commitOut
424 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
425 base := mr.MergedBase
426 if base == "" {
427 if b, err := gitutil.MergeBase(dir, "refs/heads/"+mr.TargetRef, mrHeadRef(mr.Number)); err == nil {
428 base = b
429 }
430 }
431 if base != "" {
432 if shas, err := gitutil.RevListRange(dir, base, mrHeadRef(mr.Number)); err == nil {
433 for _, sha := range shas {
434 subject := ""
435 if raw, err := gitutil.ReadCommit(dir, sha); err == nil {
436 if parsed, err := sig.ParseCommit(raw); err == nil {
437 subject = parsed.Subject
438 }
439 }
440 commits = append(commits, commitOut{sha, subject})
441 }
442 }
443 }
417444 d := struct {
418445 mrOut
419446 Checks []checkOut `json:"checks,omitempty"`
420447 Combined string `json:"checks_combined,omitempty"`
421448 UnresolvedThreads int `json:"unresolved_threads,omitempty"`
449 Commits []commitOut `json:"commits,omitempty"`
422450 Comments []commentOut `json:"comments,omitempty"`
423451 Reviews []reviewOut `json:"reviews,omitempty"`
424 }{mrToOut(repo, mr, true), checks, store.CombinedStatus(statuses), unresolved, cs, rs}
452 }{mrToOut(repo, mr, true), checks, store.CombinedStatus(statuses), unresolved, commits, cs, rs}
425453 return c.emit(d, func(w io.Writer) {
426454 fmt.Fprintf(w, "!%d %s [%s] by %s\n%s -> %s @ %.10s\n", d.Number, d.Title, d.State, d.Author, d.Source, d.TargetRef, d.HeadSHA)
427455 if d.Body != "" {
428456 fmt.Fprintf(w, "\n%s\n", d.Body)
429457 }
458 for _, cm := range commits {
459 fmt.Fprintf(w, "commit: %.10s %s\n", cm.SHA, cm.Subject)
460 }
430461 for _, x := range checks {
431462 fmt.Fprintf(w, "check: %s %s\n", x.Context, x.State)
432463 }
internal/httpd/web.go +25 −1
@@ -1345,6 +1345,29 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
13451345 stat.Files++
13461346 }
13471347 }
1348 // The commits this MR carries: base..head, the same range as the diff.
1349 type commitRow struct {
1350 SHA, ShortSHA, Subject, AuthorName, Date string
1351 Sig sigView
1352 }
1353 var commits []commitRow
1354 if base != "" {
1355 const maxMRCommits = 100
1356 shas, _ := gitutil.RevListRange(p.Dir, base, headRef)
1357 if len(shas) > maxMRCommits {
1358 shas = shas[:maxMRCommits]
1359 }
1360 for _, sha := range shas {
1361 v, parsed := s.sigFor(p.Repo, p.Dir, sha)
1362 cr := commitRow{SHA: sha, ShortSHA: sha[:10], Sig: v}
1363 if parsed != nil {
1364 cr.Subject = parsed.Subject
1365 cr.AuthorName = parsed.AuthorName
1366 cr.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02")
1367 }
1368 commits = append(commits, cr)
1369 }
1370 }
13481371 s.render(w, "mr.html", struct {
13491372 repoPage
13501373 MR store.MR
@@ -1355,10 +1378,11 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
13551378 Reviews []store.MRReview
13561379 DiffLines []diffLine
13571380 Stat diffStat
1381 Commits []commitRow
13581382 CanEdit bool
13591383 DetachedThreads []diffThread
13601384 }{p, m, md(m.Body), checks, store.CombinedStatus(checks), renderComments(comments, md),
1361 reviews, lines, stat, s.canEditItem(r, p.Repo, m.Author), detachedThreads})
1385 reviews, lines, stat, commits, s.canEditItem(r, p.Repo, m.Author), detachedThreads})
13621386 }
13631387
13641388 func (s *Server) refs(w http.ResponseWriter, r *http.Request) {
internal/web/templates/mr.html +14
@@ -33,6 +33,20 @@
3333 <p><button type="submit">comment</button></p>
3434 </form>
3535 {{end}}
36{{if .Commits}}<h3>commits <span class="count">{{len .Commits}}</span></h3>
37<ul class="loglist">
38{{range .Commits}}<li>
39 <div class="commitmain">
40 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p>
41 <p class="meta">{{.AuthorName}} · {{.Date}}</p>
42 </div>
43 <div class="commitside">
44 {{template "sigbadge" .Sig}}
45 <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code>
46 </div>
47</li>
48{{end}}
49</ul>{{end}}
3650 <details class="difffold">
3751 <summary><h3>diff</h3><span class="meta">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span></span></summary>
3852 <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span>