Commit 92c7105063
Verified · cmc
Layout: unified · split
internal/control/build.go +18 −1
| @@ -456,7 +456,7 @@ func runBuildJobs(c *Ctx, args []string) int { | ||
| 456 | 456 | for _, j := range jobs { |
| 457 | 457 | out = append(out, JobOut{Name: j.Name, Schedule: j.Schedule, Tags: j.Tags}) |
| 458 | 458 | } |
| 459 | return c.emit(out, func(w io.Writer) { | |
| 459 | return c.emitView(out, func(w io.Writer) { | |
| 460 | 460 | tb := c.table(w, "NAME", "WHEN") |
| 461 | 461 | for _, j := range out { |
| 462 | 462 | when := "on push" |
| @@ -469,6 +469,23 @@ func runBuildJobs(c *Ctx, args []string) int { | ||
| 469 | 469 | tb.row(cRef(j.Name), cText(when)) |
| 470 | 470 | } |
| 471 | 471 | tb.flush() |
| 472 | }, func() screen { | |
| 473 | rows := make([]row, len(out)) | |
| 474 | for i, j := range out { | |
| 475 | when := "on push" | |
| 476 | switch { | |
| 477 | case j.Schedule != "": | |
| 478 | when = "schedule " + j.Schedule | |
| 479 | case j.Tags != "": | |
| 480 | when = "tags " + j.Tags | |
| 481 | } | |
| 482 | rows[i] = rowOf(cRef(j.Name), cMeta(when)) | |
| 483 | } | |
| 484 | s := listScreen("Jobs", rows) | |
| 485 | if len(out) > 0 { | |
| 486 | s.actions = []action{{"Run", []string{"build", "trigger", repo.Path(), out[0].Name}}} | |
| 487 | } | |
| 488 | return s | |
| 472 | 489 | }) |
| 473 | 490 | } |
| 474 | 491 | |
internal/control/dashboard.go +7 −9
| @@ -357,20 +357,18 @@ func runFeed(c *Ctx, args []string) int { | ||
| 357 | 357 | }) |
| 358 | 358 | ds := feedOutputs(events) |
| 359 | 359 | lines := FeedLines(events) |
| 360 | return c.emitPage(p, ds, next, func(w io.Writer) { | |
| 361 | if c.Term.Cols > 0 { | |
| 362 | tb := c.table(w, feedHeader...) | |
| 363 | for _, l := range lines { | |
| 364 | tb.row(l.termCells(c)...) | |
| 365 | } | |
| 366 | tb.flush() | |
| 367 | return | |
| 368 | } | |
| 360 | return c.emitPageView(p, ds, next, func(w io.Writer) { | |
| 369 | 361 | tb := c.table(w, "WHEN", "EVENT") |
| 370 | 362 | for _, l := range lines { |
| 371 | 363 | tb.row(cAge(l.When), cFlex(l.Sentence())) |
| 372 | 364 | } |
| 373 | 365 | tb.flush() |
| 366 | }, func() screen { | |
| 367 | rows := make([]row, len(lines)) | |
| 368 | for i, l := range lines { | |
| 369 | rows[i] = rowOf(l.termCells(c)...) | |
| 370 | } | |
| 371 | return listScreen("Activity", rows) | |
| 374 | 372 | }) |
| 375 | 373 | } |
| 376 | 374 | |
internal/control/feedline.go +3 −5
| @@ -42,11 +42,9 @@ func (l FeedLine) Sentence() string { | ||
| 42 | 42 | return s |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | // feedHeader and termCells are a feed line as a terminal table shows | |
| 46 | // it: who, what, where, and how a build came out, so the outcome and the | |
| 47 | // reference can carry their colours. | |
| 48 | var feedHeader = []string{"WHEN", "WHO", "EVENT", "WHERE", "DETAIL"} | |
| 49 | ||
| 45 | // termCells is a feed line as a screen row shows it: when, who, what, | |
| 46 | // where, and how a build came out, so the outcome and the reference can | |
| 47 | // carry their colours. | |
| 50 | 48 | func (l FeedLine) termCells(c *Ctx) []cell { |
| 51 | 49 | ref := l.Ref |
| 52 | 50 | if ref != "" && ref[0] != '#' && ref[0] != '!' { |
internal/control/milestone.go +9 −1
| @@ -293,12 +293,20 @@ func runIssueTemplates(c *Ctx, args []string) int { | ||
| 293 | 293 | } |
| 294 | 294 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) |
| 295 | 295 | ts := IssueTemplates(dir, repo.DefaultBranch) |
| 296 | return c.emit(ts, func(w io.Writer) { | |
| 296 | return c.emitView(ts, func(w io.Writer) { | |
| 297 | 297 | tb := c.table(w, "NAME") |
| 298 | 298 | for _, t := range ts { |
| 299 | 299 | tb.row(cRef(t.Name)) |
| 300 | 300 | } |
| 301 | 301 | tb.flush() |
| 302 | }, func() screen { | |
| 303 | rows := make([]row, len(ts)) | |
| 304 | for i, tpl := range ts { | |
| 305 | rows[i] = rowOf(cRef(tpl.Name)) | |
| 306 | } | |
| 307 | return listScreen("Issue templates", rows, | |
| 308 | action{"New", []string{"issue", "create", repo.Path(), "--title", "<title>"}}, | |
| 309 | ) | |
| 302 | 310 | }) |
| 303 | 311 | } |
| 304 | 312 | |
internal/control/mr.go +17 −1
| @@ -2089,7 +2089,7 @@ func runMRRevisions(c *Ctx, args []string) int { | ||
| 2089 | 2089 | if err != nil { |
| 2090 | 2090 | return c.fail(protocol.ExitFailure, "%v", err) |
| 2091 | 2091 | } |
| 2092 | return c.emit(revs, func(w io.Writer) { | |
| 2092 | return c.emitView(revs, func(w io.Writer) { | |
| 2093 | 2093 | tb := c.table(w, "REV", "SHA", "WHEN") |
| 2094 | 2094 | for _, r := range revs { |
| 2095 | 2095 | mark := " " |
| @@ -2103,6 +2103,22 @@ func runMRRevisions(c *Ctx, args []string) int { | ||
| 2103 | 2103 | fmt.Fprintf(c.Stderr, "only one revision; %s!%d has not been pushed to since it was opened\n", |
| 2104 | 2104 | repo.Path(), mr.Number) |
| 2105 | 2105 | } |
| 2106 | }, func() screen { | |
| 2107 | rows := make([]row, len(revs)) | |
| 2108 | for i, r := range revs { | |
| 2109 | lead := cell{kind: kindGlyph} | |
| 2110 | if r.Current { | |
| 2111 | lead = cell{kind: kindGlyph, s: "●"} | |
| 2112 | } | |
| 2113 | rows[i] = rowOf(cRef(fmt.Sprintf("v%d", r.N)), lead, cRef(fmt.Sprintf("%.10s", r.SHA)), cAge(r.CreatedAt)) | |
| 2114 | } | |
| 2115 | if len(revs) < 2 { | |
| 2116 | fmt.Fprintf(c.Stderr, "only one revision; %s!%d has not been pushed to since it was opened\n", | |
| 2117 | repo.Path(), mr.Number) | |
| 2118 | } | |
| 2119 | return listScreen("Revisions", rows, | |
| 2120 | action{"Compare", []string{"mr", "range-diff", repo.Path(), strconv.FormatInt(mr.Number, 10)}}, | |
| 2121 | ) | |
| 2106 | 2122 | }) |
| 2107 | 2123 | } |
| 2108 | 2124 | |
internal/control/query.go +55 −6
| @@ -193,7 +193,7 @@ func savedQuery(c *Ctx, name string) (store.SavedQuery, ItemQuery, int) { | ||
| 193 | 193 | |
| 194 | 194 | // runItemQuery lists what q matches in the tables issues and mrs allow, |
| 195 | 195 | // one page at a time. The output is always the paged shape. |
| 196 | func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page) int { | |
| 196 | func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page, title string) int { | |
| 197 | 197 | var after *store.ItemCursor |
| 198 | 198 | if p.key != "" { |
| 199 | 199 | var err error |
| @@ -215,7 +215,7 @@ func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page) int { | ||
| 215 | 215 | next = encodeItemCursor(items[len(items)-1].Cursor()) |
| 216 | 216 | } |
| 217 | 217 | ds := queryItems(items) |
| 218 | return c.emitPage(p, ds, next, func(w io.Writer) { | |
| 218 | return c.emitPageView(p, ds, next, func(w io.Writer) { | |
| 219 | 219 | tb := c.table(w, "REF", "STATE", "TITLE", "AUTHOR") |
| 220 | 220 | for _, d := range ds { |
| 221 | 221 | state := d.State |
| @@ -225,6 +225,25 @@ func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page) int { | ||
| 225 | 225 | tb.row(cRef(d.Ref()), cState(state), cFlex(d.Title), cText(d.Author)) |
| 226 | 226 | } |
| 227 | 227 | tb.flush() |
| 228 | }, func() screen { | |
| 229 | rows := make([]row, len(ds)) | |
| 230 | for i, d := range ds { | |
| 231 | state := d.State | |
| 232 | if d.Draft { | |
| 233 | state = "draft" | |
| 234 | } | |
| 235 | page := "issues" | |
| 236 | if d.Kind == "mr" { | |
| 237 | page = "mrs" | |
| 238 | } | |
| 239 | rows[i] = rowOf(cLink(d.Ref(), c.siteURL(d.Repo, page, strconv.FormatInt(d.Number, 10))), cGlyph(state), cFlex(d.Title), | |
| 240 | cMeta(d.Author, d.Milestone, relAge(d.UpdatedAt, termNow()))) | |
| 241 | } | |
| 242 | s := listScreen(title, rows) | |
| 243 | if len(ds) > 0 { | |
| 244 | s.actions = []action{{"Read", []string{ds[0].Kind, "show", ds[0].Repo, strconv.FormatInt(ds[0].Number, 10)}}} | |
| 245 | } | |
| 246 | return s | |
| 228 | 247 | }) |
| 229 | 248 | } |
| 230 | 249 | |
| @@ -273,7 +292,7 @@ func listByQuery(c *Ctx, fl flags, kind string, p page) int { | ||
| 273 | 292 | if kind == "mr" && !mrs { |
| 274 | 293 | return c.usageWith("that query matches only issues; use issue list or query run") |
| 275 | 294 | } |
| 276 | return runItemQuery(c, q, kind == "issue", kind == "mr", p) | |
| 295 | return runItemQuery(c, q, kind == "issue", kind == "mr", p, "Results") | |
| 277 | 296 | } |
| 278 | 297 | |
| 279 | 298 | func savedQueryOut(sq store.SavedQuery) SavedQueryOut { |
| @@ -335,7 +354,7 @@ func runQueryList(c *Ctx, args []string) int { | ||
| 335 | 354 | for _, sq := range saved { |
| 336 | 355 | ds = append(ds, savedQueryOut(sq)) |
| 337 | 356 | } |
| 338 | return c.emit(ds, func(w io.Writer) { | |
| 357 | return c.emitView(ds, func(w io.Writer) { | |
| 339 | 358 | tb := c.table(w, "NAME", "PINNED", "QUERY") |
| 340 | 359 | for _, d := range ds { |
| 341 | 360 | pinned := "" |
| @@ -345,6 +364,23 @@ func runQueryList(c *Ctx, args []string) int { | ||
| 345 | 364 | tb.row(cRef(d.Name), cText(pinned), cFlex(d.Query)) |
| 346 | 365 | } |
| 347 | 366 | tb.flush() |
| 367 | }, func() screen { | |
| 368 | rows := make([]row, len(ds)) | |
| 369 | for i, d := range ds { | |
| 370 | pinned := "" | |
| 371 | if d.Pinned { | |
| 372 | pinned = "pinned" | |
| 373 | } | |
| 374 | rows[i] = rowOf(cRef(d.Name), cFlex(d.Query), cMeta(pinned)) | |
| 375 | } | |
| 376 | s := listScreen("Saved queries", rows) | |
| 377 | if len(ds) > 0 { | |
| 378 | s.actions = []action{ | |
| 379 | {"Queries", []string{"query", "run", ds[0].Name}}, | |
| 380 | {"Queries", []string{"query", "pin", ds[0].Name}}, | |
| 381 | } | |
| 382 | } | |
| 383 | return s | |
| 348 | 384 | }) |
| 349 | 385 | } |
| 350 | 386 | |
| @@ -362,7 +398,7 @@ func runQueryShow(c *Ctx, args []string) int { | ||
| 362 | 398 | } |
| 363 | 399 | d := savedQueryOut(sq) |
| 364 | 400 | d.Count = &n |
| 365 | return c.emit(d, func(w io.Writer) { | |
| 401 | return c.emitView(d, func(w io.Writer) { | |
| 366 | 402 | v := c.view(w) |
| 367 | 403 | v.title(d.Name, "", "") |
| 368 | 404 | pinned := "no" |
| @@ -370,6 +406,19 @@ func runQueryShow(c *Ctx, args []string) int { | ||
| 370 | 406 | pinned = "yes" |
| 371 | 407 | } |
| 372 | 408 | v.fields("query", d.Query, "matches", strconv.Itoa(n), "pinned", pinned) |
| 409 | }, func() screen { | |
| 410 | pin, pinned := "pin", "no" | |
| 411 | if d.Pinned { | |
| 412 | pin, pinned = "unpin", "yes" | |
| 413 | } | |
| 414 | return screen{fields: []field{ | |
| 415 | {"Query", []cell{cRef(d.Name), cText(d.Query)}}, | |
| 416 | {"Matches", []cell{cText(strconv.Itoa(n))}}, | |
| 417 | {"Pinned", []cell{cText(pinned)}}, | |
| 418 | }, actions: []action{ | |
| 419 | {"Queries", []string{"query", "run", d.Name}}, | |
| 420 | {"Queries", []string{"query", pin, d.Name}}, | |
| 421 | }} | |
| 373 | 422 | }) |
| 374 | 423 | } |
| 375 | 424 | |
| @@ -385,7 +434,7 @@ func runQueryRun(c *Ctx, args []string) int { | ||
| 385 | 434 | if code >= 0 { |
| 386 | 435 | return code |
| 387 | 436 | } |
| 388 | return runItemQuery(c, q, true, true, p) | |
| 437 | return runItemQuery(c, q, true, true, p, rest[0]) | |
| 389 | 438 | } |
| 390 | 439 | |
| 391 | 440 | func runQueryRemove(c *Ctx, args []string) int { |
internal/control/search.go +30 −6
| @@ -105,7 +105,9 @@ func runSearch(c *Ctx, args []string) int { | ||
| 105 | 105 | if err != nil { |
| 106 | 106 | return c.fail(protocol.ExitFailure, "%v", err) |
| 107 | 107 | } |
| 108 | return c.emit(results, func(w io.Writer) { writeSearchTable(c, w, results) }) | |
| 108 | return c.emitView(results, func(w io.Writer) { writeSearchTable(c, w, results) }, func() screen { | |
| 109 | return searchScreen(c, f.Pos[0], results) | |
| 110 | }) | |
| 109 | 111 | } |
| 110 | 112 | |
| 111 | 113 | // writeSearchTable renders search results: a repo hit has no ref number or |
| @@ -118,11 +120,7 @@ func writeSearchTable(c *Ctx, w io.Writer, results []SearchResult) { | ||
| 118 | 120 | for _, r := range results { |
| 119 | 121 | switch r.Kind { |
| 120 | 122 | case "repo": |
| 121 | if c.Term.Cols > 0 { | |
| 122 | tb.row(cText("repo"), cLink(r.Repo, c.siteURL(r.Repo)), cState(""), cFlex(r.Title)) | |
| 123 | } else { | |
| 124 | tb.row(cText("repo"), cRef(r.Repo), cFlex(r.Title)) | |
| 125 | } | |
| 123 | tb.row(cText("repo"), cRef(r.Repo), cFlex(r.Title)) | |
| 126 | 124 | default: |
| 127 | 125 | page := "issues" |
| 128 | 126 | if r.Kind == "mr" { |
| @@ -134,6 +132,32 @@ func writeSearchTable(c *Ctx, w io.Writer, results []SearchResult) { | ||
| 134 | 132 | tb.flush() |
| 135 | 133 | } |
| 136 | 134 | |
| 135 | // searchScreen is search at a terminal: a section per kind of result. | |
| 136 | func searchScreen(c *Ctx, query string, results []SearchResult) screen { | |
| 137 | repos := section{title: "Repositories"} | |
| 138 | issues := section{title: "Issues"} | |
| 139 | mrs := section{title: "Merge requests"} | |
| 140 | for _, r := range results { | |
| 141 | switch r.Kind { | |
| 142 | case "repo": | |
| 143 | repos.n++ | |
| 144 | repos.rows = append(repos.rows, rowOf(cLink(r.Repo, c.siteURL(r.Repo)), cFlex(r.Title))) | |
| 145 | default: | |
| 146 | sec, page := &issues, "issues" | |
| 147 | if r.Kind == "mr" { | |
| 148 | sec, page = &mrs, "mrs" | |
| 149 | } | |
| 150 | sec.n++ | |
| 151 | ref := cLink(fmt.Sprintf("%s%s%d", r.Repo, SearchMarker(r.Kind), r.Number), c.siteURL(r.Repo, page, strconv.FormatInt(r.Number, 10))) | |
| 152 | sec.rows = append(sec.rows, rowOf(ref, cGlyph(r.State), cFlex(r.Title), cMeta(r.Author))) | |
| 153 | } | |
| 154 | } | |
| 155 | return screen{sections: []section{repos, issues, mrs}, actions: []action{ | |
| 156 | {"Narrow", []string{"search", query, "--kind", "issue"}}, | |
| 157 | {"Narrow", []string{"search", query, "--kind", "mr"}}, | |
| 158 | }} | |
| 159 | } | |
| 160 | ||
| 137 | 161 | // SearchMarker is the sigil a result's number carries, shared with the web |
| 138 | 162 | // so a hit reads the same in both places. |
| 139 | 163 | func SearchMarker(kind string) string { |
internal/control/search_test.go −27
| @@ -2,36 +2,9 @@ package control | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | "strings" | |
| 6 | 5 | "testing" |
| 7 | 6 | ) |
| 8 | 7 | |
| 9 | // A repo hit has no state, an issue/mr hit does. At a terminal both | |
| 10 | // titles must start in the same column, not have the repo's title slide | |
| 11 | // left into the state column. | |
| 12 | func TestSearchTableAlignsTitleAtTerminal(t *testing.T) { | |
| 13 | results := []SearchResult{ | |
| 14 | {Kind: "repo", Repo: "alice/webapp", Title: "a web application"}, | |
| 15 | {Kind: "issue", Repo: "alice/webapp", Number: 4, Title: "memory leak", State: "open"}, | |
| 16 | } | |
| 17 | var b bytes.Buffer | |
| 18 | writeSearchTable(&Ctx{Term: Term{Cols: 100}}, &b, results) | |
| 19 | ||
| 20 | lines := strings.Split(strings.TrimRight(b.String(), "\n"), "\n") | |
| 21 | if len(lines) != 2 { | |
| 22 | t.Fatalf("want 2 rows, got %d lines:\n%s", len(lines), b.String()) | |
| 23 | } | |
| 24 | repoRow, issueRow := lines[0], lines[1] | |
| 25 | repoTitleAt := strings.Index(repoRow, "a web application") | |
| 26 | issueTitleAt := strings.Index(issueRow, "memory leak") | |
| 27 | if repoTitleAt < 0 || issueTitleAt < 0 { | |
| 28 | t.Fatalf("columns not found:\n%s", b.String()) | |
| 29 | } | |
| 30 | if repoTitleAt != issueTitleAt { | |
| 31 | t.Errorf("titles not aligned: repo row at %d, issue row at %d\n%s", repoTitleAt, issueTitleAt, b.String()) | |
| 32 | } | |
| 33 | } | |
| 34 | ||
| 35 | 8 | // Plain output has no header to align to, so a repo row stays 3 cells — |
| 36 | 9 | // bytes must not change from before the terminal fix. |
| 37 | 10 | func TestSearchTablePlainRepoRowIsThreeCells(t *testing.T) { |
internal/control/stage3work_test.go added +103
| @@ -0,0 +1,103 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "os" | |
| 5 | "os/exec" | |
| 6 | "path/filepath" | |
| 7 | "strings" | |
| 8 | "testing" | |
| 9 | ||
| 10 | "gitbay.org/gitbay/internal/store" | |
| 11 | ) | |
| 12 | ||
| 13 | // workFixture is a repository on disk with a CI job and an issue | |
| 14 | // template, an issue, a merge request, a commit status and a saved | |
| 15 | // query, all as alice. It returns the head sha. | |
| 16 | func workFixture(t *testing.T) (*store.Store, store.Repo, store.User, string, string) { | |
| 17 | t.Helper() | |
| 18 | st, repo, uid := newQueueTestRepo(t) | |
| 19 | u := store.User{ID: uid, Username: "alice"} | |
| 20 | root := t.TempDir() | |
| 21 | env := append(gitTestEnv(), "GIT_AUTHOR_DATE=2026-09-01T10:00:00Z", "GIT_COMMITTER_DATE=2026-09-01T10:00:00Z") | |
| 22 | git := func(dir string, args ...string) string { | |
| 23 | t.Helper() | |
| 24 | cmd := exec.Command("git", args...) | |
| 25 | cmd.Dir, cmd.Env = dir, env | |
| 26 | out, err := cmd.CombinedOutput() | |
| 27 | if err != nil { | |
| 28 | t.Fatalf("git %v: %v\n%s", args, err, out) | |
| 29 | } | |
| 30 | return strings.TrimSpace(string(out)) | |
| 31 | } | |
| 32 | src := filepath.Join(root, "src") | |
| 33 | if err := os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755); err != nil { | |
| 34 | t.Fatal(err) | |
| 35 | } | |
| 36 | os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte("jobs:\n test:\n steps:\n - go test ./...\n"), 0o644) | |
| 37 | os.WriteFile(filepath.Join(src, ".gitbay", "issue-template-bug.md"), []byte("## Steps\n"), 0o644) | |
| 38 | git(root, "init", "-q", "-b", "main", "src") | |
| 39 | git(src, "add", ".") | |
| 40 | git(src, "commit", "-q", "-m", "first") | |
| 41 | sha := git(src, "rev-parse", "HEAD") | |
| 42 | dir := RepoDir(root, repo.OwnerName, repo.Name) | |
| 43 | os.MkdirAll(filepath.Dir(dir), 0o755) | |
| 44 | git(root, "clone", "-q", "--bare", src, dir) | |
| 45 | ||
| 46 | p := repo.Path() | |
| 47 | dispatchIn(t, st, u, root, "", "issue", "create", p, "--title", "Crash on start", "--body", "It crashes.") | |
| 48 | if _, err := st.CreateMR(repo.ID, uid, repo.ID, "fix", "main", "Fix crash", "", sha, "md", false); err != nil { | |
| 49 | t.Fatal(err) | |
| 50 | } | |
| 51 | dispatchIn(t, st, u, root, "", "status", "set", p, sha, "--context", "ext/test", "--state", "success", "--description", "all green") | |
| 52 | dispatchIn(t, st, u, root, "", "query", "save", "open", "is:open") | |
| 53 | return st, repo, u, root, sha | |
| 54 | } | |
| 55 | ||
| 56 | func TestWorkPlainPinnedA(t *testing.T) { | |
| 57 | st, repo, u, root, sha := workFixture(t) | |
| 58 | p := repo.Path() | |
| 59 | for name, argv := range map[string][]string{ | |
| 60 | "query-run": {"query", "run", "open"}, | |
| 61 | "issue-list-q": {"issue", "list", "--q", "repo:alice/app is:open"}, | |
| 62 | "query-list": {"query", "list"}, | |
| 63 | "query-show": {"query", "show", "open"}, | |
| 64 | "search": {"search", "crash"}, | |
| 65 | "feed": {"feed"}, | |
| 66 | "mr-revisions": {"mr", "revisions", p, "1"}, | |
| 67 | "issue-templates": {"issue", "templates", p}, | |
| 68 | "build-jobs": {"build", "jobs", p}, | |
| 69 | "status-list": {"status", "list", p, sha}, | |
| 70 | } { | |
| 71 | pinPlain(t, name, dispatchIn(t, st, u, root, "", argv...)) | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | func TestWorkScreensA(t *testing.T) { | |
| 76 | st, repo, u, root, sha := workFixture(t) | |
| 77 | p := repo.Path() | |
| 78 | for _, tc := range []struct { | |
| 79 | argv []string | |
| 80 | want []string | |
| 81 | }{ | |
| 82 | {[]string{"query", "run", "open"}, []string{"open (2)\n", "alice/app#1", "Crash on start", "alice/app!1"}}, | |
| 83 | {[]string{"issue", "list", "--q", "repo:alice/app is:open"}, []string{"Results (1)\n", "Crash on start"}}, | |
| 84 | {[]string{"query", "list"}, []string{"Saved queries (1)\n", "open is:open"}}, | |
| 85 | {[]string{"query", "show", "open"}, []string{"Query:", "is:open", "Matches:", "2"}}, | |
| 86 | {[]string{"search", "crash"}, []string{"Issues (1)\n", "Merge requests (1)\n", "Crash on start"}}, | |
| 87 | {[]string{"feed"}, []string{"Activity ("}}, | |
| 88 | {[]string{"mr", "revisions", p, "1"}, []string{"Revisions (1)\n", "v1"}}, | |
| 89 | {[]string{"issue", "templates", p}, []string{"Issue templates (1)\nissue-template-bug.md"}}, | |
| 90 | {[]string{"build", "jobs", p}, []string{"Jobs (1)\ntest on push\n"}}, | |
| 91 | {[]string{"status", "list", p, sha}, []string{"Commit:", "Combined:", "✓", "Statuses (1)\n", "ext/test all green"}}, | |
| 92 | } { | |
| 93 | out := atTerminalIn(t, st, u, root, tc.argv...) | |
| 94 | for _, w := range tc.want { | |
| 95 | if !strings.Contains(out, w) { | |
| 96 | t.Errorf("%v: missing %q in:\n%s", tc.argv, w, out) | |
| 97 | } | |
| 98 | } | |
| 99 | if tc.argv[0] != "feed" { | |
| 100 | checkLegend(t, out) | |
| 101 | } | |
| 102 | } | |
| 103 | } | |
internal/control/status.go +13 −1
| @@ -137,13 +137,25 @@ func runStatusList(c *Ctx, args []string) int { | ||
| 137 | 137 | Combined string `json:"combined"` |
| 138 | 138 | Statuses []out `json:"statuses"` |
| 139 | 139 | }{full, combinedOf(statuses), ds} |
| 140 | return c.emit(d, func(w io.Writer) { | |
| 140 | return c.emitView(d, func(w io.Writer) { | |
| 141 | 141 | tb := c.table(w, "CONTEXT", "STATE", "DESCRIPTION") |
| 142 | 142 | tb.row(cText("combined"), cState(orNone(d.Combined)), cText(fmt.Sprintf("%.10s", d.SHA))) |
| 143 | 143 | for _, x := range ds { |
| 144 | 144 | tb.row(cText(x.Context), cState(x.State), cFlex(x.Description)) |
| 145 | 145 | } |
| 146 | 146 | tb.flush() |
| 147 | }, func() screen { | |
| 148 | combined := []cell{cGlyph(d.Combined), cState(orNone(d.Combined))} | |
| 149 | statuses := section{title: "Statuses", n: len(ds)} | |
| 150 | for _, x := range ds { | |
| 151 | statuses.rows = append(statuses.rows, rowOf(cGlyph(x.State), cText(x.Context), cFlex(x.Description), cMeta(x.Creator))) | |
| 152 | } | |
| 153 | return screen{fields: []field{ | |
| 154 | {"Commit", []cell{cRef(fmt.Sprintf("%.10s", d.SHA))}}, | |
| 155 | {"Combined", combined}, | |
| 156 | }, sections: []section{statuses}, actions: []action{ | |
| 157 | {"Report", []string{"status", "set", repo.Path(), d.SHA, "--context", "<context>", "--state", "success"}}, | |
| 158 | }} | |
| 147 | 159 | }) |
| 148 | 160 | } |
| 149 | 161 | |
internal/control/testdata/plain/build-jobs.txt added +1
| @@ -0,0 +1 @@ | ||
| 1 | test on push | |
internal/control/testdata/plain/feed.txt added +2
| @@ -0,0 +1,2 @@ | ||
| 1 | <time> alice status alice/app | |
| 2 | <time> alice opened issue alice/app#1 | |
internal/control/testdata/plain/issue-list-q.txt added +1
| @@ -0,0 +1 @@ | ||
| 1 | alice/app#1 open Crash on start alice | |
internal/control/testdata/plain/issue-templates.txt added +1
| @@ -0,0 +1 @@ | ||
| 1 | issue-template-bug.md | |
internal/control/testdata/plain/mr-revisions.txt added +1
| @@ -0,0 +1 @@ | ||
| 1 | * v1 2bc3205597 <time> | |
internal/control/testdata/plain/query-list.txt added +1
| @@ -0,0 +1 @@ | ||
| 1 | open is:open | |
internal/control/testdata/plain/query-run.txt added +2
| @@ -0,0 +1,2 @@ | ||
| 1 | alice/app!1 open Fix crash alice | |
| 2 | alice/app#1 open Crash on start alice | |
internal/control/testdata/plain/query-show.txt added +5
| @@ -0,0 +1,5 @@ | ||
| 1 | open | |
| 2 | ||
| 3 | query is:open | |
| 4 | matches 2 | |
| 5 | pinned no | |
internal/control/testdata/plain/search.txt added +2
| @@ -0,0 +1,2 @@ | ||
| 1 | issue alice/app#1 open Crash on start | |
| 2 | mr alice/app!1 open Fix crash | |
internal/control/testdata/plain/status-list.txt added +2
| @@ -0,0 +1,2 @@ | ||
| 1 | combined success 2bc3205597 | |
| 2 | ext/test success all green | |