Skip to content

Abstract git forges and add GitLab support - #816

Merged
sbertix merged 13 commits into
mainfrom
sbertix/custom-git-forge
Aug 21, 2026
Merged

Abstract git forges and add GitLab support#816
sbertix merged 13 commits into
mainfrom
sbertix/custom-git-forge

Conversation

@sbertix

@sbertix sbertix commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #365
Closes #366

Summary

Abstracts the git forge behind a ForgeClient witness so pull request functionality is no longer GitHub-only, and ships GitLab as the first additional forge.

  • Forge-neutral models and a per-repository resolution ladder: explicit per-repo override (including "none"), exact well-known hosts, membership in a CLI's authenticated hosts, then known-host substrings; never a default fallback. Self-managed instances resolve through the CLI's authenticated hosts.
  • GitLab integration via glab: sidebar MR badges with head-pipeline CI state, merged-worktree auto-archive, merge (with squash and an explicit head SHA guard for namespaces enforcing SHA checks), close, mark ready, pipeline retry, and a detail tier mapping detailed_merge_status into the shared merge readiness model.
  • Forge vocabulary (MR, ! sigils, Pipelines) in the palette, inspector, and badges; capabilities gate unsupported actions (rebase merges and CI log copy on GitLab).
  • A redesigned Git Forges settings pane: one row per forge with service mark, auth status, capability grid, and enable switch, plus a per-repository forge override in repository settings.
  • GitHub fixes along the way: fresh PRs no longer render "Blocked" while mergeability is computing, insteadOf remote rewrites resolve, and merge actions stay offered until a real block.

Settings stay backward and forward compatible: the GitHub toggle keeps its legacy key, per-repo overrides are absent when inherited, and users who had the integration off do not get GitLab enabled silently.

Type of change

  • Bug fix (the linked issue is a bug report)
  • Feature (the linked issue is a feature request marked ready)
  • Documentation
  • Other (please describe)

How was this tested?

Unit tests across resolution, settings persistence and migration, GitLab decoding and fork matching, the detail tier, and the reducer flows (teardown, stale-sweep rejection, enrichment preservation). Verified live against github.com and gitlab.com repositories: badges, pipeline state, merge on a SHA-enforcing namespace, auto-archive, and the settings pane.

  • make check passes (format + lint)
  • make test passes
  • I built and ran the app to confirm the change works

Checklist

  • This pull request is linked to an issue with Closes # above.
  • For a feature, the linked issue is labeled ready.
  • I am the author of this work and accountable for it; no commit is authored or co-authored by an AI agent.
  • I have read the Contributing guide and the Code of Conduct.

sbertix added 13 commits August 21, 2026 19:42
Decode forge state strings once at the boundary (case-insensitive, with an
unknown fallback) instead of comparing raw "MERGED"/"OPEN" literals across
views and reducers. Merge readiness now distinguishes a non-blocking
"checking" assessment from a real block, so a fresh pull request with
unknown mergeability no longer renders as Blocked.
Remote URLs now parse into a host, port, and full namespace path through a
single grammar (https, ssh with port, scp-style, git protocol), with the
GitHub owner/repo identity derived on top. Remote URL resolution goes through
ls-remote --get-url so url.<base>.insteadOf aliases resolve before parsing.
Mechanical rename with no behavior change: the models every view and reducer
consumes (pull request, status checks, check state, merge queue entry,
workflow run) drop the GitHub prefix, and the GitHub-specific client files
move under Clients/Forge/GitHub.
Pull request operations now flow through a ForgeClient witness whose
endpoints are domain operations: a summary tier fetching proposals for all
queried branches, a detail tier for the selected worktree (structurally
unable to carry state or merge timestamps), and forge-neutral actions. The
GitHub adapter dispatches to the existing gh-backed client at call time, so
batching, retries, and fork matching stay adapter-private and dependency
overrides keep applying. Adds the pure per-repository forge resolver
(override, authenticated-host membership, known-host fast path, never a
default fallback), forge capabilities and vocabulary values seeded with
GitHub defaults, and an authenticated-hosts endpoint on the gh client.
Settings gain a Git Forges pane with always-visible GitHub and GitLab rows
(CLI presence and per-host auth state), a keyed per-forge enablement map that
keeps the legacy GitHub flag decoded and written for downgrade safety, and a
per-repository forge override (automatic, GitHub, GitLab, or none) resolved
through the repository settings chain. The github deeplink and CLI tokens
stay as permanent aliases for the new forges section.

