Commit 7000aa47b1
7000aa47b179487348f77be1510104056de32258
parent: 3d834e058d
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-24 01:16 UTC
control: the next page as a command on stderr at a terminal
Ref #254
internal/control/control.go
+4
| @@ -43,6 +43,9 @@ type Ctx struct { |
| 43 | 43 | // Cmd is the command being run, set by Dispatch, so a usage error can |
| 44 | 44 | // print the registered usage rather than a copy of it. |
| 45 | 45 | Cmd Command |
| 46 | // Argv is the command's arguments after its path, global flags |
| 47 | // removed, so output can print a command to run next. |
| 48 | Argv []string |
| 46 | 49 | // Done, when the surface has one, closes when nobody is reading any |
| 47 | 50 | // more: the SSH channel closed or the HTTP request ended. A command |
| 48 | 51 | // that runs until something happens (build log --follow) stops on it. |
| @@ -142,6 +145,7 @@ func Dispatch(c *Ctx, argv []string) int { |
| 142 | 145 | } |
| 143 | 146 | args = append(args, a) |
| 144 | 147 | } |
| 148 | c.Argv = args |
| 145 | 149 | // A runner-scoped key reaches the runner protocol and nothing else, so |
| 146 | 150 | // the key a CI host holds cannot administer the instance. |
| 147 | 151 | if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { |
internal/control/cursor.go
+14 −1
| @@ -120,8 +120,21 @@ func (c *Ctx) emitPage(p page, items any, next string, plain func(w io.Writer)) |
| 120 | 120 | } |
| 121 | 121 | return c.emit(out{items, next}, func(w io.Writer) { |
| 122 | 122 | plain(w) |
| 123 | | if next != "" { |
| 123 | if next == "" { |
| 124 | return |
| 125 | } |
| 126 | if c.Term.Cols == 0 { |
| 124 | 127 | fmt.Fprintf(w, "next\t%s\n", next) |
| 128 | return |
| 129 | } |
| 130 | var again []string |
| 131 | for i := 0; i < len(c.Argv); i++ { |
| 132 | if c.Argv[i] == "--cursor" { |
| 133 | i++ |
| 134 | continue |
| 135 | } |
| 136 | again = append(again, c.Argv[i]) |
| 125 | 137 | } |
| 138 | fmt.Fprintf(c.Stderr, "more: gitbay %s %s --cursor %s\n", joinPath(c.Cmd.Path), strings.Join(again, " "), next) |
| 126 | 139 | }) |
| 127 | 140 | } |
internal/control/cursor_test.go
+29 −1
| @@ -1,6 +1,13 @@ |
| 1 | 1 | package control |
| 2 | 2 | |
| 3 | | import "testing" |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "gitbay.org/gitbay/internal/protocol" |
| 9 | "gitbay.org/gitbay/internal/store" |
| 10 | ) |
| 4 | 11 | |
| 5 | 12 | func TestCursorRoundTrip(t *testing.T) { |
| 6 | 13 | cur := encodeCursor("issue", "42") |
| @@ -37,3 +44,24 @@ func TestTrimPage(t *testing.T) { |
| 37 | 44 | t.Fatalf("unpaged: %v next=%q", items, next) |
| 38 | 45 | } |
| 39 | 46 | } |
| 47 | |
| 48 | func TestEmitPageHintsTheNextPageAtATerminal(t *testing.T) { |
| 49 | st, repo, uid := newQueueTestRepo(t) |
| 50 | for i := 0; i < 3; i++ { |
| 51 | if _, err := st.CreateBuild(repo.ID, "unit", "aaa", "main", `["true"]`, "", "", true); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | } |
| 55 | c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid}) |
| 56 | c.Term = Term{Cols: 100} |
| 57 | if code := Dispatch(c, []string{"build", "list", repo.Path(), "--limit", "2"}); code != protocol.ExitOK { |
| 58 | t.Fatalf("exit %d: %s", code, errOut) |
| 59 | } |
| 60 | if strings.Contains(c.Stdout.(*bytes.Buffer).String(), "next\t") { |
| 61 | t.Errorf("cursor row on stdout at a terminal") |
| 62 | } |
| 63 | want := "more: gitbay build list " + repo.Path() + " --limit 2 --cursor " |
| 64 | if !strings.Contains(errOut.String(), want) { |
| 65 | t.Errorf("stderr = %q, want %q…", errOut.String(), want) |
| 66 | } |
| 67 | } |