Commit 98ba76cb39

98ba76cb39757b142682db3c93917a3cce420c90

parent: acbc0f60ee

Verified · cmc

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

control: every show on the view layout

Ref #254
e2e/adminusers_test.go +1 −1
@@ -165,7 +165,7 @@ func TestAdminUserListAndShow(t *testing.T) {
165165 t.Fatalf("unknown user: exit %d", code)
166166 }
167167 // Plain output carries the same facts.
168 if out, _, _ := inst.ssh(t, adminKey, "", "admin", "user", "show", "alice"); !strings.Contains(out, "alice\tactive") ||
168 if out, _, _ := inst.ssh(t, adminKey, "", "admin", "user", "show", "alice"); !strings.Contains(out, "alice") || !strings.Contains(out, "active") ||
169169 !strings.Contains(out, "acme\tadmin") || !strings.Contains(out, "verified by admin") {
170170 t.Fatalf("plain show:\n%s", out)
171171 }
e2e/maint_test.go +2 −1
@@ -4,6 +4,7 @@ import (
44 "fmt"
55 "os"
66 "path/filepath"
7 "regexp"
78 "strings"
89 "testing"
910)
@@ -39,7 +40,7 @@ func TestAdminGCAndStats(t *testing.T) {
3940
4041 // Stats: counts and per-repo disk usage.
4142 out := inst.admin(t, "admin", "stats")
42 if !strings.Contains(out, "repos 2") || !strings.Contains(out, "issues 1 (1 open)") ||
43 if !regexp.MustCompile(`repos\s+2`).MatchString(out) || !strings.Contains(out, "1 (1 open)") ||
4344 !strings.Contains(out, "alice/app") || !strings.Contains(out, "alice/other") {
4445 t.Fatalf("stats output: %s", out)
4546 }
e2e/readonly_test.go +2 −7
@@ -173,13 +173,8 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) {
173173 // rawOutput prints content verbatim (a file, a log, a diff) and is
174174 // not fitted to the terminal.
175175 rawOutput := map[string]bool{
176 "repo download": true,
177 "account export": true,
178 "admin user show": true, // until the view layout (Part 3)
179 "admin runners": true, // until the view layout (Part 3): the queue summary line
180 "admin stats": true, // until the view layout (Part 3)
181 "repo deps status": true, // until the view layout (Part 3)
182 "release show": true, // until the view layout (Part 3)
176 "repo download": true,
177 "account export": true,
183178 }
184179 // binaryOutput's bytes are not text: a stray 0x1b is coincidence, not
185180 // an SGR sequence escaping into plain output.
e2e/reap_test.go +3 −2
@@ -7,6 +7,7 @@ import (
77 "fmt"
88 "os"
99 "path/filepath"
10 "regexp"
1011 "strings"
1112 "testing"
1213 "time"
@@ -111,7 +112,7 @@ func TestAdminRunners(t *testing.T) {
111112 t.Fatal("runner next failed")
112113 }
113114 out, _, _ := inst.ssh(t, rootKey, "", "admin", "runners")
114 if !strings.HasPrefix(out, "queue: 0 pending; last 24h: 0 claimed") || !strings.Contains(out, "\nci\t") ||
115 if !regexp.MustCompile(`pending\s+0`).MatchString(out) || !regexp.MustCompile(`claimed 24h\s+0`).MatchString(out) || !strings.Contains(out, "\nci\t") ||
115116 !strings.Contains(out, "\talice/app\tidle") {
116117 t.Fatalf("idle runner row:\n%s", out)
117118 }
@@ -142,7 +143,7 @@ func TestAdminRunners(t *testing.T) {
142143 t.Fatal("runner done failed")
143144 }
144145 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "runners"); !strings.Contains(out, "\tany\tidle") ||
145 !strings.HasPrefix(out, "queue: 0 pending; last 24h: 1 claimed, wait avg ") {
146 !regexp.MustCompile(`pending\s+0`).MatchString(out) || !regexp.MustCompile(`claimed 24h\s+1`).MatchString(out) {
146147 t.Fatalf("runner still holds a build after done:\n%s", out)
147148 }
148149 // Host-local, the same read.
e2e/release_test.go +2 −2
@@ -49,14 +49,14 @@ func TestReleases(t *testing.T) {
4949 t.Fatalf("release edit: %s", errOut)
5050 }
5151 out, _, _ := inst.ssh(t, aliceKey, "", "release", "show", "alice/app", "v1.0")
52 if !strings.Contains(out, "the **rebuilt** notes") || !strings.Contains(out, "First light") {
52 if !strings.Contains(out, "the rebuilt notes") || !strings.Contains(out, "First light") {
5353 t.Fatalf("edit lost a field:\n%s", out)
5454 }
5555 if _, _, code := inst.ssh(t, aliceKey, "", "release", "edit", "alice/app", "v1.0", "--title", "'Second light'"); code != 0 {
5656 t.Fatal("title edit failed")
5757 }
5858 out, _, _ = inst.ssh(t, aliceKey, "", "release", "show", "alice/app", "v1.0")
59 if !strings.Contains(out, "Second light") || !strings.Contains(out, "the **rebuilt** notes") {
59 if !strings.Contains(out, "Second light") || !strings.Contains(out, "the rebuilt notes") {
6060 t.Fatalf("title edit lost notes:\n%s", out)
6161 }
6262 if _, _, code := inst.ssh(t, aliceKey, "", "release", "edit", "alice/app", "v9.9", "--title", "x"); code != 3 {
e2e/reposhowstate_test.go +1 −1
@@ -54,7 +54,7 @@ func TestRepoShowCarriesViewerState(t *testing.T) {
5454 }
5555
5656 // Plain output carries the same.
57 if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app"); !strings.Contains(out, "watch: watching") ||
57 if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app"); !strings.Contains(out, "watching") ||
5858 !strings.Contains(out, "bookmarked") {
5959 t.Errorf("plain output lacks the state:\n%s", out)
6060 }
internal/control/admin.go +64 −44
@@ -249,57 +249,71 @@ func runAdminUserShow(c *Ctx, args []string) int {
249249 }
250250
251251 return c.emit(d, func(w io.Writer) {
252 fmt.Fprintf(w, "%s\t%s", d.Username, d.State)
252 admin := ""
253253 if d.Admin {
254 fmt.Fprint(w, "\tadmin")
254 admin = "yes"
255255 }
256 fmt.Fprintf(w, "\ncreated\t%s\n", d.CreatedAt)
257 if d.LastSeen != "" {
258 fmt.Fprintf(w, "last seen\t%s\n", d.LastSeen)
259 }
260 fmt.Fprintf(w, "repos\t%d\nweb sessions\t%d\n", d.Repos, d.WebSessions)
261 fmt.Fprintln(w, "keys:")
262 tk := c.table(w, "FINGERPRINT", "ALGO", "SCOPE", "LAST USED")
263 for _, k := range d.Keys {
264 tk.row(cRef(" "+k.Fingerprint), cText(k.Algo), cState(k.Scope), cAge(k.LastUsedAt))
265 }
266 tk.flush()
267 fmt.Fprintln(w, "emails:")
268 te := c.table(w, "ADDRESS", "STATE")
269 for _, e := range d.Emails {
270 state := "unverified"
271 if e.Verified {
272 state = "verified by " + e.VerifiedBy
256 v := c.view(w)
257 v.title(d.Username, "", d.State)
258 v.fields(
259 "admin", admin,
260 "created", c.when(d.CreatedAt),
261 "last seen", c.when(d.LastSeen),
262 "repos", fmt.Sprintf("%d", d.Repos),
263 "web sessions", fmt.Sprintf("%d", d.WebSessions),
264 )
265 if len(d.Keys) > 0 {
266 io.WriteString(w, "\n")
267 tk := c.table(w, "FINGERPRINT", "ALGO", "SCOPE", "LAST USED")
268 for _, k := range d.Keys {
269 tk.row(cFlex(k.Fingerprint), cText(k.Algo), cState(k.Scope), cAge(k.LastUsedAt))
273270 }
274 cells := []cell{cRef(" " + e.Address), cState(state)}
275 if e.Primary {
276 cells = append(cells, cText("primary"))
271 tk.flush()
272 }
273 if len(d.Emails) > 0 {
274 io.WriteString(w, "\n")
275 te := c.table(w, "ADDRESS", "STATE")
276 for _, e := range d.Emails {
277 state := "unverified"
278 if e.Verified {
279 state = "verified by " + e.VerifiedBy
280 }
281 cells := []cell{cRef(e.Address), cState(state)}
282 if e.Primary {
283 cells = append(cells, cText("primary"))
284 }
285 te.row(cells...)
277286 }
278 te.row(cells...)
287 te.flush()
279288 }
280 te.flush()
281 fmt.Fprintln(w, "pgp keys:")
282 tp := c.table(w, "FINGERPRINT")
283 for _, k := range d.PGPKeys {
284 tp.row(cRef(" " + k.Fingerprint))
289 if len(d.PGPKeys) > 0 {
290 io.WriteString(w, "\n")
291 tp := c.table(w, "FINGERPRINT")
292 for _, k := range d.PGPKeys {
293 tp.row(cFlex(k.Fingerprint))
294 }
295 tp.flush()
285296 }
286 tp.flush()
287 fmt.Fprintln(w, "orgs:")
288 to := c.table(w, "ORG", "ROLE")
289 for _, o := range d.Orgs {
290 to.row(cRef(" "+o.Org), cState(o.Role))
297 if len(d.Orgs) > 0 {
298 io.WriteString(w, "\n")
299 to := c.table(w, "ORG", "ROLE")
300 for _, o := range d.Orgs {
301 to.row(cRef(o.Org), cState(o.Role))
302 }
303 to.flush()
291304 }
292 to.flush()
293 fmt.Fprintln(w, "api tokens:")
294 tt := c.table(w, "NAME", "SCOPE", "LAST USED")
295 for _, t := range d.APITokens {
296 used := ""
297 if t.LastUsedAt != nil {
298 used = t.LastUsedAt.UTC().Format(time.RFC3339Nano)
305 if len(d.APITokens) > 0 {
306 io.WriteString(w, "\n")
307 tt := c.table(w, "NAME", "SCOPE", "LAST USED")
308 for _, t := range d.APITokens {
309 used := ""
310 if t.LastUsedAt != nil {
311 used = t.LastUsedAt.UTC().Format(time.RFC3339Nano)
312 }
313 tt.row(cRef(t.Name), cState(t.Scope), cAge(used))
299314 }
300 tt.row(cRef(" "+t.Name), cState(t.Scope), cAge(used))
315 tt.flush()
301316 }
302 tt.flush()
303317 })
304318}
305319
@@ -529,8 +543,14 @@ func runAdminRunners(c *Ctx, args []string) int {
529543 }
530544 d := map[string]any{"queue": queue, "runners": runners}
531545 return c.emit(d, func(w io.Writer) {
532 fmt.Fprintf(w, "queue: %d pending; last 24h: %d claimed, wait avg %ds max %ds, %d reaped\n",
533 queue.Pending, queue.Claimed24h, queue.ClaimWaitAvgS, queue.ClaimWaitMaxS, queue.Reaped24h)
546 v := c.view(w)
547 v.fields(
548 "pending", fmt.Sprintf("%d", queue.Pending),
549 "claimed 24h", fmt.Sprintf("%d", queue.Claimed24h),
550 "wait avg", fmt.Sprintf("%ds", queue.ClaimWaitAvgS),
551 "wait max", fmt.Sprintf("%ds", queue.ClaimWaitMaxS),
552 "reaped 24h", fmt.Sprintf("%d", queue.Reaped24h),
553 )
534554 tb := c.table(w, "USER", "FINGERPRINT", "LAST SEEN", "SCOPE", "HELD")
535555 for _, r := range runners {
536556 scope := r.Scope
internal/control/adminhost.go +18 −6
@@ -302,12 +302,24 @@ func runAdminStats(c *Ctx, args []string) int {
302302 d.DBBytes = fi.Size()
303303 }
304304 return c.emit(d, func(w io.Writer) {
305 fmt.Fprintf(w, "users %d · orgs %d · repos %d · issues %d (%d open) · MRs %d (%d open)\n",
306 counts.Users, counts.Orgs, counts.Repos,
307 counts.Issues, counts.OpenIssues, counts.MRs, counts.OpenMRs)
308 fmt.Fprintf(w, "database %s · repositories %s · lfs %s\n\n", humanBytes(d.DBBytes), humanBytes(d.RepoBytes), humanBytes(d.LFSBytes))
309 for _, r := range d.Repos {
310 fmt.Fprintf(w, "%s\t%s\n", r.Path, humanBytes(r.Bytes))
305 v := c.view(w)
306 v.fields(
307 "users", fmt.Sprintf("%d", counts.Users),
308 "orgs", fmt.Sprintf("%d", counts.Orgs),
309 "repos", fmt.Sprintf("%d", counts.Repos),
310 "issues", fmt.Sprintf("%d (%d open)", counts.Issues, counts.OpenIssues),
311 "MRs", fmt.Sprintf("%d (%d open)", counts.MRs, counts.OpenMRs),
312 "database", humanBytes(d.DBBytes),
313 "repositories", humanBytes(d.RepoBytes),
314 "lfs", humanBytes(d.LFSBytes),
315 )
316 if len(d.Repos) > 0 {
317 io.WriteString(w, "\n")
318 tb := c.table(w, "PATH", "BYTES")
319 for _, r := range d.Repos {
320 tb.row(cRef(r.Path), cText(humanBytes(r.Bytes)))
321 }
322 tb.flush()
311323 }
312324 })
313325}
internal/control/build.go +10 −6
@@ -183,17 +183,21 @@ func buildSubjects(c *Ctx, repo store.Repo, ds []BuildOut) map[string]string {
183183}
184184
185185func runBuildShow(c *Ctx, args []string) int {
186 _, b, code := buildRef(c, args)
186 repo, b, code := buildRef(c, args)
187187 if code >= 0 {
188188 return code
189189 }
190190 d := buildToOut(b)
191191 return c.emit(d, func(w io.Writer) {
192 fmt.Fprintf(w, "build %d\t%s\t%s\n%.10s on %s\nqueued %s", d.Number, d.Job, d.Status, d.SHA, d.Ref, d.CreatedAt)
193 if d.FinishedAt != "" {
194 fmt.Fprintf(w, ", finished %s", d.FinishedAt)
195 }
196 fmt.Fprintln(w)
192 v := c.view(w)
193 v.title(fmt.Sprintf("#%d", d.Number), d.Job, d.Status)
194 v.fields(
195 "sha", fmt.Sprintf("%.10s", d.SHA),
196 "ref", d.Ref,
197 "queued", c.when(d.CreatedAt),
198 "finished", c.when(d.FinishedAt),
199 "url", c.siteURL(repo.Path(), "builds", strconv.FormatInt(d.Number, 10)),
200 )
197201 })
198202}
199203
internal/control/deps.go +18 −8
@@ -87,7 +87,8 @@ func runDepsStatus(c *Ctx, args []string) int {
8787 check, err := c.Store.DepCheckFor(repo.ID)
8888 if errors.Is(err, store.ErrNotFound) {
8989 return c.emit(map[string]any{"enabled": false}, func(w io.Writer) {
90 fmt.Fprintf(w, "dependency checks are off for %s (repo deps enable %s)\n", repo.Path(), repo.Path())
90 v := c.view(w)
91 v.fields("checks", fmt.Sprintf("off (repo deps enable %s)", repo.Path()))
9192 })
9293 }
9394 if err != nil {
@@ -103,15 +104,24 @@ func runDepsStatus(c *Ctx, args []string) int {
103104 out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest})
104105 }
105106 return c.emit(out, func(w io.Writer) {
106 fmt.Fprintf(w, "checks on, last %s\n", orDash(check.LastCheck))
107 if check.LastError != "" {
108 fmt.Fprintf(w, "last error: %s\n", check.LastError)
109 }
107 tracked := ""
110108 if check.IssueNumber != 0 {
111 fmt.Fprintf(w, "tracked in #%d\n", check.IssueNumber)
109 tracked = fmt.Sprintf("#%d", check.IssueNumber)
112110 }
113 for _, b := range out.Behind {
114 fmt.Fprintf(w, "%s\t%s\t%s\t-> %s\n", b.Ecosystem, b.Name, b.Current, b.Latest)
111 v := c.view(w)
112 v.fields(
113 "checks", "on",
114 "last check", c.when(check.LastCheck),
115 "last error", check.LastError,
116 "tracked in", tracked,
117 )
118 if len(out.Behind) > 0 {
119 io.WriteString(w, "\n")
120 tb := c.table(w, "ECOSYSTEM", "NAME", "CURRENT", "LATEST")
121 for _, b := range out.Behind {
122 tb.row(cText(b.Ecosystem), cRef(b.Name), cText(b.Current), cText(b.Latest))
123 }
124 tb.flush()
115125 }
116126 })
117127}
internal/control/notifications.go +6 −1
@@ -198,7 +198,12 @@ func emitNotificationSettings(c *Ctx) int {
198198 }
199199 return "off"
200200 }
201 fmt.Fprintf(w, "mail: %s\nwatch: %s\npush: %s\n", onOff(mail), onOff(watch), onOff(push))
201 v := c.view(w)
202 v.fields(
203 "mail", onOff(mail),
204 "watch", onOff(watch),
205 "push", onOff(push),
206 )
202207 })
203208}
204209
internal/control/org.go +9 −5
@@ -123,12 +123,16 @@ func runOrgShow(c *Ctx, args []string) int {
123123 Members []memberOut `json:"members"`
124124 }{org.Name, ms}
125125 return c.emit(d, func(w io.Writer) {
126 fmt.Fprintf(w, "%s\n", d.Org)
127 tb := c.table(w, "USER", "ROLE")
128 for _, m := range ms {
129 tb.row(cRef(" "+m.User), cState(m.Role))
126 v := c.view(w)
127 v.title(d.Org, "", "")
128 if len(ms) > 0 {
129 io.WriteString(w, "\n")
130 tb := c.table(w, "USER", "ROLE")
131 for _, m := range ms {
132 tb.row(cRef(m.User), cState(m.Role))
133 }
134 tb.flush()
130135 }
131 tb.flush()
132136 })
133137}
134138
internal/control/profile.go +39 −20
@@ -240,31 +240,50 @@ func ActivityWindow() string {
240240
241241func emitProfile(c *Ctx, d ProfileOut) int {
242242 return c.emit(d, func(w io.Writer) {
243 fmt.Fprintf(w, "%s (%s)\n", d.Name, d.Kind)
244 if d.Description != "" {
245 fmt.Fprintf(w, "%s\n", d.Description)
246 }
247 if d.Website != "" {
248 fmt.Fprintf(w, "%s\n", d.Website)
249 }
250 for _, l := range d.Links {
251 fmt.Fprintf(w, "link\t%s\t%s\n", l.Label, l.URL)
252 }
253 for _, m := range d.Orgs {
254 fmt.Fprintf(w, "org\t%s\t%s\n", m.Name, m.Role)
243 activity := ""
244 if d.ActivityTotal > 0 {
245 activity = fmt.Sprintf("%d in the last year", d.ActivityTotal)
255246 }
256 for _, m := range d.Members {
257 fmt.Fprintf(w, "member\t%s\t%s\n", m.Name, m.Role)
247 v := c.view(w)
248 v.title(d.Name, d.Description, d.Kind)
249 v.fields(
250 "website", d.Website,
251 "url", c.siteURL(d.Name),
252 "activity", activity,
253 )
254 if len(d.Links) > 0 {
255 io.WriteString(w, "\n")
256 tb := c.table(w, "LINK", "URL")
257 for _, l := range d.Links {
258 tb.row(cText(l.Label), cFlex(l.URL))
259 }
260 tb.flush()
258261 }
259 for _, r := range d.Repos {
260 fmt.Fprintf(w, "repo\t%s\t%s\t%s\n", r.Path, r.Visibility, r.Description)
262 if len(d.Orgs) > 0 {
263 io.WriteString(w, "\n")
264 tb := c.table(w, "ORG", "ROLE")
265 for _, m := range d.Orgs {
266 tb.row(cRef(m.Name), cState(m.Role))
267 }
268 tb.flush()
261269 }
262 if d.ActivityTotal > 0 {
263 fmt.Fprintf(w, "activity\t%d in the last year\n", d.ActivityTotal)
270 if len(d.Members) > 0 {
271 io.WriteString(w, "\n")
272 tb := c.table(w, "MEMBER", "ROLE")
273 for _, m := range d.Members {
274 tb.row(cRef(m.Name), cState(m.Role))
275 }
276 tb.flush()
264277 }
265 if d.About != "" {
266 fmt.Fprintf(w, "\n%s\n", d.About)
278 if len(d.Repos) > 0 {
279 io.WriteString(w, "\n")
280 tb := c.table(w, "REPO", "VISIBILITY", "DESCRIPTION")
281 for _, r := range d.Repos {
282 tb.row(cRef(r.Path), cState(r.Visibility), cFlex(r.Description))
283 }
284 tb.flush()
267285 }
286 v.body(d.About, d.AboutFormat)
268287 })
269288}
270289
internal/control/release.go +16 −5
@@ -265,12 +265,23 @@ func runReleaseShow(c *Ctx, args []string) int {
265265 }
266266 d := releaseToOut(rel, true)
267267 return c.emit(d, func(w io.Writer) {
268 fmt.Fprintf(w, "%s\t%s\tby %s on %s\n", d.Tag, d.Title, d.Author, d.CreatedAt)
269 if d.Notes != "" {
270 fmt.Fprintf(w, "\n%s\n", d.Notes)
268 title := d.Title
269 if title == d.Tag {
270 title = ""
271271 }
272 for _, a := range d.Assets {
273 fmt.Fprintf(w, "%s\t%d\t%s\n", a.Name, a.Size, a.SHA256)
272 v := c.view(w)
273 v.title(d.Tag, title, "")
274 v.fields(
275 "author", d.Author+", "+c.when(d.CreatedAt),
276 )
277 v.body(d.Notes, d.NotesFormat)
278 if len(d.Assets) > 0 {
279 io.WriteString(w, "\n")
280 tb := c.table(w, "NAME", "SIZE", "SHA256")
281 for _, a := range d.Assets {
282 tb.row(cRef(a.Name), cNum(a.Size), cText(a.SHA256[:min(10, len(a.SHA256))]))
283 }
284 tb.flush()
274285 }
275286 })
276287}
internal/control/repo.go +43 −37
@@ -8,6 +8,7 @@ import (
88 "path"
99 "path/filepath"
1010 "slices"
11 "strconv"
1112 "strings"
1213
1314 "gitbay.org/gitbay/internal/gitutil"
@@ -361,44 +362,41 @@ func runRepoShow(c *Ctx, args []string) int {
361362 }
362363 }
363364 return c.emit(d, func(w io.Writer) {
364 line := fmt.Sprintf("%s\t%s\tdefault: %s", d.Path, d.Visibility, d.DefaultBranch)
365 if d.Archived {
366 line += "\t[archived]"
367 }
368 fmt.Fprintln(w, line)
369 if d.Description != "" {
370 fmt.Fprintf(w, "%s\n", d.Description)
371 }
372 if d.Website != "" {
373 fmt.Fprintf(w, "website: %s\n", d.Website)
374 }
375 if len(d.Topics) > 0 {
376 fmt.Fprintf(w, "topics: %s\n", strings.Join(d.Topics, ", "))
377 }
378 if len(d.ProtectedBranches) > 0 {
379 fmt.Fprintf(w, "protected: %s\n", strings.Join(d.ProtectedBranches, ", "))
380 }
381 if len(d.Domains) > 0 {
382 fmt.Fprintf(w, "pages domains: %s\n", strings.Join(d.Domains, ", "))
383 }
384 if d.ForkOf != "" {
385 fmt.Fprintf(w, "fork of: %s\n", d.ForkOf)
386 }
387 if d.Watch != "" {
388 fmt.Fprintf(w, "watch: %s\n", d.Watch)
389 }
365 bookmarked, archived := "", ""
390366 if d.Bookmarked {
391 fmt.Fprintln(w, "bookmarked")
367 bookmarked = "yes"
392368 }
393 for _, m := range d.Mirrors {
394 status := "ok"
395 if m.Pending {
396 status = "pending"
397 }
398 if m.LastError != "" {
399 status = "error: " + m.LastError
369 if d.Archived {
370 archived = "yes"
371 }
372 v := c.view(w)
373 v.title(d.Path, d.Description, d.Visibility)
374 v.fields(
375 "default branch", d.DefaultBranch,
376 "website", d.Website,
377 "topics", strings.Join(d.Topics, ", "),
378 "protected", strings.Join(d.ProtectedBranches, ", "),
379 "pages domains", strings.Join(d.Domains, ", "),
380 "fork of", d.ForkOf,
381 "watch", d.Watch,
382 "bookmarked", bookmarked,
383 "archived", archived,
384 "url", c.siteURL(d.Path),
385 )
386 if len(d.Mirrors) > 0 {
387 io.WriteString(w, "\n")
388 tb := c.table(w, "DIRECTION", "URL", "LAST SYNC", "STATUS")
389 for _, m := range d.Mirrors {
390 status := "ok"
391 if m.Pending {
392 status = "pending"
393 }
394 if m.LastError != "" {
395 status = "error: " + m.LastError
396 }
397 tb.row(cText(m.Direction), cFlex(m.URL), cText(orDash(m.LastSync)), cState(status))
400398 }
401 fmt.Fprintf(w, "mirror: %s %s\tlast %s\t%s\n", m.Direction, m.URL, orDash(m.LastSync), status)
399 tb.flush()
402400 }
403401 })
404402}
@@ -630,8 +628,16 @@ func runSettingsShow(c *Ctx, args []string) int {
630628 return code
631629 }
632630 return c.emit(repo.Settings, func(w io.Writer) {
633 fmt.Fprintf(w, "protected_branches: %s\nprotected_tags: %s\nrequire_mr: %v\nrequire_signed_commits: %v\ngit_daemon: %v\narchived: %v\n",
634 strings.Join(repo.Settings.ProtectedBranches, ", "), strings.Join(repo.Settings.ProtectedTags, ", "), repo.Settings.RequireMR, repo.Settings.RequireSignedCommits, repo.Settings.GitDaemon, repo.Settings.Archived)
631 v := c.view(w)
632 v.title(repo.Path(), "settings", "")
633 v.fields(
634 "protected branches", strings.Join(repo.Settings.ProtectedBranches, ", "),
635 "protected tags", strings.Join(repo.Settings.ProtectedTags, ", "),
636 "require mr", strconv.FormatBool(repo.Settings.RequireMR),
637 "require signed commits", strconv.FormatBool(repo.Settings.RequireSignedCommits),
638 "git daemon", strconv.FormatBool(repo.Settings.GitDaemon),
639 "archived", strconv.FormatBool(repo.Settings.Archived),
640 )
635641 })
636642}
637643
internal/control/snippet.go +14 −9
@@ -197,16 +197,21 @@ func runSnippetShow(c *Ctx, args []string) int {
197197 }
198198 sn.Files = files
199199 return c.emit(snippetOut(c, sn), func(w io.Writer) {
200 fmt.Fprintf(w, "snippet %s by %s (%s)\n", sn.PublicID, sn.OwnerName, sn.Visibility)
201 if sn.Description != "" {
202 fmt.Fprintf(w, "%s\n", sn.Description)
203 }
204 fmt.Fprintf(w, "%s\nupdated %s\n", snippetURL(c, sn), sn.UpdatedAt)
205 tb := c.table(w, "NAME", "SIZE")
206 for _, f := range files {
207 tb.row(cRef(" "+f.Name), cText(fmt.Sprintf("%d bytes", f.Size)))
200 v := c.view(w)
201 v.title(sn.PublicID, sn.Description, sn.Visibility)
202 v.fields(
203 "author", sn.OwnerName,
204 "updated", c.when(sn.UpdatedAt),
205 "url", snippetURL(c, sn),
206 )
207 if len(files) > 0 {
208 io.WriteString(w, "\n")
209 tb := c.table(w, "NAME", "SIZE")
210 for _, f := range files {
211 tb.row(cRef(f.Name), cText(fmt.Sprintf("%d bytes", f.Size)))
212 }
213 tb.flush()
208214 }
209 tb.flush()
210215 })
211216}
212217
internal/control/teams.go +10 −5
@@ -177,12 +177,17 @@ func runTeamShow(c *Ctx, args []string) int {
177177 Grants []store.TeamGrant `json:"grants,omitempty"`
178178 }{team.Name, members, grants}
179179 return c.emit(d, func(w io.Writer) {
180 fmt.Fprintf(w, "%s/%s\nmembers: %s\n", org.Name, team.Name, strings.Join(members, ", "))
181 tb := c.table(w, "REPO", "ROLE")
182 for _, g := range grants {
183 tb.row(cRef(g.RepoPath), cState(g.Role))
180 v := c.view(w)
181 v.title(org.Name+"/"+team.Name, "", "")
182 v.fields("members", strings.Join(members, ", "))
183 if len(grants) > 0 {
184 io.WriteString(w, "\n")
185 tb := c.table(w, "REPO", "ROLE")
186 for _, g := range grants {
187 tb.row(cRef(g.RepoPath), cState(g.Role))
188 }
189 tb.flush()
184190 }
185 tb.flush()
186191 })
187192}
188193
internal/control/wiki.go +18 −1
@@ -163,8 +163,25 @@ func runWikiShow(c *Ctx, args []string) int {
163163 } else {
164164 d.Content = string(raw)
165165 }
166 format := "md"
167 if ext == ".org" {
168 format = "org"
169 }
166170 return c.emit(d, func(w io.Writer) {
167 fmt.Fprint(w, d.Content)
171 v := c.view(w)
172 v.title(repo.Path(), page, "")
173 binaryNote := ""
174 if binary {
175 binaryNote = fmt.Sprintf("%d bytes, binary", d.Size)
176 }
177 v.fields(
178 "file", d.File,
179 "binary", binaryNote,
180 "url", c.siteURL(repo.Path(), "wiki", page),
181 )
182 if !binary {
183 v.body(d.Content, format)
184 }
168185 })
169186 }
170187 return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path())