Commit 77fb535e35

77fb535e3597945c4521a5cd4a4385e68ec4f8a6

parent: 9c0a74553a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-02 04:16 UTC

control: repo settings show, deps status and commit as screens

The settings screen also shows required approvals, resolved threads,
code owners and website, which the piped output does not.

Ref #319

Layout: unified · split

internal/control/deps.go +29 −2
@@ -4,6 +4,7 @@ import (
44 "errors"
55 "fmt"
66 "io"
7 "strconv"
78
89 "gitbay.org/gitbay/internal/policy"
910 "gitbay.org/gitbay/internal/protocol"
@@ -92,9 +93,14 @@ func runDepsStatus(c *Ctx, args []string) int {
9293 }
9394 check, err := c.Store.DepCheckFor(repo.ID)
9495 if errors.Is(err, store.ErrNotFound) {
95 return c.emit(map[string]any{"enabled": false}, func(w io.Writer) {
96 return c.emitView(map[string]any{"enabled": false}, func(w io.Writer) {
9697 v := c.view(w)
9798 v.fields("checks", fmt.Sprintf("off (repo deps enable %s)", repo.Path()))
99 }, func() screen {
100 return screen{
101 fields: []field{{"Checks", []cell{cText("off")}}},
102 actions: []action{{"Deps", []string{"repo", "deps", "enable", repo.Path()}}},
103 }
98104 })
99105 }
100106 if err != nil {
@@ -109,7 +115,7 @@ func runDepsStatus(c *Ctx, args []string) int {
109115 for _, r := range reports {
110116 out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest})
111117 }
112 return c.emit(out, func(w io.Writer) {
118 return c.emitView(out, func(w io.Writer) {
113119 tracked := ""
114120 if check.IssueNumber != 0 {
115121 tracked = fmt.Sprintf("#%d", check.IssueNumber)
@@ -129,5 +135,26 @@ func runDepsStatus(c *Ctx, args []string) int {
129135 }
130136 tb.flush()
131137 }
138 }, func() screen {
139 state := []cell{cGlyph("ok"), cText("on")}
140 if check.LastError != "" {
141 state = []cell{cGlyph("failed"), cText("on")}
142 }
143 s := screen{fields: []field{
144 {"Checks", state},
145 {"Last check", []cell{cAge(check.LastCheck)}},
146 {"Error", []cell{cMark(check.LastError, sgrRed)}},
147 }}
148 if check.IssueNumber != 0 {
149 n := strconv.FormatInt(check.IssueNumber, 10)
150 s.fields = append(s.fields, field{"Tracked in", []cell{cLink("#"+n, c.siteURL(repo.Path(), "issues", n))}})
151 }
152 behind := section{title: "Behind", n: len(out.Behind)}
153 for _, b := range out.Behind {
154 behind.rows = append(behind.rows, rowOf(cRef(b.Name), cText(b.Ecosystem), cMeta(b.Current+" → "+b.Latest)))
155 }
156 s.sections = []section{behind}
157 s.actions = []action{{"Deps", []string{"repo", "deps", "disable", repo.Path()}}}
158 return s
132159 })
133160}
internal/control/repo.go +44 −1
@@ -811,7 +811,7 @@ func runSettingsShow(c *Ctx, args []string) int {
811811 if code >= 0 {
812812 return code
813813 }
814 return c.emit(repo.Settings, func(w io.Writer) {
814 return c.emitView(repo.Settings, func(w io.Writer) {
815815 v := c.view(w)
816816 v.title(repo.Path(), "settings", "")
817817 v.fields(
@@ -824,6 +824,49 @@ func runSettingsShow(c *Ctx, args []string) int {
824824 "git daemon", strconv.FormatBool(repo.Settings.GitDaemon),
825825 "archived", strconv.FormatBool(repo.Settings.Archived),
826826 )
827 }, func() screen {
828 set := repo.Settings
829 onOff := func(b bool) cell {
830 if b {
831 return cText("on")
832 }
833 return cMeta("off")
834 }
835 approvals := cMeta("off")
836 if set.RequireApprovals > 0 {
837 approvals = cText(strconv.Itoa(set.RequireApprovals))
838 }
839 list := func(xs []string) cell {
840 if len(xs) == 0 {
841 return cMeta("none")
842 }
843 return cText(strings.Join(xs, ", "))
844 }
845 mr := "on"
846 if set.RequireMR {
847 mr = "off"
848 }
849 s := screen{fields: []field{
850 {"Repo", []cell{cLink(repo.Path(), c.siteURL(repo.Path())), cMeta("settings")}},
851 {"Protected", []cell{list(set.ProtectedBranches)}},
852 {"Protected tags", []cell{list(set.ProtectedTags)}},
853 {"Require MR", []cell{onOff(set.RequireMR)}},
854 {"Require checks", []cell{onOff(set.RequireChecks)}},
855 {"Contexts", []cell{list(set.RequiredContexts)}},
856 {"Approvals", []cell{approvals}},
857 {"Resolved threads", []cell{onOff(set.RequireResolved)}},
858 {"Code owners", []cell{onOff(set.RequireCodeowners)}},
859 {"Signed commits", []cell{onOff(set.RequireSignedCommits)}},
860 {"Git daemon", []cell{onOff(set.GitDaemon)}},
861 {"Archived", []cell{onOff(set.Archived)}},
862 }, actions: []action{
863 {"Settings", []string{"repo", "settings", "protect", repo.Path(), "<branch>"}},
864 {"Settings", []string{"repo", "settings", "require-mr", repo.Path(), mr}},
865 }}
866 if set.Website != "" {
867 s.fields = append(s.fields, field{"Website", []cell{cText(set.Website)}})
868 }
869 return s
827870 })
828871}
829872
internal/control/sig.go +20 −19
@@ -354,27 +354,28 @@ func runRepoCommit(c *Ctx, args []string) int {
354354 fmt.Fprintf(w, "\n%s", d.Diff)
355355 return
356356 }
357 v := c.view(w)
358 v.title(d.SHA[:10], d.Subject, d.Signature.State)
359 signer := d.Signature.Signer
360 if signer != "" && d.Signature.Fingerprint != "" {
361 signer += ", key " + d.Signature.Fingerprint
357 short, url := d.SHA[:min(10, len(d.SHA))], c.siteURL(repo.OwnerName, repo.Name, "commit", d.SHA[:min(12, len(d.SHA))])
358 s := screen{body: body, format: "text", fields: []field{
359 {"Commit", []cell{cLink(short, url), cText(d.Subject)}},
360 {"Author", []cell{cText(fmt.Sprintf("%s <%s>", d.AuthorName, d.AuthorEmail)), cAge(d.Date)}},
361 }}
362 if d.CommitterEmail != "" && d.CommitterEmail != d.AuthorEmail {
363 s.fields = append(s.fields, field{"Committer", []cell{cText(d.CommitterEmail)}})
362364 }
363 v.fields(
364 "author", fmt.Sprintf("%s <%s>, %s", d.AuthorName, d.AuthorEmail, c.when(d.Date)),
365 "committer", d.CommitterEmail,
366 "signer", signer,
367 "url", c.siteURL(repo.OwnerName, repo.Name, "commit", d.SHA[:12]),
368 )
369 v.text(body)
370 if len(d.Checks) > 0 {
371 v.section("checks")
372 tb := c.table(w, "CHECK", "STATE")
373 for _, ch := range d.Checks {
374 tb.row(cText(ch.Context), cState(ch.State))
375 }
376 tb.flush()
365 s.fields = append(s.fields, field{"Signature", []cell{cGlyph(d.Signature.State), cState(d.Signature.State), cMeta(d.Signature.Signer, d.Signature.Fingerprint)}})
366 if !c.Term.Links {
367 s.fields = append(s.fields, field{"URL", []cell{cText(url)}})
368 }
369 checks := section{title: "Checks", n: len(d.Checks)}
370 for _, ch := range d.Checks {
371 checks.rows = append(checks.rows, rowOf(cGlyph(ch.State), cFlex(ch.Context)))
372 }
373 s.sections = []section{checks}
374 s.actions = []action{
375 {"Read", []string{"repo", "log", repo.Path(), "--ref", short}},
376 {"Read", []string{"repo", "tree", repo.Path(), "--ref", short}},
377377 }
378 c.render(w, s)
378379 fmt.Fprintf(w, "\n%s", c.Term.diff(d.Diff))
379380 })
380381}
internal/control/stage3repo_test.go +40
@@ -244,3 +244,43 @@ func TestRepoBrowseScreens(t *testing.T) {
244244 }
245245 checkLegend(t, out)
246246}
247
248func TestRepoShowsPlainPinned(t *testing.T) {
249 st, repo, u, root := browseFixture(t)
250 p := repo.Path()
251 pinPlain(t, "repo-settings-show", dispatchIn(t, st, u, root, "", "repo", "settings", "show", p))
252 pinPlain(t, "repo-deps-status-off", dispatchIn(t, st, u, root, "", "repo", "deps", "status", p))
253 pinPlain(t, "repo-commit", dispatchIn(t, st, u, root, "", "repo", "commit", p, "aee4475ad9"))
254 dispatchIn(t, st, u, root, "", "repo", "deps", "enable", p)
255 pinPlain(t, "repo-deps-status", dispatchIn(t, st, u, root, "", "repo", "deps", "status", p))
256}
257
258func TestRepoShowScreens(t *testing.T) {
259 st, repo, u, root := browseFixture(t)
260 p := repo.Path()
261 for _, tc := range []struct {
262 argv []string
263 want []string
264 }{
265 {[]string{"repo", "settings", "show", p}, []string{"Repo:", "alice/app", "Require MR:", "off", "Approvals:"}},
266 {[]string{"repo", "deps", "status", p}, []string{"Checks: off\n", "gitbay repo deps enable alice/app"}},
267 {[]string{"repo", "commit", p, "aee4475ad9"}, []string{"Commit:", "aee4475ad9 docs: guide", "Author:", "t <t@example.test>", "gitbay repo log alice/app --ref aee4475ad9", "+# guide"}},
268 } {
269 out := atTerminalIn(t, st, u, root, tc.argv...)
270 for _, w := range tc.want {
271 if !strings.Contains(out, w) {
272 t.Errorf("%v: missing %q in:\n%s", tc.argv, w, out)
273 }
274 }
275 checkLegend(t, out)
276 }
277 dispatchIn(t, st, u, root, "", "repo", "deps", "enable", p)
278 out := atTerminalIn(t, st, u, root, "repo", "deps", "status", p)
279 if !strings.Contains(out, "Checks:") || !strings.Contains(out, "gitbay repo deps disable alice/app") {
280 t.Errorf("deps on:\n%s", out)
281 }
282 checkLegend(t, out)
283 if i, j := strings.Index(out, "gitbay repo deps"), strings.Index(out, "Checks:"); i < j {
284 t.Errorf("legend before the header:\n%s", out)
285 }
286}
internal/control/testdata/plain/repo-commit.txt added +16
@@ -0,0 +1,16 @@
1commit aee4475ad9033773a17e148aafa3105212ac9c10
2Author: t <t@example.test>
3Date: <time>
4
5 docs: guide
6
7 docs/guide.md | 1 +
8 1 file changed, 1 insertion(+)
9
10diff --git a/docs/guide.md b/docs/guide.md
11new file mode 100644
12index 0000000..cd51204
13--- /dev/null
14+++ b/docs/guide.md
15@@ -0,0 +1 @@
16+# guide
internal/control/testdata/plain/repo-deps-status-off.txt added +1
@@ -0,0 +1 @@
1 checks off (repo deps enable alice/app)
internal/control/testdata/plain/repo-deps-status.txt added +1
@@ -0,0 +1 @@
1 checks on
internal/control/testdata/plain/repo-settings-show.txt added +7
@@ -0,0 +1,7 @@
1alice/app settings
2
3 require mr false
4 require checks false
5 require signed commits false
6 git daemon false
7 archived false