Skip to content

Commit 79b7e0b

Browse files
Add table view for mq status
Co-authored-by: Andrew <andrewxhill@gmail.com>
1 parent e495fba commit 79b7e0b

3 files changed

Lines changed: 185 additions & 2 deletions

File tree

internal/app/app_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,37 @@ func TestStatusJSONReportsQueuedWork(t *testing.T) {
17601760
}
17611761
}
17621762

1763+
func TestStatusTableReportsHumanQueueSummary(t *testing.T) {
1764+
repoRoot, _ := createTestRepoWithRemote(t)
1765+
initRepoForWorker(t, repoRoot)
1766+
1767+
featurePath := filepath.Join(t.TempDir(), "feature-status-table")
1768+
runTestCommand(t, repoRoot, "git", "worktree", "add", "-b", "feature/status-table", featurePath)
1769+
writeFileAndCommit(t, featurePath, "status-table.txt", "status table\n", "feature status table")
1770+
submitBranch(t, featurePath)
1771+
queuePublish(t, repoRoot)
1772+
1773+
var stdout bytes.Buffer
1774+
var stderr bytes.Buffer
1775+
if err := runCLI([]string{"status", "--repo", repoRoot, "--table", "--events", "2"}, newStepPrinter(&stdout), &stderr); err != nil {
1776+
t.Fatalf("runCLI returned error: %v", err)
1777+
}
1778+
1779+
output := stdout.String()
1780+
if !strings.Contains(output, "TYPE") || !strings.Contains(output, "STATUS") || !strings.Contains(output, "BRANCH") {
1781+
t.Fatalf("expected table headers, got %q", output)
1782+
}
1783+
if !strings.Contains(output, "publish") || !strings.Contains(output, "submit") {
1784+
t.Fatalf("expected publish and submit rows, got %q", output)
1785+
}
1786+
if !strings.Contains(output, "feature/status-table") {
1787+
t.Fatalf("expected feature branch in table, got %q", output)
1788+
}
1789+
if !strings.Contains(output, "Counts: queued_submissions=") {
1790+
t.Fatalf("expected compact counts summary, got %q", output)
1791+
}
1792+
}
1793+
17631794
func TestStatusUsesDurableRepoRecordWhenConfigDrifts(t *testing.T) {
17641795
repoRoot, _ := createTestRepoWithRemote(t)
17651796
initRepoForWorker(t, repoRoot)

internal/app/completion.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ _mainline_completions()
9494
COMPREPLY=( $(compgen -W "--repo --branch --sha --worktree --requested-by --priority --allow-newer-head --json --check --check-only --queue-only --wait --for --timeout --poll-interval" -- "$cur") )
9595
;;
9696
status)
97-
COMPREPLY=( $(compgen -W "--repo --json --events" -- "$cur") )
97+
COMPREPLY=( $(compgen -W "--repo --json --table --events" -- "$cur") )
9898
;;
9999
next)
100100
COMPREPLY=( $(compgen -W "commit push --repo --json --timeout --poll-interval" -- "$cur") )
@@ -233,7 +233,7 @@ _mainline() {
233233
return
234234
;;
235235
status)
236-
_arguments '--repo[repository path]:path:_files -/' '--json[json output]' '--events[number of recent events]:count:'
236+
_arguments '--repo[repository path]:path:_files -/' '--json[json output]' '--table[compact human-readable table output]' '--events[number of recent events]:count:'
237237
return
238238
;;
239239
confidence)
@@ -365,6 +365,8 @@ complete -c mainline -n "__fish_seen_subcommand_from watch" -l max-cycles
365365
complete -c mq -n "__fish_seen_subcommand_from watch" -l max-cycles
366366
complete -c mainline -n "__fish_seen_subcommand_from status" -l events
367367
complete -c mq -n "__fish_seen_subcommand_from status" -l events
368+
complete -c mainline -n "__fish_seen_subcommand_from status" -l table
369+
complete -c mq -n "__fish_seen_subcommand_from status" -l table
368370
complete -c mainline -n "__fish_seen_subcommand_from confidence" -l json
369371
complete -c mq -n "__fish_seen_subcommand_from confidence" -l json
370372
complete -c mainline -n "__fish_seen_subcommand_from confidence" -l events

