Commit 9594a7a725
9594a7a725d9d8bb255960cfdb5dffb71fb26919
parent: 7000aa47b1
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-24 01:19 UTC
control: quote the next-page command
Ref #254
internal/control/cursor.go
+20 −1
| @@ -6,12 +6,26 @@ import ( |
| 6 | 6 | "fmt" |
| 7 | 7 | "io" |
| 8 | 8 | "reflect" |
| 9 | "regexp" |
| 9 | 10 | "strconv" |
| 10 | 11 | "strings" |
| 11 | 12 | |
| 12 | 13 | "gitbay.org/gitbay/internal/protocol" |
| 13 | 14 | ) |
| 14 | 15 | |
| 16 | // bareWord matches arguments that need no quoting for the server-side |
| 17 | // POSIX tokenizer. |
| 18 | var bareWord = regexp.MustCompile(`^[A-Za-z0-9@%+=:,./_!-]+$`) |
| 19 | |
| 20 | // shellWord quotes one argument the way the gitbay CLI does, so a |
| 21 | // printed command can be pasted. |
| 22 | func shellWord(arg string) string { |
| 23 | if arg != "" && bareWord.MatchString(arg) { |
| 24 | return arg |
| 25 | } |
| 26 | return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'" |
| 27 | } |
| 28 | |
| 15 | 29 | // Cursor pagination. A cursor is opaque to clients: base64url of |
| 16 | 30 | // "<kind>:<key>", where key is the sort key of the last row of the |
| 17 | 31 | // previous page. The kind keeps a cursor minted by one command from |
| @@ -135,6 +149,11 @@ func (c *Ctx) emitPage(p page, items any, next string, plain func(w io.Writer)) |
| 135 | 149 | } |
| 136 | 150 | again = append(again, c.Argv[i]) |
| 137 | 151 | } |
| 138 | | fmt.Fprintf(c.Stderr, "more: gitbay %s %s --cursor %s\n", joinPath(c.Cmd.Path), strings.Join(again, " "), next) |
| 152 | cmd := []string{"gitbay", joinPath(c.Cmd.Path)} |
| 153 | for _, a := range again { |
| 154 | cmd = append(cmd, shellWord(a)) |
| 155 | } |
| 156 | cmd = append(cmd, "--cursor", next) |
| 157 | fmt.Fprintf(c.Stderr, "more: %s\n", strings.Join(cmd, " ")) |
| 139 | 158 | }) |
| 140 | 159 | } |
internal/control/cursor_test.go
+27 −2
| @@ -9,6 +9,26 @@ import ( |
| 9 | 9 | "gitbay.org/gitbay/internal/store" |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | func TestShellWord(t *testing.T) { |
| 13 | tests := []struct { |
| 14 | input string |
| 15 | want string |
| 16 | }{ |
| 17 | {"plain", "plain"}, |
| 18 | {"foo-bar", "foo-bar"}, |
| 19 | {"123", "123"}, |
| 20 | {"foo bar", "'foo bar'"}, |
| 21 | {"it's", "'it'\\''s'"}, |
| 22 | {"a'b'c", "'a'\\''b'\\''c'"}, |
| 23 | {"", "''"}, |
| 24 | } |
| 25 | for _, tt := range tests { |
| 26 | if got := shellWord(tt.input); got != tt.want { |
| 27 | t.Errorf("shellWord(%q) = %q, want %q", tt.input, got, tt.want) |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | |
| 12 | 32 | func TestCursorRoundTrip(t *testing.T) { |
| 13 | 33 | cur := encodeCursor("issue", "42") |
| 14 | 34 | key, err := decodeCursor("issue", cur) |
| @@ -60,8 +80,13 @@ func TestEmitPageHintsTheNextPageAtATerminal(t *testing.T) { |
| 60 | 80 | if strings.Contains(c.Stdout.(*bytes.Buffer).String(), "next\t") { |
| 61 | 81 | t.Errorf("cursor row on stdout at a terminal") |
| 62 | 82 | } |
| 83 | stderr := errOut.String() |
| 63 | 84 | 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) |
| 85 | if !strings.Contains(stderr, want) { |
| 86 | t.Errorf("stderr = %q, want %q…", stderr, want) |
| 87 | } |
| 88 | // Ensure no double space in the output. |
| 89 | if strings.Contains(stderr, " ") { |
| 90 | t.Errorf("stderr contains double space: %q", stderr) |
| 66 | 91 | } |
| 67 | 92 | } |