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 (
66 "fmt"
77 "io"
88 "reflect"
9 "regexp"
910 "strconv"
1011 "strings"
1112
1213 "gitbay.org/gitbay/internal/protocol"
1314)
1415
16// bareWord matches arguments that need no quoting for the server-side
17// POSIX tokenizer.
18var 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.
22func shellWord(arg string) string {
23 if arg != "" && bareWord.MatchString(arg) {
24 return arg
25 }
26 return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
27}
28
1529// Cursor pagination. A cursor is opaque to clients: base64url of
1630// "<kind>:<key>", where key is the sort key of the last row of the
1731// 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))
135149 }
136150 again = append(again, c.Argv[i])
137151 }
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, " "))
139158 })
140159}
internal/control/cursor_test.go +27 −2
@@ -9,6 +9,26 @@ import (
99 "gitbay.org/gitbay/internal/store"
1010)
1111
12func 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
1232func TestCursorRoundTrip(t *testing.T) {
1333 cur := encodeCursor("issue", "42")
1434 key, err := decodeCursor("issue", cur)
@@ -60,8 +80,13 @@ func TestEmitPageHintsTheNextPageAtATerminal(t *testing.T) {
6080 if strings.Contains(c.Stdout.(*bytes.Buffer).String(), "next\t") {
6181 t.Errorf("cursor row on stdout at a terminal")
6282 }
83 stderr := errOut.String()
6384 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)
6691 }
6792}