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. |
| 752 | 752 | The =gitbay= CLI sends a leading =--term=<cols>[,color]= argument on |
| 753 | 753 | the SSH command line when stdout is a terminal (OpenSSH's multiplexed |
| 754 | 754 | sessions, which the CLI uses, do not forward a session's =SetEnv=). |
| 755 | | The server strips =--term=<v>= wherever it appears in the argument |
| 756 | | list. The server then prints: |
| 755 | The argument goes first: the server reads =--term=<v>= only as the |
| 756 | first argument, and ignores it over HTTP. The server then prints: |
| 757 | 757 | |
| 758 | 758 | - lists under a header, padded, fitted to the width (the title or |
| 759 | 759 | description column is cut with =…= first), states in colour, ages as |
| @@ -781,10 +781,11 @@ command. |
| 781 | 781 | |
| 782 | 782 | Stock ssh without the CLI's multiplexing gets the plain output unless |
| 783 | 783 | it passes the same leading argument or sets the environment variable |
| 784 | | sshd is told to accept: |
| 784 | sshd is told to accept. OpenSSH parses options after the host, so the |
| 785 | argument goes after =--=: |
| 785 | 786 | |
| 786 | 787 | #+begin_src sh |
| 787 | | ssh git@gitbay.org --term=120,color issue list krz/gitbay |
| 788 | ssh git@gitbay.org -- --term=120,color issue list krz/gitbay |
| 788 | 789 | ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay |
| 789 | 790 | #+end_src |
| 790 | 791 | |
CHANGELOG.org
+6 −5
| @@ -30,11 +30,12 @@ terminal size. |
| 30 | 30 | moved to stderr. =notifications device add= prints =registered |
| 31 | 31 | device <n>=. =dashboard= prints =none= under an empty section. |
| 32 | 32 | |
| 33 | | The CLI sends a leading =--term=<cols>[,color]= argument on the SSH |
| 34 | | command line, since OpenSSH's multiplexed sessions do not forward a |
| 35 | | session's =SetEnv=; the server strips it wherever it appears. Stock |
| 36 | | ssh opts in with =ssh git@gitbay.org --term=120,color issue list |
| 37 | | krz/gitbay= or =-o SetEnv=GITBAY_TERM=120,color=. Operators running |
| 33 | The CLI sends =--term=<cols>[,color]= as the first argument on the |
| 34 | SSH command line, since OpenSSH's multiplexed sessions do not forward |
| 35 | a session's =SetEnv=; the server reads it only there, and ignores it |
| 36 | over HTTP. Stock ssh opts in with =ssh git@gitbay.org -- --term=120,color |
| 37 | issue list krz/gitbay= (the =--= keeps ssh from reading it as its own |
| 38 | option) or =-o SetEnv=GITBAY_TERM=120,color=. Operators running |
| 38 | 39 | the system-sshd forced command add =AcceptEnv GITBAY_TERM= to |
| 39 | 40 | =sshd_config= for the =SetEnv= form; the =--term= argument needs no |
| 40 | 41 | sshd configuration. |
internal/control/control.go
+8 −13
| @@ -123,17 +123,16 @@ func Dispatch(c *Ctx, argv []string) int { |
| 123 | 123 | // A leading --term=<v> selects terminal output for this session, the |
| 124 | 124 | // same as GITBAY_TERM. It must come off before Lookup: Lookup matches |
| 125 | 125 | // 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) |
| 131 | 131 | } |
| 132 | | c.Term = ParseTerm(v) |
| 133 | 132 | 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 | } |
| 137 | 136 | } |
| 138 | 137 | cmd, rest, ok := Lookup(argv) |
| 139 | 138 | c.Cmd = cmd |
| @@ -149,10 +148,6 @@ func Dispatch(c *Ctx, argv []string) int { |
| 149 | 148 | c.JSON = true |
| 150 | 149 | continue |
| 151 | 150 | } |
| 152 | | if v, ok := strings.CutPrefix(a, "--term="); ok { |
| 153 | | c.Term = ParseTerm(v) |
| 154 | | continue |
| 155 | | } |
| 156 | 151 | args = append(args, a) |
| 157 | 152 | } |
| 158 | 153 | c.Argv = args |
internal/control/control_test.go
+26
| @@ -276,3 +276,29 @@ func TestArgumentRefusalsNameTheUsage(t *testing.T) { |
| 276 | 276 | } |
| 277 | 277 | } |
| 278 | 278 | } |
| 279 | |
| 280 | // TestTermArgument: --term= is read only as the first argument, and |
| 281 | // never over HTTP. |
| 282 | func 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 | } |