internal/app/status.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"strings"
11+
"text/tabwriter"
1112
"time"
1213

1314
"github.com/recallnet/mainline/internal/domain"
@@ -187,10 +188,12 @@ Flags:
187188

188189
var repoPath string
189190
var asJSON bool
191+
var asTable bool
190192
var limit int
191193

192194
fs.StringVar(&repoPath, "repo", ".", "repository path")
193195
fs.BoolVar(&asJSON, "json", false, "output json")
196+
fs.BoolVar(&asTable, "table", false, "render a compact human-readable table")
194197
fs.IntVar(&limit, "events", 5, "number of recent events to show")
195198

196199
if err := fs.Parse(args); err != nil {
@@ -207,6 +210,9 @@ Flags:
207210
encoder.SetIndent("", " ")
208211
return encoder.Encode(result)
209212
}
213+
if asTable {
214+
return renderStatusTable(stdout, result)
215+
}
210216

211217
return renderStatus(stdout, result)
212218
}
@@ -450,6 +456,150 @@ func renderStatus(stdout *stepPrinter, result statusResult) error {
450456
return nil
451457
}
452458

459+
func renderStatusTable(stdout *stepPrinter, result statusResult) error {
460+
stdout.Section("Queue")
461+
stdout.Line("Repo: %s", result.RepositoryRoot)
462+
stdout.Line("State: %s", result.QueueSummary.Headline)
463+
if result.ProtectedBranchSHA != "" {
464+
stdout.Line("Protected: %s @ %s", result.ProtectedBranch, shortenSHA(result.ProtectedBranchSHA))
465+
} else {
466+
stdout.Line("Protected: %s", result.ProtectedBranch)
467+
}
468+
if result.ProtectedUpstream.HasUpstream {
469+
stdout.Line("Upstream: %s (ahead %d, behind %d)", result.ProtectedUpstream.Upstream, result.ProtectedUpstream.AheadCount, result.ProtectedUpstream.BehindCount)
470+
}
471+
if result.ProtectedWorktreeActivity != nil {
472+
stdout.Line("Activity: %s", result.ProtectedWorktreeActivity.Summary)
473+
}
474+
475+
tw := tabwriter.NewWriter(stdout.Raw(), 0, 0, 2, ' ', 0)
476+
_, _ = fmt.Fprintln(tw, "")
477+
_, _ = fmt.Fprintln(tw, "TYPE\tID\tSTATUS\tSTAGE\tBRANCH\tTARGET")
478+
479+
rows := statusTableRows(result)
480+
if len(rows) == 0 {
481+
_, _ = fmt.Fprintln(tw, "-\t-\tidle\t-\t-\t-")
482+
} else {
483+
for _, row := range rows {
484+
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", row[0], row[1], row[2], row[3], row[4], row[5])
485+
}
486+
}
487+
if err := tw.Flush(); err != nil {
488+
return err
489+
}
490+
491+
stdout.Line("")
492+
stdout.Line("Counts: queued_submissions=%d running_submissions=%d blocked_submissions=%d queued_publishes=%d running_publishes=%d",
493+
result.Counts.QueuedSubmissions,
494+
result.Counts.RunningSubmissions,
495+
result.Counts.BlockSubmissions,
496+
result.Counts.QueuedPublishes,
497+
result.Counts.RunningPublishes,
498+
)
499+
if len(result.Alerts) > 0 {
500+
for _, alert := range result.Alerts {
501+
stdout.Line("Alert: %s", alert)
502+
}
503+
}
504+
return nil
505+
}
506+
507+
func statusTableRows(result statusResult) [][]string {
508+
rows := make([][]string, 0, len(result.ActivePublishes)+len(result.ActiveSubmissions)+2)
509+
for _, request := range result.ActivePublishes {
510+
rows = append(rows, []string{
511+
"publish",
512+
fmt.Sprintf("%d", request.ID),
513+
string(request.Status),
514+
emptyDash(request.ActiveStage),
515+
"-",
516+
shortenSHA(request.TargetSHA),
517+
})
518+
}
519+
for _, submission := range result.ActiveSubmissions {
520+
rows = append(rows, []string{
521+
"submit",
522+
fmt.Sprintf("%d", submission.ID),
523+
string(submission.Status),
524+
submissionStage(submission),
525+
submissionDisplayRef(submission.integrationSubmission()),
526+
shortenSHA(nonEmpty(submission.ProtectedTipSHA, submission.SourceSHA)),
527+
})
528+
}
529+
if result.LatestSubmission != nil && !containsStatusRow(rows, "submit", result.LatestSubmission.ID) {
530+
rows = append(rows, []string{
531+
"submit",
532+
fmt.Sprintf("%d", result.LatestSubmission.ID),
533+
string(result.LatestSubmission.Status),
534+
submissionStage(*result.LatestSubmission),
535+
submissionDisplayRef(result.LatestSubmission.integrationSubmission()),
536+
shortenSHA(nonEmpty(result.LatestSubmission.ProtectedTipSHA, result.LatestSubmission.SourceSHA)),
537+
})
538+
}
539+
if result.LatestPublish != nil && !containsStatusRow(rows, "publish", result.LatestPublish.ID) {
540+
rows = append(rows, []string{
541+
"publish",
542+
fmt.Sprintf("%d", result.LatestPublish.ID),
543+
string(result.LatestPublish.Status),
544+
emptyDash(result.LatestPublish.ActiveStage),
545+
"-",
546+
shortenSHA(result.LatestPublish.TargetSHA),
547+
})
548+
}
549+
return rows
550+
}
551+
552+
func containsStatusRow(rows [][]string, rowType string, id int64) bool {
553+
needle := fmt.Sprintf("%d", id)
554+
for _, row := range rows {
555+
if len(row) >= 2 && row[0] == rowType && row[1] == needle {
556+
return true
557+
}
558+
}
559+
return false
560+
}
561+
562+
func submissionStage(submission statusSubmission) string {
563+
switch submission.Status {
564+
case domain.SubmissionStatusQueued:
565+
return "queued"
566+
case domain.SubmissionStatusRunning:
567+
return "integrating"
568+
case domain.SubmissionStatusBlocked:
569+
return "blocked"
570+
case domain.SubmissionStatusSucceeded:
571+
if submission.Outcome != "" {
572+
return string(submission.Outcome)
573+
}
574+
return "integrated"
575+
default:
576+
if submission.Outcome != "" {
577+
return string(submission.Outcome)
578+
}
579+
return "-"
580+
}
581+
}
582+
583+
func shortenSHA(sha string) string {
584+
trimmed := strings.TrimSpace(sha)
585+
if len(trimmed) > 8 {
586+
return trimmed[:8]
587+
}
588+
if trimmed == "" {
589+
return "-"
590+
}
591+
return trimmed
592+
}
593+
594+
func nonEmpty(values ...string) string {
595+
for _, value := range values {
596+
if strings.TrimSpace(value) != "" {
597+
return value
598+
}
599+
}
600+
return ""
601+
}
602+
453603
func buildStatusRebaseGuidance(cfg policy.File, comparison git.BranchComparison, protectedStatus git.BranchStatus, repoPath string, branch string) *statusRebaseGuidance {
454604
guidance := &statusRebaseGuidance{
455605
NeedsRebase: comparison.BehindCount > 0,

0 commit comments

Comments
 (0)