Commit f513f70f9a

f513f70f9aea8b921d1d86e52df5f3fd304f88be

parent: 1712c81b0a

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-24 04:30 UTC

control: --term only first, never over HTTP

Ref #254
.gitbay/wiki/Users.org +5 −4
@@ -752,8 +752,8 @@ terminal, and follows these rules so every noun reads the same way.
752752The =gitbay= CLI sends a leading =--term=<cols>[,color]= argument on
753753the SSH command line when stdout is a terminal (OpenSSH's multiplexed
754754sessions, which the CLI uses, do not forward a session's =SetEnv=).
755The server strips =--term=<v>= wherever it appears in the argument
756list. The server then prints:
755The argument goes first: the server reads =--term=<v>= only as the
756first argument, and ignores it over HTTP. The server then prints:
757757
758758- lists under a header, padded, fitted to the width (the title or
759759 description column is cut with =…= first), states in colour, ages as
@@ -781,10 +781,11 @@ command.
781781
782782Stock ssh without the CLI's multiplexing gets the plain output unless
783783it passes the same leading argument or sets the environment variable
784sshd is told to accept:
784sshd is told to accept. OpenSSH parses options after the host, so the
785argument goes after =--=:
785786
786787#+begin_src sh
787ssh git@gitbay.org --term=120,color issue list krz/gitbay
788ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
788789ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
789790#+end_src
790791
CHANGELOG.org +6 −5
@@ -30,11 +30,12 @@ terminal size.
3030 moved to stderr. =notifications device add= prints =registered
3131 device <n>=. =dashboard= prints =none= under an empty section.
3232
33The CLI sends a leading =--term=<cols>[,color]= argument on the SSH
34command line, since OpenSSH's multiplexed sessions do not forward a
35session's =SetEnv=; the server strips it wherever it appears. Stock
36ssh opts in with =ssh git@gitbay.org --term=120,color issue list
37krz/gitbay= or =-o SetEnv=GITBAY_TERM=120,color=. Operators running
33The CLI sends =--term=<cols>[,color]= as the first argument on the
34SSH command line, since OpenSSH's multiplexed sessions do not forward
35a session's =SetEnv=; the server reads it only there, and ignores it
36over HTTP. Stock ssh opts in with =ssh git@gitbay.org -- --term=120,color
37issue list krz/gitbay= (the =--= keeps ssh from reading it as its own
38option) or =-o SetEnv=GITBAY_TERM=120,color=. Operators running
3839the system-sshd forced command add =AcceptEnv GITBAY_TERM= to
3940=sshd_config= for the =SetEnv= form; the =--term= argument needs no
4041sshd configuration.
internal/control/control.go +8 −13
@@ -123,17 +123,16 @@ func Dispatch(c *Ctx, argv []string) int {
123123 // A leading --term=<v> selects terminal output for this session, the
124124 // same as GITBAY_TERM. It must come off before Lookup: Lookup matches
125125 // argv against a command's Path, and a --term= in front would never
126 // match one.
127 for len(argv) > 0 {
128 v, ok := strings.CutPrefix(argv[0], "--term=")
129 if !ok {
130 break
126 // match one. Over HTTP it is dropped unread: the web and the API
127 // render no terminal.
128 if v, ok := strings.CutPrefix(argv[0], "--term="); ok {
129 if !c.ViaAPI {
130 c.Term = ParseTerm(v)
131131 }
132 c.Term = ParseTerm(v)
133132 argv = argv[1:]
134 }
135 if len(argv) == 0 {
136 return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help")
133 if len(argv) == 0 {
134 return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help")
135 }
137136 }
138137 cmd, rest, ok := Lookup(argv)
139138 c.Cmd = cmd
@@ -149,10 +148,6 @@ func Dispatch(c *Ctx, argv []string) int {
149148 c.JSON = true
150149 continue
151150 }
152 if v, ok := strings.CutPrefix(a, "--term="); ok {
153 c.Term = ParseTerm(v)
154 continue
155 }
156151 args = append(args, a)
157152 }
158153 c.Argv = args
internal/control/control_test.go +26
@@ -276,3 +276,29 @@ func TestArgumentRefusalsNameTheUsage(t *testing.T) {
276276 }
277277 }
278278}
279
280// TestTermArgument: --term= is read only as the first argument, and
281// never over HTTP.
282func TestTermArgument(t *testing.T) {
283 cases := []struct {
284 name string
285 viaAPI bool
286 argv []string
287 want Term
288 wantArgv []string
289 }{
290 {"leading", false, []string{"--term=80,color", "issue", "list", "a/b"}, Term{Cols: 80, Color: true}, []string{"a/b"}},
291 {"later", false, []string{"issue", "list", "a/b", "--term=x"}, Term{}, []string{"a/b", "--term=x"}},
292 {"over HTTP", true, []string{"--term=80,color", "issue", "list", "a/b"}, Term{}, []string{"a/b"}},
293 }
294 for _, tc := range cases {
295 c := &Ctx{Scope: "git", ViaAPI: tc.viaAPI, Stdout: io.Discard, Stderr: io.Discard}
296 Dispatch(c, tc.argv)
297 if c.Term != tc.want {
298 t.Errorf("%s: Term %+v, want %+v", tc.name, c.Term, tc.want)
299 }
300 if !slices.Equal(c.Argv, tc.wantArgv) {
301 t.Errorf("%s: Argv %q, want %q", tc.name, c.Argv, tc.wantArgv)
302 }
303 }
304}