CLI output: docs, review fixes, changelog !471

merged merged by cmc on 2026-09-24 15:08 UTC · krz/gitbay:cli-output-docs into main

24 files changed, +516 −278

Layout: unified · split

.gitbay/wiki/Admin.org +4
@@ -39,6 +39,10 @@ the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports
3939 sshd requires that binary to be root-owned and not group/world
4040 writable. Unknown keys fail authentication inside sshd, so system mode
4141 requires =registration.mode = "closed"= (check-config enforces this).
42 Under the forced command the CLI's leading =--term=<cols>[,color]=
43 argument works as is; a client that sets =GITBAY_TERM= instead needs
44 =AcceptEnv GITBAY_TERM= in =sshd_config=, since that env request is
45 handled by the host's sshd, not gitbayd.
4246
4347* Configuration reference
4448
.gitbay/wiki/Users.org +45 −4
@@ -727,10 +727,9 @@ terminal, and follows these rules so every noun reads the same way.
727727
728728- A list command prints one row per item, tab-separated, no header.
729729 Columns run identifier, state, then description; a trailing column
730 may carry a word (=due 2027-01-01=, =via team=). The =gitbay= CLI pads
731 the tabs into aligned columns when stdout is a terminal and leaves
732 them as tabs when piped, so =cut -f= sees the same bytes stock ssh
733 prints. Under =--json= nothing is touched.
730 may carry a word (=due 2027-01-01=, =via team=). Piped, a timestamp in
731 a row is RFC3339 to the second, UTC. Under =--json= nothing is
732 touched.
734733- An empty list prints nothing on stdout and =nothing to list= on
735734 stderr.
736735- A mutation prints one line: verb, object, identifier
@@ -748,6 +747,48 @@ terminal, and follows these rules so every noun reads the same way.
748747 something to them (a key, a member, a label on an issue), =set= for
749748 a value, =revoke= for a credential, =show= and =list= for reads.
750749
750** At a terminal
751
752The =gitbay= CLI sends a leading =--term=<cols>[,color]= argument on
753the SSH command line when stdout is a terminal (OpenSSH's multiplexed
754sessions, which the CLI uses, do not forward a session's =SetEnv=).
755The argument goes first: the server reads =--term=<v>= only as the
756first argument, and ignores it over HTTP. The server then prints:
757
758- lists under a header, padded, fitted to the width (the title or
759 description column is cut with =…= first), states in colour, ages as
760 =2h ago=, and the next page as a command on stderr (piped output
761 keeps a =next\t<cursor>= row instead);
762- =show= views with a title line, aligned fields, the body rendered
763 from markdown or org, one line per event, and comments under a rule;
764 a sub-table (labels, revisions, and the like) carries a section label
765 line in both terminal and piped output; timestamps as
766 =2026-09-23 23:26 UTC=. =wiki show= piped prints the page source
767 verbatim; the rendered page is terminal-only;
768- help with flag descriptions, defaults, and examples; =gitbay --help=
769 groups commands under WORK, REPOSITORIES, YOU and INSTANCE;
770 =help --json= adds =flags= and =examples=.
771
772=NO_COLOR=, =TERM=dumb= and =--no-color= drop the colour. =show=,
773=diff= and =log= at a terminal go through =$GITBAY_PAGER=, else
774=$PAGER=, else =less= (with =LESS=FRX= when =LESS= is unset); an empty
775=GITBAY_PAGER= turns paging off. Paging never applies to =--follow=,
776=--json=, or piped output.
777
778=GITBAY_TERM=off= stops the CLI sending =--term= at all, for an
779instance older than v1.36.0, which refuses the argument as an unknown
780command.
781
782Stock ssh without the CLI's multiplexing gets the plain output unless
783it passes the same leading argument or sets the environment variable
784sshd is told to accept. OpenSSH parses options after the host, so the
785argument goes after =--=:
786
787#+begin_src sh
788ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
789ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
790#+end_src
791
751792* Scripting
752793
753794Every read command takes =--json= and emits one envelope:
CHANGELOG.org +36
@@ -4,6 +4,42 @@ Versioning follows semver from v0.1.0. Database migrations run
44automatically on daemon start; upgrade notes appear per release when
55anything beyond "replace the binary and restart" is needed.
66
7* v1.36.0 — 2026-09-23
8
9Terminal output for the CLI (#254).
10
11*Upgrade note.* Upgrade the instance before the CLI: an older server
12refuses the CLI's leading =--term= argument as an unknown command.
13Against an older instance, set =GITBAY_TERM=off= and the CLI sends no
14terminal size.
15
16- At a terminal, lists print under a header, fitted to the width, with
17 states in colour and relative ages; the next page is a command on
18 stderr. Piped output keeps the same tab-separated rows, with
19 timestamps as RFC3339 to the second; =repo log= rows are
20 tab-separated (were fixed-width) and =repo settings show= keys print
21 as fields (=require mr=, not =require_mr:=).
22- =show= commands print a title line, aligned fields, the body
23 rendered from markdown or org, events one per line and comments
24 under a rule, through a pager when longer than the screen.
25- Help describes every flag, with defaults and examples, and
26 =gitbay --help= groups the commands under WORK, REPOSITORIES, YOU
27 and INSTANCE. =help --json= adds =flags= and =examples=.
28- =release list= takes =--limit= and =--cursor=, and leaves the title
29 empty when it repeats the tag. =mr revisions='s one-revision note
30 moved to stderr. =notifications device add= prints =registered
31 device <n>=. =dashboard= prints =none= under an empty section.
32
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
39the system-sshd forced command add =AcceptEnv GITBAY_TERM= to
40=sshd_config= for the =SetEnv= form; the =--term= argument needs no
41sshd configuration.
42
743* v1.35.1 — 2026-09-23
844
945The landing page's recording (#253).
cmd/gitbay/ssh.go +3 −2
@@ -114,9 +114,10 @@ var noColor bool
114114
115115// termValue is GITBAY_TERM for this invocation: the terminal's width,
116116// and whether colour is wanted. Empty when stdout is not a terminal,
117// so piped output stays the rows stock ssh prints.
117// so piped output stays the rows stock ssh prints, and when GITBAY_TERM
118// is "off", for an instance older than --term.
118119func termValue(isTerminal bool, cols int, env func(string) string) string {
119 if !isTerminal || cols < 40 {
120 if !isTerminal || cols < 40 || env("GITBAY_TERM") == "off" {
120121 return ""
121122 }
122123 v := strconv.Itoa(cols)
cmd/gitbay/summaries_gen.go +231 −231
@@ -3,236 +3,236 @@
33package main
44
55var summaries = map[string]string{
6 "account export": "write your account bundle (profile, repos, issues, MRs) as JSON",
7 "account import-bundle": "replay an account bundle (see gitbay migrate)",
8 "admin email verify": "mark an address verified by admin assertion",
9 "admin invite": "issue a registration invite and mail its code",
10 "admin mr prune": "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)",
11 "admin repo archive": "archive any repository (instance admins; audited)",
12 "admin repo delete": "delete any repository (instance admins; audited)",
13 "admin repo list": "list every repository with size and last push (instance admins)",
14 "admin repo unarchive": "unarchive any repository (instance admins; audited)",
15 "admin repo visibility": "set any repository's visibility (instance admins; audited)",
16 "admin runners forget": "alias of admin runners remove",
17 "admin runners remove": "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
18 "admin runners": "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
19 "admin stats": "instance statistics: counts and per-repository disk usage",
20 "admin user create": "create an account, optionally with a key and a verified address (instance admins)",
21 "admin user delete": "delete an account that anchors nothing (keys, emails and sessions go with it)",
22 "admin user demote": "remove instance admin from an account (never the last one)",
23 "admin user disable": "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
24 "admin user enable": "restore a suspended account",
25 "admin user limits": "show or set an account's repository and storage caps (instance admins)",
26 "admin user list": "list accounts (instance admins)",
27 "admin user promote": "make an account an instance admin",
28 "admin user show": "show an account: keys, emails, orgs, tokens, sessions (instance admins)",
29 "audit": "instance audit log (admins)",
30 "build cancel": "withdraw a queued build before a runner claims it",
31 "build jobs": "list the jobs a trigger can name",
32 "build list": "list recent builds",
33 "build log": "print a build's log, or follow it until the build ends",
34 "build show": "show one build",
35 "build trigger": "queue a job now (scheduled or not)",
36 "dashboard": "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
37 "email add": "add an address and mail a verification code",
38 "email list": "list the addresses on your account",
39 "email primary": "make a verified address the primary",
40 "email remove": "remove an address; not the primary, nor the last verified one",
41 "email verify": "confirm a verification code",
42 "explore": "list public repositories",
43 "feed": "activity on repositories you can reach",
44 "help": "list available commands",
45 "issue assign": "assignees",
46 "issue close": "close an issue",
47 "issue comment": "comment",
48 "issue create": "open an issue",
49 "issue edit": "edit title or body",
50 "issue label": "labels",
51 "issue list": "list issues",
52 "issue milestone": "set or clear an issue's milestone",
53 "issue reopen": "reopen an issue",
54 "issue show": "show an issue with comments",
55 "issue templates": "list issue templates (.gitbay/issue-template*.md)",
56 "keys add": "register an SSH public key (authorized_keys format)",
57 "keys label": "name a key; an empty label clears it",
58 "keys list": "list registered SSH keys",
59 "keys remove": "remove an SSH key by fingerprint",
60 "label list": "list a repository's labels with colour and use",
61 "label remove": "remove a label from the repository and from every issue and merge request",
62 "label set": "create a label or set its colour",
63 "milestone close": "close a milestone",
64 "milestone create": "create a milestone",
65 "milestone list": "list milestones with progress",
66 "milestone reopen": "reopen a milestone",
67 "mr close": "close without merging",
68 "mr comment": "comment",
69 "mr create": "open a merge request",
70 "mr diff": "show the diff",
71 "mr diff-comment": "comment on a diff line",
72 "mr draft": "mark a merge request as work in progress",
73 "mr edit": "edit title or body",
74 "mr label": "labels",
75 "mr list": "list merge requests",
76 "mr merge": "merge",
77 "mr milestone": "set or clear an MR's milestone",
78 "mr range-diff": "what changed between two revisions of a merge request",
79 "mr ready": "take the draft mark off, so it can merge",
80 "mr resolve": "resolve a review thread",
81 "mr retarget": "retarget onto another branch",
82 "mr review request": "ask specific people for a review",
83 "mr review": "review",
84 "mr revisions": "the heads a merge request has had",
85 "mr show": "show a merge request",
86 "mr threads": "review threads on an MR",
87 "mr unresolve": "reopen a review thread",
88 "notifications device add": "register an Apple device for push, token on stdin",
89 "notifications device list": "your registered devices",
90 "notifications device remove": "deregister a device",
91 "notifications list": "your notification inbox, newest first",
92 "notifications read": "mark notifications read",
93 "notifications settings mail": "activity by mail as well as the inbox (login links are unaffected)",
94 "notifications settings push": "activity on your registered devices as well as the inbox",
95 "notifications settings show": "your notification preferences",
96 "notifications settings watch": "every issue and merge request on repositories you can write to",
97 "org create": "create an organization (you become its first admin)",
98 "org delete": "delete an empty organization",
99 "org label list": "list an org's labels with use across the repositories you can read",
100 "org label remove": "remove an org label from the org and from every issue under it",
101 "org label set": "create an org label every org repository sees, or set its colour; folds in same-named repo labels",
102 "org list": "list organizations you belong to",
103 "org members add": "add or update a member",
104 "org members list": "list members",
105 "org members remove": "remove a member",
106 "org milestone close": "close an org milestone",
107 "org milestone create": "create an org milestone spanning every org repository; folds in same-titled repo milestones",
108 "org milestone list": "list an org's milestones with progress across the repositories you can read",
109 "org milestone reopen": "reopen an org milestone",
110 "org profile": "show or set an org's profile",
111 "org rename": "rename an organization",
112 "org settings members-role": "role plain membership implies on every org repo",
113 "org show": "show an organization and its members",
114 "org team add": "add org members to a team",
115 "org team create": "create a team",
116 "org team delete": "delete a team (its grants with it)",
117 "org team grant": "grant a team a role on an org repo",
118 "org team list": "list an org's teams",
119 "org team remove": "remove members from a team",
120 "org team revoke": "revoke a team's grant",
121 "org team show": "show a team's members and grants",
122 "pgp add": "register an OpenPGP public key (armored)",
123 "pgp list": "list registered OpenPGP keys",
124 "pgp remove": "remove an OpenPGP key by fingerprint",
125 "profile set": "set your profile",
126 "profile show": "show a user's or org's profile",
127 "register": "create an account (only meaningful for unregistered keys)",
128 "release asset add": "upload an asset from stdin",
129 "release asset get": "write an asset to stdout",
130 "release asset remove": "remove an asset",
131 "release create": "create a release on a tag",
132 "release delete": "delete a release and its assets",
133 "release edit": "update a release's title and notes",
134 "release list": "list releases",
135 "release show": "show a release with assets",
136 "repo access grant": "grant access",
137 "repo access list": "list who can reach the repository, with the role and where it comes from",
138 "repo access revoke": "revoke access",
139 "repo archive": "archive a repository (read-only: pushes and issue/MR writes refused)",
140 "repo blame": "attribute lines to commits",
141 "repo bookmark": "bookmark a repository to come back to",
142 "repo bookmarks": "list the repositories you have bookmarked",
143 "repo cat": "read a file",
144 "repo commit": "show one commit with its patch",
145 "repo commit-file": "write a file and commit it",
146 "repo create": "create a repository",
147 "repo delete": "delete a repository",
148 "repo deploy-key add": "bind a read-only (or --rw) key to one repository",
149 "repo deploy-key list": "list deploy keys",
150 "repo deploy-key remove": "remove a deploy key",
151 "repo deps disable": "stop checking dependencies",
152 "repo deps enable": "check dependencies for updates",
153 "repo deps status": "show dependency check state",
154 "repo diff": "the patch between two refs, from their merge base",
155 "repo domain add": "claim a custom pages domain (verify with a DNS TXT record)",
156 "repo domain list": "list custom pages domains",
157 "repo domain remove": "remove a custom pages domain",
158 "repo domain verify": "check the DNS challenge and activate a claim",
159 "repo download": "write a tar.gz of a ref to stdout",
160 "repo fork": "fork a repository under your account",
161 "repo grep": "search file contents",
162 "repo import": "server-side mirror of a foreign repository",
163 "repo import-issues": "import issue and PR history from GitHub or Forgejo",
164 "repo list": "list repositories you own or can access",
165 "repo log": "commit log with signature states",
166 "repo mirror add": "mirror to or from a remote",
167 "repo mirror list": "list mirrors with sync status",
168 "repo mirror remove": "remove a mirror",
169 "repo mirror sync": "schedule an immediate sync",
170 "repo mute": "mute a repository, including work you are part of",
171 "repo pin": "pin a repository to your dashboard",
172 "repo refs": "list branches and tags",
173 "repo rename": "rename a repository",
174 "repo runner add": "attach a runner's public key to a repository",
175 "repo runner list": "list the runners attached to a repository",
176 "repo runner remove": "detach a runner from a repository",
177 "repo search": "find repositories by name, description, or topic",
178 "repo secret list": "list build secret names",
179 "repo secret remove": "remove a build secret",
180 "repo secret set": "set a build secret",
181 "repo settings default-branch": "set the default branch",
182 "repo settings description": "set the repository description",
183 "repo settings git-daemon": "expose over git://",
184 "repo settings protect": "protect a branch",
185 "repo settings protect-tag": "protect tags matching a glob (created once, never moved or deleted)",
186 "repo settings require-approvals": "require N fresh approvals to merge",
187 "repo settings require-checks": "gate merges on green statuses",
6 "account export": "write your account bundle (profile, repos, issues, MRs) as JSON",
7 "account import-bundle": "replay an account bundle (see gitbay migrate)",
8 "admin email verify": "mark an address verified by admin assertion",
9 "admin invite": "issue a registration invite and mail its code",
10 "admin mr prune": "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)",
11 "admin repo archive": "archive any repository (instance admins; audited)",
12 "admin repo delete": "delete any repository (instance admins; audited)",
13 "admin repo list": "list every repository with size and last push (instance admins)",
14 "admin repo unarchive": "unarchive any repository (instance admins; audited)",
15 "admin repo visibility": "set any repository's visibility (instance admins; audited)",
16 "admin runners forget": "alias of admin runners remove",
17 "admin runners remove": "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
18 "admin runners": "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
19 "admin stats": "instance statistics: counts and per-repository disk usage",
20 "admin user create": "create an account, optionally with a key and a verified address (instance admins)",
21 "admin user delete": "delete an account that anchors nothing (keys, emails and sessions go with it)",
22 "admin user demote": "remove instance admin from an account (never the last one)",
23 "admin user disable": "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
24 "admin user enable": "restore a suspended account",
25 "admin user limits": "show or set an account's repository and storage caps (instance admins)",
26 "admin user list": "list accounts (instance admins)",
27 "admin user promote": "make an account an instance admin",
28 "admin user show": "show an account: keys, emails, orgs, tokens, sessions (instance admins)",
29 "audit": "instance audit log (admins)",
30 "build cancel": "withdraw a queued build before a runner claims it",
31 "build jobs": "list the jobs a trigger can name",
32 "build list": "list recent builds",
33 "build log": "print a build's log, or follow it until the build ends",
34 "build show": "show one build",
35 "build trigger": "queue a job now (scheduled or not)",
36 "dashboard": "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
37 "email add": "add an address and mail a verification code",
38 "email list": "list the addresses on your account",
39 "email primary": "make a verified address the primary",
40 "email remove": "remove an address; not the primary, nor the last verified one",
41 "email verify": "confirm a verification code",
42 "explore": "list public repositories",
43 "feed": "activity on repositories you can reach",
44 "help": "list available commands",
45 "issue assign": "assignees",
46 "issue close": "close an issue",
47 "issue comment": "comment",
48 "issue create": "open an issue",
49 "issue edit": "edit title or body",
50 "issue label": "labels",
51 "issue list": "list issues",
52 "issue milestone": "set or clear an issue's milestone",
53 "issue reopen": "reopen an issue",
54 "issue show": "show an issue with comments",
55 "issue templates": "list issue templates (.gitbay/issue-template*.md)",
56 "keys add": "register an SSH public key (authorized_keys format)",
57 "keys label": "name a key; an empty label clears it",
58 "keys list": "list registered SSH keys",
59 "keys remove": "remove an SSH key by fingerprint",
60 "label list": "list a repository's labels with colour and use",
61 "label remove": "remove a label from the repository and from every issue and merge request",
62 "label set": "create a label or set its colour",
63 "milestone close": "close a milestone",
64 "milestone create": "create a milestone",
65 "milestone list": "list milestones with progress",
66 "milestone reopen": "reopen a milestone",
67 "mr close": "close without merging",
68 "mr comment": "comment",
69 "mr create": "open a merge request",
70 "mr diff": "show the diff",
71 "mr diff-comment": "comment on a diff line",
72 "mr draft": "mark a merge request as work in progress",
73 "mr edit": "edit title or body",
74 "mr label": "labels",
75 "mr list": "list merge requests",
76 "mr merge": "merge",
77 "mr milestone": "set or clear an MR's milestone",
78 "mr range-diff": "what changed between two revisions of a merge request",
79 "mr ready": "take the draft mark off, so it can merge",
80 "mr resolve": "resolve a review thread",
81 "mr retarget": "retarget onto another branch",
82 "mr review request": "ask specific people for a review",
83 "mr review": "review",
84 "mr revisions": "the heads a merge request has had",
85 "mr show": "show a merge request",
86 "mr threads": "review threads on an MR",
87 "mr unresolve": "reopen a review thread",
88 "notifications device add": "register an Apple device for push, token on stdin",
89 "notifications device list": "your registered devices",
90 "notifications device remove": "deregister a device",
91 "notifications list": "your notification inbox, newest first",
92 "notifications read": "mark notifications read",
93 "notifications settings mail": "activity by mail as well as the inbox (login links are unaffected)",
94 "notifications settings push": "activity on your registered devices as well as the inbox",
95 "notifications settings show": "your notification preferences",
96 "notifications settings watch": "every issue and merge request on repositories you can write to",
97 "org create": "create an organization (you become its first admin)",
98 "org delete": "delete an empty organization",
99 "org label list": "list an org's labels with use across the repositories you can read",
100 "org label remove": "remove an org label from the org and from every issue under it",
101 "org label set": "create an org label every org repository sees, or set its colour; folds in same-named repo labels",
102 "org list": "list organizations you belong to",
103 "org members add": "add or update a member",
104 "org members list": "list members",
105 "org members remove": "remove a member",
106 "org milestone close": "close an org milestone",
107 "org milestone create": "create an org milestone spanning every org repository; folds in same-titled repo milestones",
108 "org milestone list": "list an org's milestones with progress across the repositories you can read",
109 "org milestone reopen": "reopen an org milestone",
110 "org profile": "show or set an org's profile",
111 "org rename": "rename an organization",
112 "org settings members-role": "role plain membership implies on every org repo",
113 "org show": "show an organization and its members",
114 "org team add": "add org members to a team",
115 "org team create": "create a team",
116 "org team delete": "delete a team (its grants with it)",
117 "org team grant": "grant a team a role on an org repo",
118 "org team list": "list an org's teams",
119 "org team remove": "remove members from a team",
120 "org team revoke": "revoke a team's grant",
121 "org team show": "show a team's members and grants",
122 "pgp add": "register an OpenPGP public key (armored)",
123 "pgp list": "list registered OpenPGP keys",
124 "pgp remove": "remove an OpenPGP key by fingerprint",
125 "profile set": "set your profile",
126 "profile show": "show a user's or org's profile",
127 "register": "create an account (only meaningful for unregistered keys)",
128 "release asset add": "upload an asset from stdin",
129 "release asset get": "write an asset to stdout",
130 "release asset remove": "remove an asset",
131 "release create": "create a release on a tag",
132 "release delete": "delete a release and its assets",
133 "release edit": "update a release's title and notes",
134 "release list": "list releases",
135 "release show": "show a release with assets",
136 "repo access grant": "grant access",
137 "repo access list": "list who can reach the repository, with the role and where it comes from",
138 "repo access revoke": "revoke access",
139 "repo archive": "archive a repository (read-only: pushes and issue/MR writes refused)",
140 "repo blame": "attribute lines to commits",
141 "repo bookmark": "bookmark a repository to come back to",
142 "repo bookmarks": "list the repositories you have bookmarked",
143 "repo cat": "read a file",
144 "repo commit": "show one commit with its patch",
145 "repo commit-file": "write a file and commit it",
146 "repo create": "create a repository",
147 "repo delete": "delete a repository",
148 "repo deploy-key add": "bind a read-only (or --rw) key to one repository",
149 "repo deploy-key list": "list deploy keys",
150 "repo deploy-key remove": "remove a deploy key",
151 "repo deps disable": "stop checking dependencies",
152 "repo deps enable": "check dependencies for updates",
153 "repo deps status": "show dependency check state",
154 "repo diff": "the patch between two refs, from their merge base",
155 "repo domain add": "claim a custom pages domain (verify with a DNS TXT record)",
156 "repo domain list": "list custom pages domains",
157 "repo domain remove": "remove a custom pages domain",
158 "repo domain verify": "check the DNS challenge and activate a claim",
159 "repo download": "write a tar.gz of a ref to stdout",
160 "repo fork": "fork a repository under your account",
161 "repo grep": "search file contents",
162 "repo import": "server-side mirror of a foreign repository",
163 "repo import-issues": "import issue and PR history from GitHub or Forgejo",
164 "repo list": "list repositories you own or can access",
165 "repo log": "commit log with signature states",
166 "repo mirror add": "mirror to or from a remote",
167 "repo mirror list": "list mirrors with sync status",
168 "repo mirror remove": "remove a mirror",
169 "repo mirror sync": "schedule an immediate sync",
170 "repo mute": "mute a repository, including work you are part of",
171 "repo pin": "pin a repository to your dashboard",
172 "repo refs": "list branches and tags",
173 "repo rename": "rename a repository",
174 "repo runner add": "attach a runner's public key to a repository",
175 "repo runner list": "list the runners attached to a repository",
176 "repo runner remove": "detach a runner from a repository",
177 "repo search": "find repositories by name, description, or topic",
178 "repo secret list": "list build secret names",
179 "repo secret remove": "remove a build secret",
180 "repo secret set": "set a build secret",
181 "repo settings default-branch": "set the default branch",
182 "repo settings description": "set the repository description",
183 "repo settings git-daemon": "expose over git://",
184 "repo settings protect": "protect a branch",
185 "repo settings protect-tag": "protect tags matching a glob (created once, never moved or deleted)",
186 "repo settings require-approvals": "require N fresh approvals to merge",
187 "repo settings require-checks": "gate merges on green statuses",
188188 "repo settings require-codeowners": "require an owner's approval for every file CODEOWNERS covers",
189 "repo settings require-mr": "protected branches take changes through merge requests only",
190 "repo settings require-resolved": "require all review threads resolved to merge",
191 "repo settings require-signed": "require verified commit signatures",
192 "repo settings show": "show settings",
193 "repo settings unprotect": "unprotect a branch",
194 "repo settings unprotect-tag": "drop a protected-tag glob",
195 "repo settings visibility": "set repository visibility",
196 "repo settings website": "set the repository website",
197 "repo show": "show repository details",
198 "repo topics add": "add topics",
199 "repo topics remove": "remove topics",
200 "repo topics": "list topics",
201 "repo transfer": "move a repository to another owner",
202 "repo tree": "list a directory",
203 "repo unarchive": "unarchive a repository",
204 "repo unbookmark": "remove a bookmark",
205 "repo unpin": "unpin a repository",
206 "repo unwatch": "back to the default: only work you are part of",
207 "repo watch": "hear about all activity on a repository",
208 "runner done": "finish a build",
209 "runner log": "append a build's log from stdin",
210 "runner next": "claim the oldest pending build this key may run (runner protocol)",
211 "search": "find repositories, issues and merge requests across the instance",
212 "snippet create": "create a snippet from one file on stdin",
213 "snippet delete": "delete a snippet and its files",
214 "snippet edit": "change a snippet's description or visibility",
215 "snippet file get": "write a snippet file to stdout",
216 "snippet file remove": "remove a file from a snippet",
217 "snippet file set": "add a file to a snippet, or replace one, from stdin",
218 "snippet list": "list your snippets, or an owner's public ones",
219 "snippet show": "show a snippet's metadata and files",
220 "status list": "statuses on a commit",
221 "status set": "report a commit status (CI)",
222 "token create": "mint an API token (shown once)",
223 "token list": "list API tokens",
224 "token revoke": "revoke an API token by name",
225 "web login": "mint a one-time browser login URL",
226 "web sessions list": "list your browser sessions",
227 "web sessions revoke": "end a browser session, or all of them",
228 "web theme set": "follow the browser's scheme, or force light or dark",
229 "web theme show": "the colour scheme the web UI uses for you",
230 "webhook add": "add a webhook",
231 "webhook deliveries": "recent deliveries",
232 "webhook list": "list webhooks",
233 "webhook redeliver": "queue a delivery again",
234 "webhook remove": "remove a webhook",
235 "whoami": "show the authenticated account",
236 "wiki list": "list a repository's wiki pages",
237 "wiki show": "print a wiki page",
189 "repo settings require-mr": "protected branches take changes through merge requests only",
190 "repo settings require-resolved": "require all review threads resolved to merge",
191 "repo settings require-signed": "require verified commit signatures",
192 "repo settings show": "show settings",
193 "repo settings unprotect": "unprotect a branch",
194 "repo settings unprotect-tag": "drop a protected-tag glob",
195 "repo settings visibility": "set repository visibility",
196 "repo settings website": "set the repository website",
197 "repo show": "show repository details",
198 "repo topics add": "add topics",
199 "repo topics remove": "remove topics",
200 "repo topics": "list topics",
201 "repo transfer": "move a repository to another owner",
202 "repo tree": "list a directory",
203 "repo unarchive": "unarchive a repository",
204 "repo unbookmark": "remove a bookmark",
205 "repo unpin": "unpin a repository",
206 "repo unwatch": "back to the default: only work you are part of",
207 "repo watch": "hear about all activity on a repository",
208 "runner done": "finish a build",
209 "runner log": "append a build's log from stdin",
210 "runner next": "claim the oldest pending build this key may run (runner protocol)",
211 "search": "find repositories, issues and merge requests across the instance",
212 "snippet create": "create a snippet from one file on stdin",
213 "snippet delete": "delete a snippet and its files",
214 "snippet edit": "change a snippet's description or visibility",
215 "snippet file get": "write a snippet file to stdout",
216 "snippet file remove": "remove a file from a snippet",
217 "snippet file set": "add a file to a snippet, or replace one, from stdin",
218 "snippet list": "list your snippets, or an owner's public ones",
219 "snippet show": "show a snippet's metadata and files",
220 "status list": "statuses on a commit",
221 "status set": "report a commit status (CI)",
222 "token create": "mint an API token (shown once)",
223 "token list": "list API tokens",
224 "token revoke": "revoke an API token by name",
225 "web login": "mint a one-time browser login URL",
226 "web sessions list": "list your browser sessions",
227 "web sessions revoke": "end a browser session, or all of them",
228 "web theme set": "follow the browser's scheme, or force light or dark",
229 "web theme show": "the colour scheme the web UI uses for you",
230 "webhook add": "add a webhook",
231 "webhook deliveries": "recent deliveries",
232 "webhook list": "list webhooks",
233 "webhook redeliver": "queue a delivery again",
234 "webhook remove": "remove a webhook",
235 "whoami": "show the authenticated account",
236 "wiki list": "list a repository's wiki pages",
237 "wiki show": "print a wiki page",
238238}
cmd/gitbay/summaries_test.go +7 −2
@@ -3,6 +3,7 @@ package main
33import (
44 "flag"
55 "fmt"
6 "go/format"
67 "os"
78 "slices"
89 "strings"
@@ -25,11 +26,15 @@ func TestSummariesAreCurrent(t *testing.T) {
2526 slices.Sort(lines)
2627 b.WriteString(strings.Join(lines, ""))
2728 b.WriteString("}\n")
29 want, err := format.Source([]byte(b.String()))
30 if err != nil {
31 t.Fatal(err)
32 }
2833 if *updateSummaries {
29 os.WriteFile("summaries_gen.go", []byte(b.String()), 0o644)
34 os.WriteFile("summaries_gen.go", want, 0o644)
3035 }
3136 got, _ := os.ReadFile("summaries_gen.go")
32 if string(got) != b.String() {
37 if string(got) != string(want) {
3338 t.Fatal("summaries_gen.go is stale: go test ./cmd/gitbay -run TestSummariesAreCurrent -update")
3439 }
3540}
cmd/gitbay/term_test.go +1
@@ -22,6 +22,7 @@ func TestTermValue(t *testing.T) {
2222 {true, 120, map[string]string{"NO_COLOR": "1"}, false, "120"},
2323 {true, 120, map[string]string{"TERM": "dumb"}, false, "120"},
2424 {true, 120, nil, true, "120"},
25 {true, 120, map[string]string{"GITBAY_TERM": "off"}, false, ""},
2526 }
2627 for _, c := range cases {
2728 noColor = c.noColor
e2e/readonly_test.go +3 −1
@@ -232,8 +232,10 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) {
232232 }
233233}
234234
235var sgrRe = regexp.MustCompile("\x1b\\[[0-9;]*m")
236
235237func stripSGRe2e(s string) string {
236 return regexp.MustCompile("\x1b\\[[0-9;]*m").ReplaceAllString(s, "")
238 return sgrRe.ReplaceAllString(s, "")
237239}
238240
239241func displayCells(s string) int {
internal/control/adminhost.go +1 −1
@@ -333,7 +333,7 @@ func runAdminStats(c *Ctx, args []string) int {
333333 "lfs", humanBytes(d.LFSBytes),
334334 )
335335 if len(d.Repos) > 0 {
336 io.WriteString(w, "\n")
336 v.section("repos")
337337 tb := c.table(w, "PATH", "BYTES")
338338 for _, r := range d.Repos {
339339 tb.row(cRef(r.Path), cText(humanBytes(r.Bytes)))
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}
internal/control/deps.go +1 −1
@@ -122,7 +122,7 @@ func runDepsStatus(c *Ctx, args []string) int {
122122 "tracked in", tracked,
123123 )
124124 if len(out.Behind) > 0 {
125 io.WriteString(w, "\n")
125 v.section("behind")
126126 tb := c.table(w, "ECOSYSTEM", "NAME", "CURRENT", "LATEST")
127127 for _, b := range out.Behind {
128128 tb.row(cText(b.Ecosystem), cRef(b.Name), cText(b.Current), cText(b.Latest))
internal/control/help.go +2 −1
@@ -112,7 +112,8 @@ func runHelp(c *Ctx, args []string) int {
112112}
113113
114114// program is how help spells the command it documents: the CLI at a
115// terminal (only the CLI sends GITBAY_TERM), ssh otherwise.
115// terminal (only the CLI or a caller passing --term sets one), ssh
116// otherwise.
116117func (c *Ctx) program() string {
117118 if c.Term.Cols > 0 {
118119 return "gitbay"
internal/control/mr.go +1 −1
@@ -790,7 +790,7 @@ func runMRShow(c *Ctx, args []string) int {
790790 v.body(d.Body, d.BodyFormat)
791791
792792 if len(commits) > 1 {
793 io.WriteString(w, "\n")
793 v.section("commit")
794794 tb := c.table(w, "SHA", "SUBJECT")
795795 for _, cm := range commits {
796796 tb.row(cRef(fmt.Sprintf("%.10s", cm.SHA)), cFlex(cm.Subject))
internal/control/org.go +1 −1
@@ -140,7 +140,7 @@ func runOrgShow(c *Ctx, args []string) int {
140140 v := c.view(w)
141141 v.title(d.Org, "", "")
142142 if len(ms) > 0 {
143 io.WriteString(w, "\n")
143 v.section("members")
144144 tb := c.table(w, "USER", "ROLE")
145145 for _, m := range ms {
146146 tb.row(cRef(m.User), cState(m.Role))
internal/control/release.go +1 −1
@@ -310,7 +310,7 @@ func runReleaseShow(c *Ctx, args []string) int {
310310 )
311311 v.body(d.Notes, d.NotesFormat)
312312 if len(d.Assets) > 0 {
313 io.WriteString(w, "\n")
313 v.section("assets")
314314 tb := c.table(w, "NAME", "SIZE", "SHA256")
315315 for _, a := range d.Assets {
316316 tb.row(cRef(a.Name), cNum(a.Size), cFlex(a.SHA256))
internal/control/snippet.go +1 −1
@@ -231,7 +231,7 @@ func runSnippetShow(c *Ctx, args []string) int {
231231 "url", snippetURL(c, sn),
232232 )
233233 if len(files) > 0 {
234 io.WriteString(w, "\n")
234 v.section("files")
235235 tb := c.table(w, "NAME", "SIZE")
236236 for _, f := range files {
237237 tb.row(cRef(f.Name), cText(fmt.Sprintf("%d bytes", f.Size)))
internal/control/table.go +1
@@ -61,6 +61,7 @@ func (t *table) row(cs ...cell) {
6161 }
6262 now := termNow()
6363 for i := range cs {
64 cs[i].s = termSafe(cs[i].s)
6465 if cs[i].kind == kindAge {
6566 cs[i].s = relAge(cs[i].s, now)
6667 }
internal/control/teams.go +1 −1
@@ -190,7 +190,7 @@ func runTeamShow(c *Ctx, args []string) int {
190190 v.title(org.Name+"/"+team.Name, "", "")
191191 v.fields("members", strings.Join(members, ", "))
192192 if len(grants) > 0 {
193 io.WriteString(w, "\n")
193 v.section("grants")
194194 tb := c.table(w, "REPO", "ROLE")
195195 for _, g := range grants {
196196 tb.row(cRef(g.RepoPath), cState(g.Role))
internal/control/term.go +38 −7
@@ -37,15 +37,46 @@ func ParseTerm(v string) Term {
3737}
3838
3939const (
40 sgrReset = "\x1b[0m"
41 sgrBold = "\x1b[1m"
42 sgrDim = "\x1b[2m"
43 sgrUnderline = "\x1b[4m"
44 sgrRed = "\x1b[31m"
45 sgrGreen = "\x1b[32m"
46 sgrMagenta = "\x1b[35m"
40 sgrReset = "\x1b[0m"
41 sgrBold = "\x1b[1m"
42 sgrDim = "\x1b[2m"
43 sgrRed = "\x1b[31m"
44 sgrGreen = "\x1b[32m"
45 sgrMagenta = "\x1b[35m"
4746)
4847
48// termSafe replaces the bytes a terminal would act on — ESC, the C0
49// controls but tab and newline, DEL, and the C1 controls — with U+FFFD,
50// so user text cannot move the cursor, set the clipboard (OSC 52) or
51// clear the screen. Carriage return is dropped rather than replaced:
52// web forms store CRLF line endings, and a lone CR would let text
53// overwrite its own line. Terminal output only: plain output is unchanged.
54func termSafe(s string) string {
55 unsafe := func(r rune) bool {
56 return (r < 0x20 && r != '\t' && r != '\n') || (r >= 0x7f && r <= 0x9f)
57 }
58 if strings.IndexFunc(s, unsafe) < 0 {
59 return s
60 }
61 return strings.Map(func(r rune) rune {
62 switch {
63 case r == '\r':
64 return -1
65 case unsafe(r):
66 return '\uFFFD'
67 }
68 return r
69 }, s)
70}
71
72// safe is termSafe at a terminal and s unchanged in plain output.
73func (t Term) safe(s string) string {
74 if t.Cols == 0 {
75 return s
76 }
77 return termSafe(s)
78}
79
4980// paint wraps s in an SGR sequence when colour is on.
5081func (t Term) paint(sgr, s string) string {
5182 if !t.Color || sgr == "" || s == "" {
internal/control/term_test.go +6 −6
@@ -26,12 +26,12 @@ func TestParseTerm(t *testing.T) {
2626
2727func TestCells(t *testing.T) {
2828 cases := map[string]int{
29 "abc": 3,
30 "日本": 4,
31 "é": 1,
32 "é": 1,
33 "\x1b[32mopen\x1b[0m": 4,
34 "": 0,
29 "abc": 3,
30 "日本": 4,
31 "é": 1,
32 "é": 1,
33 "\x1b[32mopen\x1b[0m": 4,
34 "": 0,
3535 }
3636 for in, want := range cases {
3737 if got := cells(in); got != want {
internal/control/termsafe_test.go added +89
@@ -0,0 +1,89 @@
1package control
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/protocol"
9 "gitbay.org/gitbay/internal/store"
10)
11
12const evil = "x\x1b]52;c;ZXZpbA==\x07y\x1b[2Jz"
13
14func TestTermSafe(t *testing.T) {
15 cases := map[string]string{
16 "plain\ttext\n": "plain\ttext\n",
17 "a\x1bb": "a�b",
18 "a\x00c\x7fd": "a�c�d",
19 "a\r\nb\r\n": "a\nb\n",
20 "a\rb": "ab",
21 "a\u0085b\u009bc": "a�b�c",
22 "ümlaut": "ümlaut",
23 }
24 for in, want := range cases {
25 if got := termSafe(in); got != want {
26 t.Errorf("termSafe(%q) = %q, want %q", in, got, want)
27 }
28 }
29}
30
31func noControls(t *testing.T, what, out string) {
32 t.Helper()
33 s := stripSGR(out)
34 if strings.ContainsAny(s, "\x1b\x07") {
35 t.Errorf("%s: control bytes reach the terminal: %q", what, s)
36 }
37}
38
39func TestTableRowControlBytes(t *testing.T) {
40 var plain, term bytes.Buffer
41 for _, c := range []struct {
42 ctx *Ctx
43 w *bytes.Buffer
44 }{{&Ctx{}, &plain}, {&Ctx{Term: Term{Cols: 80, Color: true}}, &term}} {
45 tb := c.ctx.table(c.w, "#", "STATE", "TITLE")
46 tb.row(cRef("#1"), cState("open"), cFlex(evil))
47 tb.flush()
48 }
49 noControls(t, "table", term.String())
50 if want := "#1\topen\t" + evil + "\n"; plain.String() != want {
51 t.Errorf("plain changed: %q", plain.String())
52 }
53}
54
55func TestIssueShowControlBytes(t *testing.T) {
56 show := func(term Term) string {
57 st, repo, uid := newQueueTestRepo(t)
58 if _, err := st.CreateIssue(repo.ID, uid, evil, "body "+evil, "md"); err != nil {
59 t.Fatal(err)
60 }
61 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice"})
62 c.Term = term
63 if code := Dispatch(c, []string{"issue", "show", repo.Path(), "1"}); code != protocol.ExitOK {
64 t.Fatalf("exit %d: %s", code, errOut)
65 }
66 return c.Stdout.(*bytes.Buffer).String()
67 }
68 noControls(t, "issue show", show(Term{Cols: 80, Color: true}))
69 if out := show(Term{}); !strings.Contains(out, "#1 "+evil+" open\n") {
70 t.Errorf("plain title changed:\n%q", out)
71 }
72}
73
74// A body written in a browser arrives with CRLF line endings.
75func TestIssueShowCRLFBody(t *testing.T) {
76 st, repo, uid := newQueueTestRepo(t)
77 if _, err := st.CreateIssue(repo.ID, uid, "t", "first line\r\nsecond line\r\n", "md"); err != nil {
78 t.Fatal(err)
79 }
80 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice"})
81 c.Term = Term{Cols: 80}
82 if code := Dispatch(c, []string{"issue", "show", repo.Path(), "1"}); code != protocol.ExitOK {
83 t.Fatalf("exit %d: %s", code, errOut)
84 }
85 out := c.Stdout.(*bytes.Buffer).String()
86 if strings.ContainsAny(out, "\r�") || !strings.Contains(out, "first line second line") {
87 t.Errorf("CRLF body at a terminal:\n%q", out)
88 }
89}
internal/control/view.go +7 −4
@@ -70,6 +70,7 @@ func (v *view) section(label string) {
7070func (v *view) title(ref, title, state string) {
7171 v.sep()
7272 t := v.c.Term
73 ref, title, state = t.safe(ref), t.safe(title), t.safe(state)
7374 if t.Cols == 0 {
7475 switch {
7576 case title == "" && state == "":
@@ -134,7 +135,7 @@ func (v *view) fields(kv ...string) {
134135 }
135136 v.sep()
136137 for i := 0; i+1 < len(kv); i += 2 {
137 key, val := kv[i], kv[i+1]
138 key, val := v.c.Term.safe(kv[i]), v.c.Term.safe(kv[i+1])
138139 if val == "" {
139140 continue
140141 }
@@ -159,6 +160,7 @@ func (v *view) body(src, format string) {
159160 return
160161 }
161162 v.sep()
163 src = v.c.Term.safe(src)
162164 for _, line := range strings.Split(strings.TrimRight(termtext.Render(src, format, v.opts()), "\n"), "\n") {
163165 if line == "" {
164166 io.WriteString(v.w, "\n")
@@ -171,8 +173,8 @@ func (v *view) body(src, format string) {
171173// event is one line for a system comment: its text without link
172174// targets, the time at the right edge at a terminal.
173175func (v *view) event(text, format, ts string) {
174 line := "· " + termtext.Inline(text, format)
175 when := v.c.when(ts)
176 line := "· " + termtext.Inline(v.c.Term.safe(text), format)
177 when := v.c.Term.safe(v.c.when(ts))
176178 if cols := v.c.Term.Cols; cols > 0 {
177179 room := cols - 2 - 2 - cells(when)
178180 line = pad(clip(line, room), room)
@@ -182,7 +184,8 @@ func (v *view) event(text, format, ts string) {
182184}
183185
184186func (v *view) comment(author, ts, body, format string) {
185 when := v.c.when(ts)
187 when := v.c.Term.safe(v.c.when(ts))
188 author = v.c.Term.safe(author)
186189 cols := v.c.Term.Cols
187190 if cols > 0 {
188191 suffix := ", " + when + " "
internal/termtext/termtext.go +2
@@ -20,6 +20,8 @@ type Options struct {
2020 Base string
2121}
2222
23// Render lays out markdown or org for a terminal. It passes control
24// bytes in src through: callers sanitise src first (control.termSafe).
2325func Render(src, format string, o Options) string {
2426 if format == "org" {
2527 return Org(src, o)