#+title: Parity Which surface can do what. The CLI is meant to be the complete interface: every capability exists over SSH, and the other surfaces dispatch the same control commands rather than reimplementing them, so they cannot drift. This page is updated in the merge request that changes a row. Three surfaces now: the CLI over SSH, the web, and the iOS client (krz/gitbay-ios). A =no= in the cli column is a defect, not a preference — it means some surface reached around the registry and stranded a capability where only it can reach. * Rule A capability lands over SSH first. If it belongs to the triage/review/respond loop, it lands on the web in the same merge request. Anything whose input is a credential — secrets, mirror tokens, API tokens — stays SSH-only by design: the web dispatcher refuses =SSHOnly= commands outright. Session minting is not one of them: what a browser submits to ask for a login link is a username or an address, and the credential it gets back travels by mail. Rows are one page or one action each. Grouped rows hide gaps, twice now: "browse, log, blame, search" read as covered while blame had no command at all, and "build list, log" read as covered while the job list a trigger can name had none (krz/gitbay#50), leaving the picker browser-only and the iOS build screen unable to say more than the log. * Merge requests | capability | cli | web | ios | |------------------------+-----+-----+-----| | read, diff, commits | yes | yes | yes | | review and check times | yes | yes | yes | | who resolved it, when | yes | yes | yes | | comment | yes | yes | yes | | edit title and body | yes | yes | yes | | review (approve etc.) | yes | yes | yes | | resolve a thread | yes | yes | yes | | comment on a diff line | yes | yes | yes | | merge (all strategies) | yes | yes | yes | | close | yes | yes | yes | | close in favour of another | yes | yes | no | | create | yes | yes | yes | | draft, ready | yes | yes | yes | | search title and body | yes | yes | yes | | create from a fork | yes | yes | yes | | retarget | yes | yes | yes | | milestone | yes | yes | yes | | request a review | yes | yes | yes | | choose body markup | yes | yes | yes | | stacked merge requests | yes | yes | yes | | revisions | yes | yes | yes | | range-diff | yes | no | yes | | merge gates | yes | yes | yes | Reviews and checks carry the time they last said something, and a =ci/= check carries how long its build ran, so a merge request reads without opening the build. Statuses posted through =status set= have no build and report only the time. A merged or closed merge request names who resolved it and when; a row without that stamp — a =migrate= import, or one merged before krz/gitbay!137 with no =mr.merged= event to backfill from — says neither rather than inventing a time. A merge request whose target is another open merge request's source branch is stacked on it: =mr show= and the page say so both ways, and when the lower one merges, everything stacked on it is retargeted onto what it merged into with its reviews kept. A squash or rebase merge is refused while anything is stacked on the merge request, since it would rewrite the commits the stack builds on. All three surfaces show the stack both ways. A draft merge request is open but not asking: it does not merge, and it does not appear in anyone's review queue. Draft is a flag rather than a fifth state, so every =state = 'open'= rule still means what it did. =mr revisions= lists the heads a merge request has had; the web page lists them beside the reviews they staled, and the iOS client has a Revisions section. =mr range-diff= compares two heads: the iOS client shows it from a revision to the one before, as text; the web has no view. Batched review is not built. =mr review request --add = asks a *particular* person, who then carries the merge request in their queue and is notified; =--remove= withdraws the ask. Without one the queue is computed from involvement — what you own, are granted, or reach through an org or team — so a collaborator with write access who has not touched a thread hears nothing until they do (krz/gitbay#145). Creating from a fork works anywhere the source can be typed as =owner/name:branch=. The web's source picker offers the branches of every fork the viewer can push to in that form, and the repository header has the fork control, so the whole path — fork, edit a file, propose — runs in a browser. Retargeting moves an open merge request onto another branch of the same repository and stales the reviews, since an approval was of the diff against the old branch. * Issues | capability | cli | web | ios | |---------------------+-----+-----+-----| | read, list, filter | yes | yes | yes | | filter by label, assignee, author, milestone | yes | yes | yes | | search title and body | yes | yes | yes | | create | yes | yes | yes | | comment | yes | yes | yes | | edit title and body | yes | yes | yes | | close and reopen | yes | yes | yes | | labels, assignees | yes | yes | yes | | milestone | yes | yes | yes | | milestone list | yes | yes | yes | | labels: list, colour | yes | yes | yes | | choose body markup | yes | yes | yes | | issue templates | yes | yes | yes | | milestone create, close, reopen | yes | no | yes | | org labels: set, list, remove | yes | list | no | | org milestones: create, list, close, reopen | yes | list | no | | closes across repositories | yes | yes | yes | Labels are created on the fly by =issue label --add= and managed by =label list=, =label set