Commit acbc0f60ee

acbc0f60ee8435ed6470cc17faa8508a5c8eb597

parent: f830a4d84c

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-24 03:06 UTC

control: mr show on the view layout

Ref #254
e2e/gates_test.go +1 −1
@@ -88,7 +88,7 @@ func TestMergeGatesVisible(t *testing.T) {
8888 if strings.Contains(out, `"unmet"`) || !strings.Contains(out, `"approvals":["bob"]`) {
8989 t.Fatalf("gates after approval:\n%s", out)
9090 }
91 if out, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/svc", "1"); !strings.Contains(out, "gates: met; fast-forward possible") {
91 if out, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/svc", "1"); !strings.Contains(out, "met; fast-forward possible") {
9292 t.Fatalf("text gates line: %s", out)
9393 }
9494 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/svc", "1"); code != 0 {
e2e/mr_test.go +1 −1
@@ -282,7 +282,7 @@ func TestMergeRequests(t *testing.T) {
282282 }
283283 // So does mr show, human and JSON.
284284 showOut, _, code := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1")
285 if code != 0 || !strings.Contains(showOut, "commit: ") {
285 if code != 0 || !strings.Contains(showOut, "add feature (amended)") {
286286 t.Fatalf("mr show missing commits: %d\n%s", code, showOut)
287287 }
288288 showJSON, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1", "--json")
internal/control/mr.go +114 −41
@@ -602,75 +602,148 @@ func runMRShow(c *Ctx, args []string) int {
602602 if d.Draft {
603603 state = "draft"
604604 }
605 fmt.Fprintf(w, "!%d %s [%s] by %s\n%s -> %s @ %.10s\n", d.Number, d.Title, state, d.Author, d.Source, d.TargetRef, d.HeadSHA)
606 if len(d.Labels) > 0 {
607 fmt.Fprintf(w, "labels: %s\n", strings.Join(d.Labels, ", "))
608 }
609 if len(d.ReviewRequests) > 0 {
610 fmt.Fprintf(w, "reviewers: %s\n", strings.Join(d.ReviewRequests, ", "))
611 }
605 v := c.view(w)
606 v.title(fmt.Sprintf("!%d", d.Number), d.Title, state)
607
608 stackedOn, stacked := "", ""
612609 if d.StackedOn != nil {
613 fmt.Fprintf(w, "stacked on !%d %s\n", d.StackedOn.Number, d.StackedOn.Title)
610 stackedOn = fmt.Sprintf("!%d %s", d.StackedOn.Number, d.StackedOn.Title)
614611 }
615612 for _, k := range d.Stacked {
616 fmt.Fprintf(w, "stacked: !%d %s\n", k.Number, k.Title)
613 if stacked != "" {
614 stacked += ", "
615 }
616 stacked += fmt.Sprintf("!%d %s", k.Number, k.Title)
617617 }
618 merged, closed, superseded := "", "", ""
618619 if d.MergedAt != "" {
619 fmt.Fprintf(w, "merged %s%s\n", d.MergedAt, byWhom(d.MergedBy))
620 merged = c.when(d.MergedAt) + byWhom(d.MergedBy)
620621 }
621622 if d.ClosedAt != "" {
622 fmt.Fprintf(w, "closed %s%s\n", d.ClosedAt, byWhom(d.ClosedBy))
623 closed = c.when(d.ClosedAt) + byWhom(d.ClosedBy)
623624 }
624625 if d.SupersededBy != 0 {
625 fmt.Fprintf(w, "superseded by: !%d\n", d.SupersededBy)
626 }
627 if d.Body != "" {
628 fmt.Fprintf(w, "\n%s\n", d.Body)
629 }
630 for _, cm := range commits {
631 fmt.Fprintf(w, "commit: %.10s %s\n", cm.SHA, cm.Subject)
632 }
633 for _, x := range checks {
634 dur := ""
635 if x.Duration != "" {
636 dur = " in " + x.Duration
637 }
638 fmt.Fprintf(w, "check: %s %s at %s%s\n", x.Context, x.State, x.UpdatedAt, dur)
639 }
640 if d.UnresolvedThreads > 0 {
641 fmt.Fprintf(w, "unresolved threads: %d\n", d.UnresolvedThreads)
626 superseded = fmt.Sprintf("!%d", d.SupersededBy)
642627 }
628 gates := ""
643629 if g := d.Gates; g != nil {
644630 ff := "fast-forward possible"
645631 if !g.FastForward {
646632 ff = "fast-forward not possible"
647633 }
648634 if len(g.Unmet) == 0 {
649 fmt.Fprintf(w, "gates: met; %s\n", ff)
635 gates = "met; " + ff
650636 } else {
651 fmt.Fprintf(w, "gates: %d unmet; %s\n", len(g.Unmet), ff)
652 for _, u := range g.Unmet {
653 fmt.Fprintf(w, "gate: %s\n", u)
637 gates = fmt.Sprintf("%d unmet; %s", len(g.Unmet), ff)
638 }
639 }
640 unresolved := ""
641 if d.UnresolvedThreads > 0 {
642 unresolved = fmt.Sprintf("%d", d.UnresolvedThreads)
643 }
644 v.fields(
645 "author", d.Author+", "+c.when(d.CreatedAt),
646 "source", fmt.Sprintf("%s -> %s", d.Source, d.TargetRef),
647 "head", fmt.Sprintf("%.10s", d.HeadSHA),
648 "milestone", d.Milestone,
649 "labels", strings.Join(d.Labels, ", "),
650 "reviewers", strings.Join(d.ReviewRequests, ", "),
651 "stacked on", stackedOn,
652 "stacked", stacked,
653 "merged", merged,
654 "closed", closed,
655 "superseded by", superseded,
656 "unresolved threads", unresolved,
657 "gates", gates,
658 "url", c.siteURL(repo.Path(), "mrs", strconv.FormatInt(d.Number, 10)),
659 )
660 if g := d.Gates; g != nil && len(g.Unmet) > 0 {
661 var kv []string
662 for _, u := range g.Unmet {
663 kv = append(kv, "unmet", u)
664 }
665 v.fields(kv...)
666 }
667 v.body(d.Body, d.BodyFormat)
668
669 if len(commits) == 1 {
670 v.fields("commit", fmt.Sprintf("%.10s %s", commits[0].SHA, commits[0].Subject))
671 } else if len(commits) > 1 {
672 io.WriteString(w, "\n")
673 tb := c.table(w, "SHA", "SUBJECT")
674 for _, cm := range commits {
675 tb.row(cRef(fmt.Sprintf("%.10s", cm.SHA)), cFlex(cm.Subject))
676 }
677 tb.flush()
678 }
679
680 if len(checks) == 1 {
681 x := checks[0]
682 dur := ""
683 if x.Duration != "" {
684 dur = " in " + x.Duration
685 }
686 v.fields("check", fmt.Sprintf("%s %s at %s%s", x.Context, x.State, c.when(x.UpdatedAt), dur))
687 } else if len(checks) > 1 {
688 io.WriteString(w, "\n")
689 tb := c.table(w, "CHECK", "STATE", "UPDATED")
690 for _, x := range checks {
691 tb.row(cText(x.Context), cState(x.State), cText(c.when(x.UpdatedAt)))
692 }
693 tb.flush()
694 }
695
696 if len(rs) == 1 {
697 v.fields("review", reviewLine(rs[0])+" at "+c.when(rs[0].CreatedAt))
698 } else if len(rs) > 1 {
699 io.WriteString(w, "\n")
700 tb := c.table(w, "REVIEWER", "VERDICT", "WHEN")
701 for _, r := range rs {
702 verdict := r.Verdict
703 if r.Stale {
704 verdict += " (stale)"
705 }
706 if !r.Counts {
707 verdict += " (advisory)"
654708 }
709 tb.row(cText(r.Reviewer), cState(verdict), cText(c.when(r.CreatedAt)))
655710 }
711 tb.flush()
656712 }
657 for _, r := range rs {
658 stale := ""
659 if r.Stale {
660 stale = " (stale)"
713
714 events := false
715 for _, cm := range cs {
716 if cm.Kind != "system" {
717 continue
661718 }
662 advisory := ""
663 if !r.Counts {
664 advisory = " (advisory: no write access)"
719 if !events {
720 io.WriteString(w, "\n")
721 events = true
665722 }
666 fmt.Fprintf(w, "review: %s %s%s%s at %s\n", r.Reviewer, r.Verdict, stale, advisory, r.CreatedAt)
723 v.event(cm.Body, cm.BodyFormat, cm.CreatedAt)
667724 }
668725 for _, cm := range cs {
669 fmt.Fprintf(w, "\n--- %s at %s\n%s\n", cm.Author, cm.CreatedAt, cm.Body)
726 if cm.Kind == "system" {
727 continue
728 }
729 v.comment(cm.Author, cm.CreatedAt, cm.Body, cm.BodyFormat)
670730 }
671731 })
672732}
673733
734// reviewLine renders one review as fields prose: "reviewer verdict
735// (stale) (advisory) at when".
736func reviewLine(r ReviewOut) string {
737 s := r.Reviewer + " " + r.Verdict
738 if r.Stale {
739 s += " (stale)"
740 }
741 if !r.Counts {
742 s += " (advisory: no write access)"
743 }
744 return s
745}
746
674747func runMRDiff(c *Ctx, args []string) int {
675748 repo, mr, code := mrRef(c, args, policy.CanRead)
676749 if code >= 0 {
internal/control/view.go +30 −8
@@ -41,35 +41,57 @@ func (v *view) opts() termtext.Options {
4141// title prints "ref title state", wrapping title+state to the
4242// terminal width. Continuation lines indent under the title, and each
4343// line is painted after wrapping so no SGR sequence crosses a break.
44// title or state may be "": either is skipped rather than leaving a
45// trailing blank field.
4446func (v *view) title(ref, title, state string) {
4547 t := v.c.Term
4648 if t.Cols == 0 {
47 io.WriteString(v.w, ref+" "+t.paint(sgrBold, title)+" "+t.paint(stateColor(state), state)+"\n")
49 switch {
50 case title == "" && state == "":
51 io.WriteString(v.w, ref+"\n")
52 case state == "":
53 io.WriteString(v.w, ref+" "+t.paint(sgrBold, title)+"\n")
54 case title == "":
55 io.WriteString(v.w, ref+" "+t.paint(stateColor(state), state)+"\n")
56 default:
57 io.WriteString(v.w, ref+" "+t.paint(sgrBold, title)+" "+t.paint(stateColor(state), state)+"\n")
58 }
4859 return
4960 }
5061 prefix := ref + " "
5162 indent := strings.Repeat(" ", cells(prefix))
52 lines := termtext.Wrap(title+" "+state, t.Cols-cells(prefix))
63 rest := title
64 if state != "" {
65 if rest != "" {
66 rest += " "
67 }
68 rest += state
69 }
70 if rest == "" {
71 io.WriteString(v.w, ref+"\n")
72 return
73 }
74 lines := termtext.Wrap(rest, t.Cols-cells(prefix))
5375 for i, line := range lines {
5476 p := indent
5577 if i == 0 {
5678 p = prefix
5779 }
58 if i < len(lines)-1 {
80 if i < len(lines)-1 || state == "" {
5981 io.WriteString(v.w, p+t.paint(sgrBold, line)+"\n")
6082 continue
6183 }
6284 // Last line carries the state word, the last word overall
6385 // (states are single words): keep it coloured, not bold.
64 rest, last := line, line
86 head, last := line, line
6587 if idx := strings.LastIndex(line, " "); idx >= 0 {
66 rest, last = line[:idx], line[idx+1:]
88 head, last = line[:idx], line[idx+1:]
6789 } else {
68 rest = ""
90 head = ""
6991 }
7092 io.WriteString(v.w, p)
71 if rest != "" {
72 io.WriteString(v.w, t.paint(sgrBold, rest)+" ")
93 if head != "" {
94 io.WriteString(v.w, t.paint(sgrBold, head)+" ")
7395 }
7496 io.WriteString(v.w, t.paint(stateColor(state), last)+"\n")
7597 }