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 | for _, j := range jobs { | 456 | for _, j := range jobs { |
| 457 | out = append(out, JobOut{Name: j.Name, Schedule: j.Schedule, Tags: j.Tags}) | 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 | tb := c.table(w, "NAME", "WHEN") | 460 | tb := c.table(w, "NAME", "WHEN") |
| 461 | for _, j := range out { | 461 | for _, j := range out { |
| 462 | when := "on push" | 462 | when := "on push" |
| @@ -469,6 +469,23 @@ func runBuildJobs(c *Ctx, args []string) int { | |||
| 469 | tb.row(cRef(j.Name), cText(when)) | 469 | tb.row(cRef(j.Name), cText(when)) |
| 470 | } | 470 | } |
| 471 | tb.flush() | 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 | ds := feedOutputs(events) | 358 | ds := feedOutputs(events) |
| 359 | lines := FeedLines(events) | 359 | lines := FeedLines(events) |
| 360 | return c.emitPage(p, ds, next, func(w io.Writer) { | 360 | return c.emitPageView(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 | } | ||
| 369 | tb := c.table(w, "WHEN", "EVENT") | 361 | tb := c.table(w, "WHEN", "EVENT") |
| 370 | for _, l := range lines { | 362 | for _, l := range lines { |
| 371 | tb.row(cAge(l.When), cFlex(l.Sentence())) | 363 | tb.row(cAge(l.When), cFlex(l.Sentence())) |
| 372 | } | 364 | } |
| 373 | tb.flush() | 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 | return s | 42 | return s |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | // feedHeader and termCells are a feed line as a terminal table shows | 45 | // termCells is a feed line as a screen row shows it: when, who, what, |
| 46 | // it: who, what, where, and how a build came out, so the outcome and the | 46 | // where, and how a build came out, so the outcome and the reference can |
| 47 | // reference can carry their colours. | 47 | // carry their colours. |
| 48 | var feedHeader = []string{"WHEN", "WHO", "EVENT", "WHERE", "DETAIL"} | ||
| 49 | |||
| 50 | func (l FeedLine) termCells(c *Ctx) []cell { | 48 | func (l FeedLine) termCells(c *Ctx) []cell { |
| 51 | ref := l.Ref | 49 | ref := l.Ref |
| 52 | if ref != "" && ref[0] != '#' && ref[0] != '!' { | 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 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) | 294 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) |
| 295 | ts := IssueTemplates(dir, repo.DefaultBranch) | 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 | tb := c.table(w, "NAME") | 297 | tb := c.table(w, "NAME") |
| 298 | for _, t := range ts { | 298 | for _, t := range ts { |
| 299 | tb.row(cRef(t.Name)) | 299 | tb.row(cRef(t.Name)) |
| 300 | } | 300 | } |
| 301 | tb.flush() | 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 | if err != nil { | 2089 | if err != nil { |
| 2090 | return c.fail(protocol.ExitFailure, "%v", err) | 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 | tb := c.table(w, "REV", "SHA", "WHEN") | 2093 | tb := c.table(w, "REV", "SHA", "WHEN") |
| 2094 | for _, r := range revs { | 2094 | for _, r := range revs { |
| 2095 | mark := " " | 2095 | mark := " " |
| @@ -2103,6 +2103,22 @@ func runMRRevisions(c *Ctx, args []string) int { | |||
| 2103 | fmt.Fprintf(c.Stderr, "only one revision; %s!%d has not been pushed to since it was opened\n", | 2103 | fmt.Fprintf(c.Stderr, "only one revision; %s!%d has not been pushed to since it was opened\n", |
| 2104 | repo.Path(), mr.Number) | 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 | // runItemQuery lists what q matches in the tables issues and mrs allow, | 194 | // runItemQuery lists what q matches in the tables issues and mrs allow, |
| 195 | // one page at a time. The output is always the paged shape. | 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 | var after *store.ItemCursor | 197 | var after *store.ItemCursor |
| 198 | if p.key != "" { | 198 | if p.key != "" { |
| 199 | var err error | 199 | var err error |
| @@ -215,7 +215,7 @@ func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page) int { | |||
| 215 | next = encodeItemCursor(items[len(items)-1].Cursor()) | 215 | next = encodeItemCursor(items[len(items)-1].Cursor()) |
| 216 | } | 216 | } |
| 217 | ds := queryItems(items) | 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 | tb := c.table(w, "REF", "STATE", "TITLE", "AUTHOR") | 219 | tb := c.table(w, "REF", "STATE", "TITLE", "AUTHOR") |
| 220 | for _, d := range ds { | 220 | for _, d := range ds { |
| 221 | state := d.State | 221 | state := d.State |
| @@ -225,6 +225,25 @@ func runItemQuery(c *Ctx, q ItemQuery, issues, mrs bool, p page) int { | |||
| 225 | tb.row(cRef(d.Ref()), cState(state), cFlex(d.Title), cText(d.Author)) | 225 | tb.row(cRef(d.Ref()), cState(state), cFlex(d.Title), cText(d.Author)) |
| 226 | } | 226 | } |
| 227 | tb.flush() | 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 | if kind == "mr" && !mrs { | 292 | if kind == "mr" && !mrs { |
| 274 | return c.usageWith("that query matches only issues; use issue list or query run") | 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 | func savedQueryOut(sq store.SavedQuery) SavedQueryOut { | 298 | func savedQueryOut(sq store.SavedQuery) SavedQueryOut { |
| @@ -335,7 +354,7 @@ func runQueryList(c *Ctx, args []string) int { | |||
| 335 | for _, sq := range saved { | 354 | for _, sq := range saved { |
| 336 | ds = append(ds, savedQueryOut(sq)) | 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 | tb := c.table(w, "NAME", "PINNED", "QUERY") | 358 | tb := c.table(w, "NAME", "PINNED", "QUERY") |
| 340 | for _, d := range ds { | 359 | for _, d := range ds { |
| 341 | pinned := "" | 360 | pinned := "" |
| @@ -345,6 +364,23 @@ func runQueryList(c *Ctx, args []string) int { | |||
| 345 | tb.row(cRef(d.Name), cText(pinned), cFlex(d.Query)) | 364 | tb.row(cRef(d.Name), cText(pinned), cFlex(d.Query)) |
| 346 | } | 365 | } |
| 347 | tb.flush() | 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 | d := savedQueryOut(sq) | 399 | d := savedQueryOut(sq) |
| 364 | d.Count = &n | 400 | d.Count = &n |
| 365 | return c.emit(d, func(w io.Writer) { | 401 | return c.emitView(d, func(w io.Writer) { |
| 366 | v := c.view(w) | 402 | v := c.view(w) |
| 367 | v.title(d.Name, "", "") | 403 | v.title(d.Name, "", "") |
| 368 | pinned := "no" | 404 | pinned := "no" |
| @@ -370,6 +406,19 @@ func runQueryShow(c *Ctx, args []string) int { | |||
| 370 | pinned = "yes" | 406 | pinned = "yes" |
| 371 | } | 407 | } |
| 372 | v.fields("query", d.Query, "matches", strconv.Itoa(n), "pinned", pinned) | 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 | if code >= 0 { | 434 | if code >= 0 { |
| 386 | return code | 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 | func runQueryRemove(c *Ctx, args []string) int { | 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 | if err != nil { | 105 | if err != nil { |
| 106 | return c.fail(protocol.ExitFailure, "%v", err) | 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 | // writeSearchTable renders search results: a repo hit has no ref number or | 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 | for _, r := range results { | 120 | for _, r := range results { |
| 119 | switch r.Kind { | 121 | switch r.Kind { |
| 120 | case "repo": | 122 | case "repo": |
| 121 | if c.Term.Cols > 0 { | 123 | tb.row(cText("repo"), cRef(r.Repo), cFlex(r.Title)) |
| 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 | } | ||
| 126 | default: | 124 | default: |
| 127 | page := "issues" | 125 | page := "issues" |
| 128 | if r.Kind == "mr" { | 126 | if r.Kind == "mr" { |
| @@ -134,6 +132,32 @@ func writeSearchTable(c *Ctx, w io.Writer, results []SearchResult) { | |||
| 134 | tb.flush() | 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 | // SearchMarker is the sigil a result's number carries, shared with the web | 161 | // SearchMarker is the sigil a result's number carries, shared with the web |
| 138 | // so a hit reads the same in both places. | 162 | // so a hit reads the same in both places. |
| 139 | func SearchMarker(kind string) string { | 163 | func SearchMarker(kind string) string { |
internal/control/search_test.go −27
| @@ -2,36 +2,9 @@ package control | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | 4 | "bytes" |
| 5 | "strings" | ||
| 6 | "testing" | 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 | // Plain output has no header to align to, so a repo row stays 3 cells — | 8 | // Plain output has no header to align to, so a repo row stays 3 cells — |
| 36 | // bytes must not change from before the terminal fix. | 9 | // bytes must not change from before the terminal fix. |
| 37 | func TestSearchTablePlainRepoRowIsThreeCells(t *testing.T) { | 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 | Combined string `json:"combined"` | 137 | Combined string `json:"combined"` |
| 138 | Statuses []out `json:"statuses"` | 138 | Statuses []out `json:"statuses"` |
| 139 | }{full, combinedOf(statuses), ds} | 139 | }{full, combinedOf(statuses), ds} |
| 140 | return c.emit(d, func(w io.Writer) { | 140 | return c.emitView(d, func(w io.Writer) { |
| 141 | tb := c.table(w, "CONTEXT", "STATE", "DESCRIPTION") | 141 | tb := c.table(w, "CONTEXT", "STATE", "DESCRIPTION") |
| 142 | tb.row(cText("combined"), cState(orNone(d.Combined)), cText(fmt.Sprintf("%.10s", d.SHA))) | 142 | tb.row(cText("combined"), cState(orNone(d.Combined)), cText(fmt.Sprintf("%.10s", d.SHA))) |
| 143 | for _, x := range ds { | 143 | for _, x := range ds { |
| 144 | tb.row(cText(x.Context), cState(x.State), cFlex(x.Description)) | 144 | tb.row(cText(x.Context), cState(x.State), cFlex(x.Description)) |
| 145 | } | 145 | } |
| 146 | tb.flush() | 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 | ||