Repository navigation
feat: GitLab merge requests as pull requests - #690
Merged
Merged
Conversation
HostKind::Gitlab (gitlab.com by name or a "gitlab" DNS label; self-managed servers by configuration, with a port and no path) and GitLab's terms, which take glab's merge commands from GitHub's. services::gitlab reaches each server over REST v4 with its own token (saved, GITLAB_TOKEN for GITLAB_HOST's server or gitlab.com, glab's stored login per host) and reads discussions, awards, labels, permissions and blobs over GraphQL, which answers a public project anonymously where REST's notes and labels refuse. Writes cover comments, replies, resolution, awards, edits, labels by name, reviewers by id, the Draft: title prefix, close and reopen, rebase, merge with squash, a cleaned message and merge_when_pipeline_succeeds, and a review replayed as line discussions, a note and an approval. The conflict rule that holds a false for 30 seconds at one head, Tcode's viewed marks, write answers, media reads and the CLI runner move to forge so Forgejo and GitLab share them; one viewed-marks store serves both. Capabilities gain update_merge, which GitLab lacks, so its branch update is a rebase.
The GitLab mark joins the icon assets. On a host without update_merge the behind chip and the menu offer only the rebase, and the primary update asks for it; a review a host stopped part way through says so in its own toast.
…hout counts shows none
…s GitLab's answer shows
…date describes a rebase
… host only rebases
…and auto-merge reads the one REST field
Tryanks
marked this pull request as ready for review
October 11, 2026 00:22
Tryanks
enabled auto-merge (squash)
October 11, 2026 00:22
# Conflicts: # crates/ui/src/pull_requests/lifecycle.rs # crates/ui/src/pull_requests/mod.rs
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 3 of #643 (A7 of #535): GitLab (gitlab.com and self-managed) behind the host boundary, following the Forgejo host of #683.
Evidence
Before: GitLab URLs were read as GitHub's (unknown host → GitHub) and failed; Add host had no GitLab kind.
After:
Fresh profile, English, light; a synthetic self-managed host
gitlab.acme.test:8443; no CLI logins (a throwaway HOME, so none of the machine's own gh/tea/glab logins appear), so gitlab.com is not listed: nothing names it. Add host typescode.acme.test:8443, which is not listed. The desktop shots and the phone Source Control were re-shot after the first review; the phone conversation is kept from round 1, since nothing in the corrections changes that page. The MR page isgitlab-org/gitlab-test!26, GitLab's public test-fixture project, linked anonymously through the link dialog: the conversation shows awards on the description, avatars and a coloured label; Files shows the hunks with per-file counts, GitLab's own "+3 −0" for the MR (it read "+0 −0" before) and "Viewed marks kept by Tcode". In the first round, toggling a mark wroteviewed-marks.jsonkeyed by the file's GitLab blob id5c5eba4…, and a note GitLab marks edited showed "edited". That MR has no line discussion, so the shots show no review thread; one could not be created without a token. A quick dark-theme look at Source Control showed the marks and notices in theme colours.Live reads on gitlab.com through Tcode's host (
forge::connectdriven by a throwaway probe, not committed, anonymous; counts aregitlab host=… path=…request log lines):gitlab-org/cli!4049(merged, resolved line discussions, head pipeline success): summary 1 (Merged, checks Passing, Clean, 12 files); conversation 1 (7 comments, 3 threads all resolved, 4 labels with colours, reviewers GitLabDuo ChangesRequested and jay_mccure Approved, complete); files 2 (12 files with hunks, complete); file text 1 (go.mod, 157 lines); viewed files 3 (12 blob revisions); label candidates 2 (104, 4 applied); reviewer candidates 2 (102, 2 requested); action state 2 (behind 0); watch detail 1 (Merged, head sha, one Pipeline check Success, Clean); activity 1 (13 remarks, 6 on lines). 16 lines in total.gitlab-org/cli!4044(open, unresolved threads): summary 1 (Open, Passing, Clean), action state 2 (Blocked fromdiscussions_not_resolved, behind 26), watch detail 1, activity 1. A first run hit the 60 s deadline on three reads while gitlab.com was slow from here; the rerun answered all of them.notes,discussionsandprojects/:id/labelswith 401 to an anonymous reader, so discussions, awards, permissions and label candidates are read over GraphQL (Source control: other hosts, Forgejo/Gitea, GitLab, Bitbucket, Azure DevOps (A7) #643: "direct REST (or GraphQL where the host offers it)"); GraphQL'sMergeRequesthas nolastEditedAt, so the description carries no edited-at; a merged MR keepsmerge_when_pipeline_succeeds: true, so auto-merge reads as armed only while open; GraphQL requests logged no path.Writes: unverified. This machine has no
glaband noGITLAB_TOKEN, so no scratch projecttcode-a7-gitlab-checkwas created and nothing needed deleting. Unverified against a live server: comment, reply, resolve/unresolve, award and unaward, edit note, edit title/description, labels, reviewers, draft/ready (title prefix), close, reopen, rebase, merge (merge/squash, cleaned message,merge_when_pipeline_succeedsand its cancel), review (line discussions withposition, summary note, approve). Also unverified: glab's credential helper andauth status --output json(read from glab's source,internal/commands/auth/login/helper.goandauth/status/status.go, not run), RESTmerge_method/squash_optionfor an authenticated reader (anonymous reads omit them;lib/api/entities/project.rbexposes them on the full entity), and media reads with a token, including whether GitLab's web upload route (https://host/<project>/uploads/<secret>/<file>) accepts a Bearer personal access token for a private project; if it does not, private images answer 302 or 404 and are not drawn, and the API route/api/v4/projects/:id/uploads/:secret/:filenameis the alternative.Upstream gaps fixed (#643): head SHA on every watch read (
sha); checks state on the snapshot from the head pipeline; mergeability through the ForgejoVerdictsrule, so a conflict reported while GitLab rechecks is held 30 s at one head; edited-at on notes fromlastEditedBy(GitLab bumpslastEditedAtwhen a thread is resolved; only an edit names an editor); merges sendmerge_when_pipeline_succeeds: falseunless asked; update branch is a rebase; review replay reports Partial.Left out because the API cannot do it (no substitute built):
request_changesfalse, so the review sheet offers Comment and Approve.PUT /rebase); capabilityupdate_mergefalse, so the merge item is absent and the primary update asks for the rebase.POST /unapprove, but no Tcode action takes back a verdict (no such action exists for any host), so it is not wired.mergeRequestSetReviewersexists on gitlab.com's schema (introspected:projectPath,iid,reviewerUsernames,operationMode: REPLACE|APPEND|REMOVE), so additions APPEND and removals REMOVE against the set GitLab holds when it applies them; each change is checked against the reviewer set GitLab answers with, and what did not take (for example a second reviewer on a tier that takes one) is reported Partial or Rejected instead of Applied.Review fixes (round 2)
GITLAB_TOKENfollows glab's host variables in glab's order,GITLAB_HOST,GITLAB_URI,GL_HOST(glabinternal/config/schema.go:154); withGL_HOST=https://gitlab.corp.examplethe token goes there and never to gitlab.com.GITLAB_URIwas added beside the review'sGL_HOSTbecause it carries the same hazard. The test failed without the fix (the token went to gitlab.com).GITLAB_TOKENname it, like codeberg.org; screenshots re-shot.diffStatsSummary { additions deletions };PullRequestSnapshot.additions/deletionsbecameOption<u64>(a kept number still reads as a count; absent hides the stat in the row and the Files header instead of showing zeros).](/uploads/,]: /uploads/,src=/href=attributes) are namedhttps://host/<project>/uploads/…, so the client can draw them and the media read finds them named; the separatebelowpath match became redundant and was removed. An edit of such a body sends the absolute links back, which GitLab accepts. Fenced code blocks (``` and ~~~) and inline code spans stay as written (second review), so a comment showing Markdown syntax keeps its code text. Left relative, so fail-closed (the link stays as GitLab wrote it and its media is not read):[r]:/uploads/…without a space, angle-bracket destinations `](</uploads/…>)`, unquoted `src=/uploads/…`, and indented (four-space) code blocks, which are rewritten like text.mergeRequestSetReviewersAPPEND/REMOVE (above); the REST read-modify-write is gone.RebaseStarted(its doc now covers one pull request's update the host finishes on its own); the lifecycle toast says "Rebase started for %{head}" / "%{host_name} rebases it shortly. Pull before pushing from this thread." (design §5.3), and the rebase-only primary and the behind chip's menu label use a rebase tooltip instead of "Merges the latest …".update_mergegate.manualandscheduledpipelines read as Pending (a pipeline blocked on a manual job must not read as Passing); the watch keeps reading such a merge request as in flight.auto_merge_enabledreads: GitLab's REST merge request entity exposes onlymerge_when_pipeline_succeeds(lib/api/entities/merge_request_basic.rb:65).merge_methodisrebase_mergecreates a merge commit, yet it is offered as Rebase, so agent-credit cleaning is skipped for it; L5 —auto_merge_allowedis unconditional (GitLab has no project switch Tcode reads for it).diffStatsSummaryread logs at debug and the snapshot carries no stat.Tests
uploads_are_named_on_their_servergains a fenced block (``` and ~~~), an inline code span followed by a link that is still named, and a non-ASCII code span (the first draft of the span scan sliced at byte offsets and would have panicked on it).gitlab_com_is_listed_once_something_names_it;a_kept_snapshot_reads_its_line_counts(a literal kept snapshot with numbers reads them; without the fields, no count);uploads_are_named_on_their_server;a_reviewer_change_is_what_gitlab_answers(a second reviewer GitLab keeps out without an error is not Applied; an answer without the merge request is no removal);a_merge_update_is_refused_where_the_host_only_rebases(dispatcher:UpdateBranch{rebase:false}→Rejected(Unsupported)before any request;rebase:truepasses the gate and meets the host, here turned off, asHostDisabled). Extended:a_token_goes_only_to_the_server_it_is_for(GL_HOSTalone, andGITLAB_HOSTbeatingGL_HOST; the fake glab also refuses to run with any host variable);a_pipeline_reads_as_one_check(manual and scheduled are pending; that expectation changed with the contract, per the review).urls_and_remotes_find_the_project_by_port_and_subgroups(MR URL with port and subgroups, with and without/-/; https, ssh:// and scp remotes; userinfo host refused; a one-segment project refused; URL-encoded project id);a_token_goes_only_to_the_server_it_is_for(gitlab:GITLAB_TOKENonly forGITLAB_HOST's server, else gitlab.com; a fakeglabthat refuses to run withGITLAB_TOKENin its environment answers only its ownhost:port);merge_status_reads_as_mergeability_and_merge_state;a_pipeline_reads_as_one_check;a_draft_is_its_titles_prefix;a_comment_names_the_lines_gitlab_places_it_by(it failed first: the hunk counter started one line late, fixed in the code).each_host_goes_to_its_kinds_implementation(gitlab.com and agitlablabel by name;gitlab.acme.testconfigured as Forgejo stays Forgejo; a configuredcode.acme.test:8443routes its MR URL to GitLab);the_model_reads_the_hosts_settings_configure(the GitHub-only linking text is still the literal origin/main text; GitLab configured addsglaband "GitHub or GitLab";glab mr closestill detected);each_kind_names_its_hosts_by_its_own_rule(GitLab port allowed, path refused); the URL-hint test (GitLab MR URL, one-segment project refused).forgejo::reads::a_conflict_is_a_false_that_holds_at_one_head→forge::verdicts::a_conflict_is_one_that_holds_at_one_head(the rule is shared now; it reads aMergeabilityinstead of Forgejo JSON, whosemergeablemapping stays covered bystatuses_and_mergeability_read_as_the_watch_needs);forgejo::api::a_redirect_off_the_servers_origin_drops_the_token→forge::httpwith the media read it guards.Checks at the PR head
cargo fmt --all --check: cleancargo clippy --workspace --all-targets --locked -- -D warnings: cleancargo nextest run --workspace --locked --no-fail-fast:1122 tests run: 1122 passed, 13 skippedcargo machete: no unused dependenciesFollow-ups (not in this PR)
in_order/Stepintoforge::http.Merge Danger
Door: two-way
Blast Radius: source-control
Wire changes, noted above PROTOCOL_VERSION (no bump):
HostKindgainsgitlab;PullRequestCapabilities.update_merge(GitHub and Forgejo true);PullRequestSnapshot.additions/deletionsmay be null (kept thread metadata with numbers still loads; an older build reading a null fails on that snapshot);RebaseStartedalso answers one pull request's branch update. GitLab tokens are saved under@gitlab. Hosts namedgitlab.*or with agitlablabel that were read as GitHub before now route to GitLab, unless settings say otherwise. The model-visible text is unchanged for GitHub-only settings and grows only when a GitLab host is configured. Side effects on other hosts:is_pull_request_url(a menu hint) now accepts GitLab MR URLs;merges_or_closesstill matchesglab mr merge|close, now through GitLab's terms. Forgejo's verdicts, viewed marks, write answers and media read moved toforgeunchanged, and the two hosts now share one viewed-marks store instead of each holding the file (two stores on one file would overwrite each other's marks). The GitLab mark (assets/icons/gitlab.svg) is Simple Icons' (CC0).Part of #643