Commit 98ba76cb39
Verified · cmc
e2e/adminusers_test.go +1 −1
| @@ -165,7 +165,7 @@ func TestAdminUserListAndShow(t *testing.T) { | ||
| 165 | 165 | t.Fatalf("unknown user: exit %d", code) |
| 166 | 166 | } |
| 167 | 167 | // 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") || | |
| 169 | 169 | !strings.Contains(out, "acme\tadmin") || !strings.Contains(out, "verified by admin") { |
| 170 | 170 | t.Fatalf("plain show:\n%s", out) |
| 171 | 171 | } |
e2e/maint_test.go +2 −1
| @@ -4,6 +4,7 @@ import ( | ||
| 4 | 4 | "fmt" |
| 5 | 5 | "os" |
| 6 | 6 | "path/filepath" |
| 7 | "regexp" | |
| 7 | 8 | "strings" |
| 8 | 9 | "testing" |
| 9 | 10 | ) |
| @@ -39,7 +40,7 @@ func TestAdminGCAndStats(t *testing.T) { | ||
| 39 | 40 | |
| 40 | 41 | // Stats: counts and per-repo disk usage. |
| 41 | 42 | 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)") || | |
| 43 | 44 | !strings.Contains(out, "alice/app") || !strings.Contains(out, "alice/other") { |
| 44 | 45 | t.Fatalf("stats output: %s", out) |
| 45 | 46 | } |
e2e/readonly_test.go +2 −7
| @@ -173,13 +173,8 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) { | ||
| 173 | 173 | // rawOutput prints content verbatim (a file, a log, a diff) and is |
| 174 | 174 | // not fitted to the terminal. |
| 175 | 175 | 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, | |
| 183 | 178 | } |
| 184 | 179 | // binaryOutput's bytes are not text: a stray 0x1b is coincidence, not |
| 185 | 180 | // an SGR sequence escaping into plain output. |
e2e/reap_test.go +3 −2
| @@ -7,6 +7,7 @@ import ( | ||
| 7 | 7 | "fmt" |
| 8 | 8 | "os" |
| 9 | 9 | "path/filepath" |
| 10 | "regexp" | |
| 10 | 11 | "strings" |
| 11 | 12 | "testing" |
| 12 | 13 | "time" |
| @@ -111,7 +112,7 @@ func TestAdminRunners(t *testing.T) { | ||
| 111 | 112 | t.Fatal("runner next failed") |
| 112 | 113 | } |
| 113 | 114 | 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") || | |
| 115 | 116 | !strings.Contains(out, "\talice/app\tidle") { |
| 116 | 117 | t.Fatalf("idle runner row:\n%s", out) |
| 117 | 118 | } |
| @@ -142,7 +143,7 @@ func TestAdminRunners(t *testing.T) { | ||
| 142 | 143 | t.Fatal("runner done failed") |
| 143 | 144 | } |
| 144 | 145 | 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) { | |
| 146 | 147 | t.Fatalf("runner still holds a build after done:\n%s", out) |
| 147 | 148 | } |
| 148 | 149 | // Host-local, the same read. |
e2e/release_test.go +2 −2
| @@ -49,14 +49,14 @@ func TestReleases(t *testing.T) { | ||
| 49 | 49 | t.Fatalf("release edit: %s", errOut) |
| 50 | 50 | } |
| 51 | 51 | 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") { | |
| 53 | 53 | t.Fatalf("edit lost a field:\n%s", out) |
| 54 | 54 | } |
| 55 | 55 | if _, _, code := inst.ssh(t, aliceKey, "", "release", "edit", "alice/app", "v1.0", "--title", "'Second light'"); code != 0 { |
| 56 | 56 | t.Fatal("title edit failed") |
| 57 | 57 | } |
| 58 | 58 | 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") { | |
| 60 | 60 | t.Fatalf("title edit lost notes:\n%s", out) |
| 61 | 61 | } |
| 62 | 62 | 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) { | ||
| 54 | 54 | } |
| 55 | 55 | |
| 56 | 56 | // 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") || | |
| 58 | 58 | !strings.Contains(out, "bookmarked") { |
| 59 | 59 | t.Errorf("plain output lacks the state:\n%s", out) |
| 60 | 60 | } |
internal/control/admin.go +64 −44
| @@ -249,57 +249,71 @@ func runAdminUserShow(c *Ctx, args []string) int { | ||
| 249 | 249 | } |
| 250 | 250 | |
| 251 | 251 | return c.emit(d, func(w io.Writer) { |
| 252 | fmt.Fprintf(w, "%s\t%s", d.Username, d.State) | |
| 252 | admin := "" | |
| 253 | 253 | if d.Admin { |
| 254 | fmt.Fprint(w, "\tadmin") | |
| 254 | admin = "yes" | |
| 255 | 255 | } |
| 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)) | |
| 273 | 270 | } |
| 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...) | |
| 277 | 286 | } |
| 278 | te.row(cells...) | |
| 287 | te.flush() | |
| 279 | 288 | } |
| 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() | |
| 285 | 296 | } |
| 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() | |
| 291 | 304 | } |
| 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)) | |
| 299 | 314 | } |
| 300 | tt.row(cRef(" "+t.Name), cState(t.Scope), cAge(used)) | |
| 315 | tt.flush() | |
| 301 | 316 | } |
| 302 | tt.flush() | |
| 303 | 317 | }) |
| 304 | 318 | } |
| 305 | 319 | |
| @@ -529,8 +543,14 @@ func runAdminRunners(c *Ctx, args []string) int { | ||
| 529 | 543 | } |
| 530 | 544 | d := map[string]any{"queue": queue, "runners": runners} |
| 531 | 545 | 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 | ) | |
| 534 | 554 | tb := c.table(w, "USER", "FINGERPRINT", "LAST SEEN", "SCOPE", "HELD") |
| 535 | 555 | for _, r := range runners { |
| 536 | 556 | scope := r.Scope |
internal/control/adminhost.go +18 −6
| @@ -302,12 +302,24 @@ func runAdminStats(c *Ctx, args []string) int { | ||
| 302 | 302 | d.DBBytes = fi.Size() |
| 303 | 303 | } |
| 304 | 304 | 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() | |
| 311 | 323 | } |
| 312 | 324 | }) |
| 313 | 325 | } |
internal/control/build.go +10 −6
| @@ -183,17 +183,21 @@ func buildSubjects(c *Ctx, repo store.Repo, ds []BuildOut) map[string]string { | ||
| 183 | 183 | } |
| 184 | 184 | |
| 185 | 185 | func runBuildShow(c *Ctx, args []string) int { |
| 186 | _, b, code := buildRef(c, args) | |
| 186 | repo, b, code := buildRef(c, args) | |
| 187 | 187 | if code >= 0 { |
| 188 | 188 | return code |
| 189 | 189 | } |
| 190 | 190 | d := buildToOut(b) |
| 191 | 191 | 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 | ) | |
| 197 | 201 | }) |
| 198 | 202 | } |
| 199 | 203 | |
internal/control/deps.go +18 −8
| @@ -87,7 +87,8 @@ func runDepsStatus(c *Ctx, args []string) int { | ||
| 87 | 87 | check, err := c.Store.DepCheckFor(repo.ID) |
| 88 | 88 | if errors.Is(err, store.ErrNotFound) { |
| 89 | 89 | 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())) | |
| 91 | 92 | }) |
| 92 | 93 | } |
| 93 | 94 | if err != nil { |
| @@ -103,15 +104,24 @@ func runDepsStatus(c *Ctx, args []string) int { | ||
| 103 | 104 | out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest}) |
| 104 | 105 | } |
| 105 | 106 | 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 := "" | |
| 110 | 108 | if check.IssueNumber != 0 { |
| 111 | fmt.Fprintf(w, "tracked in #%d\n", check.IssueNumber) | |
| 109 | tracked = fmt.Sprintf("#%d", check.IssueNumber) | |
| 112 | 110 | } |
| 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() | |
| 115 | 125 | } |
| 116 | 126 | }) |
| 117 | 127 | } |
internal/control/notifications.go +6 −1
| @@ -198,7 +198,12 @@ func emitNotificationSettings(c *Ctx) int { | ||
| 198 | 198 | } |
| 199 | 199 | return "off" |
| 200 | 200 | } |
| 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 | ) | |
| 202 | 207 | }) |
| 203 | 208 | } |
| 204 | 209 | |
internal/control/org.go +9 −5
| @@ -123,12 +123,16 @@ func runOrgShow(c *Ctx, args []string) int { | ||
| 123 | 123 | Members []memberOut `json:"members"` |
| 124 | 124 | }{org.Name, ms} |
| 125 | 125 | 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() | |
| 130 | 135 | } |
| 131 | tb.flush() | |
| 132 | 136 | }) |
| 133 | 137 | } |
| 134 | 138 | |
internal/control/profile.go +39 −20
| @@ -240,31 +240,50 @@ func ActivityWindow() string { | ||
| 240 | 240 | |
| 241 | 241 | func emitProfile(c *Ctx, d ProfileOut) int { |
| 242 | 242 | 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) | |
| 255 | 246 | } |
| 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() | |
| 258 | 261 | } |
| 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() | |
| 261 | 269 | } |
| 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() | |
| 264 | 277 | } |
| 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() | |
| 267 | 285 | } |
| 286 | v.body(d.About, d.AboutFormat) | |
| 268 | 287 | }) |
| 269 | 288 | } |
| 270 | 289 | |
internal/control/release.go +16 −5
| @@ -265,12 +265,23 @@ func runReleaseShow(c *Ctx, args []string) int { | ||
| 265 | 265 | } |
| 266 | 266 | d := releaseToOut(rel, true) |
| 267 | 267 | 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 = "" | |
| 271 | 271 | } |
| 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() | |
| 274 | 285 | } |
| 275 | 286 | }) |
| 276 | 287 | } |
internal/control/repo.go +43 −37
| @@ -8,6 +8,7 @@ import ( | ||
| 8 | 8 | "path" |
| 9 | 9 | "path/filepath" |
| 10 | 10 | "slices" |
| 11 | "strconv" | |
| 11 | 12 | "strings" |
| 12 | 13 | |
| 13 | 14 | "gitbay.org/gitbay/internal/gitutil" |
| @@ -361,44 +362,41 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 361 | 362 | } |
| 362 | 363 | } |
| 363 | 364 | 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 := "", "" | |
| 390 | 366 | if d.Bookmarked { |
| 391 | fmt.Fprintln(w, "bookmarked") | |
| 367 | bookmarked = "yes" | |
| 392 | 368 | } |
| 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)) | |
| 400 | 398 | } |
| 401 | fmt.Fprintf(w, "mirror: %s %s\tlast %s\t%s\n", m.Direction, m.URL, orDash(m.LastSync), status) | |
| 399 | tb.flush() | |
| 402 | 400 | } |
| 403 | 401 | }) |
| 404 | 402 | } |
| @@ -630,8 +628,16 @@ func runSettingsShow(c *Ctx, args []string) int { | ||
| 630 | 628 | return code |
| 631 | 629 | } |
| 632 | 630 | 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 | ) | |
| 635 | 641 | }) |
| 636 | 642 | } |
| 637 | 643 | |
internal/control/snippet.go +14 −9
| @@ -197,16 +197,21 @@ func runSnippetShow(c *Ctx, args []string) int { | ||
| 197 | 197 | } |
| 198 | 198 | sn.Files = files |
| 199 | 199 | 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() | |
| 208 | 214 | } |
| 209 | tb.flush() | |
| 210 | 215 | }) |
| 211 | 216 | } |
| 212 | 217 | |
internal/control/teams.go +10 −5
| @@ -177,12 +177,17 @@ func runTeamShow(c *Ctx, args []string) int { | ||
| 177 | 177 | Grants []store.TeamGrant `json:"grants,omitempty"` |
| 178 | 178 | }{team.Name, members, grants} |
| 179 | 179 | 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() | |
| 184 | 190 | } |
| 185 | tb.flush() | |
| 186 | 191 | }) |
| 187 | 192 | } |
| 188 | 193 | |
internal/control/wiki.go +18 −1
| @@ -163,8 +163,25 @@ func runWikiShow(c *Ctx, args []string) int { | ||
| 163 | 163 | } else { |
| 164 | 164 | d.Content = string(raw) |
| 165 | 165 | } |
| 166 | format := "md" | |
| 167 | if ext == ".org" { | |
| 168 | format = "org" | |
| 169 | } | |
| 166 | 170 | 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 | } | |
| 168 | 185 | }) |
| 169 | 186 | } |
| 170 | 187 | return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path()) |