The GitLab adapter serves the summary tier with one state-inclusive,
recency-ordered merge request list per repository (plus bounded per-branch
lookups when a full page may have truncated), matched locally on source
branch; merge, close, and mark-ready map to glab with an explicit auto-merge
flag and an unsupported error for per-merge rebase. Every invocation names
its host explicitly. Forge resolution goes through a registry (override,
known-host fast path, authenticated-host membership from each CLI's own
config), diff-stat fields are optional and hide when a forge reports none,
and CI log and rerun operations surface typed unsupported errors until the
detail tier lands.
The selected worktree's proposal is enriched by a per-selection detail fetch
whose payload type declares no state or merge timestamp, so it can never
influence the merged-worktree transition or the branch watermark. The GitLab
adapter serves it from one mr view call, mapping detailed_merge_status
through an adapter-owned table: mergeable, conflicts, and requested changes
land in the shared vocabulary, hard blocks carry GitLab's prose verbatim,
and in-flight or unknown statuses degrade to the non-blocking checking
state. The head pipeline maps to a single rollup check, pipelines back the
existing latest-run and re-run flows, and rerunning is capability-gated.

Palette titles and gates now come from the resolved forge's vocabulary and
capabilities (byte-identical for GitHub), the repository's resolved forge is
cached in state, and the inspector renders forge-correct nouns, sigils, and
empty states.
Availability now unions all enabled forges, so disabling one cannot
stall the others; disabling a forge tears down its rows, cancels its
in-flight sweeps, and rejects late or unresolved results. Resolution
caches remote hosts and authenticated hosts with short TTLs and never
caches errors. The GitLab client filters fork merge requests per
project, parses both glab config indent styles, encodes query values
strictly, and falls back to per-branch queries on full pages. A legacy
opt-out keeps newly introduced forges disabled for users who had turned
the GitHub integration off.
Ports the rebased #810 flow off the deleted GitHub-only remote resolver:
the re-fetch resolves its forge per repository, per-forge teardown cancels
the in-flight open-fetches scoped to its repositories, and the two fetch
toasts use forge-neutral copy.
Each forge is one row in the agents style: service mark, title, status
detail first (signed-in user and hosts bold with divider-split segments,
or the error in red with install links inline), the purpose line, and a
capability grid derived from ForgeCapabilities, with the enable switch
trailing and a Beta pill on GitLab. The merge pickers move to a headless
leading section, the shared Beta pill adopts the tinted style, and the
pane gains the GitLab tanuki asset and a new sidebar icon.
One GraphQL call per sweep fetches each matched merge request's head
pipeline, so unselected rows show CI state like GitHub rows; a failed
call degrades to no badge, never a failed sweep. Rollup-only summaries
keep detail enrichment while still taking the fresher pipeline. Also
probes glab's macOS config location (Library/Application Support) so
keyring logins surface in authenticated hosts and the settings status.
Merge in the palette and inspector was gated on confirmed mergeability,
which hid it while a forge still reports checking. The gate now hides
merge only on a real block. The inspector's action rows also pick up
capability gating and forge vocabulary, and its header drops the plain
number that duplicated the badge.
Adapters stamp how their forge writes proposal numbers, so sidebar and
inspector badges render !12 for merge requests without a registry
lookup. GitLab merges read the current head SHA first and pass --sha:
namespaces enforcing SHA checks reject sha-less merges outright, and
the guard also fails a merge whose branch moved since it was reviewed.
@sbertix
sbertix enabled auto-merge (squash) August 21, 2026 19:07
@sbertix
sbertix merged commit 22294db into main Aug 21, 2026
3 checks passed
@sbertix
sbertix deleted the sbertix/custom-git-forge branch August 21, 2026 19:18
@sbertix sbertix mentioned this pull request Aug 21, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitLab support?

1 participant