Skip to content

feat(server): add GitLab glab CLI support for merge request operations#592

Draft
binbandit wants to merge 10 commits intopingdotgg:mainfrom
binbandit:feat/gitlab-glab-support
Draft

feat(server): add GitLab glab CLI support for merge request operations#592
binbandit wants to merge 10 commits intopingdotgg:mainfrom
binbandit:feat/gitlab-glab-support

Conversation

@binbandit
Copy link
Contributor

@binbandit binbandit commented Mar 9, 2026

Summary

Adds GitLab (glab CLI) support for the PR/MR creation flow, alongside the existing GitHub (gh) support. The hosting provider is detected automatically from the repository's origin remote URL — no user configuration required.

Closes #535
Closes #191

Architecture

The existing GitHubCli Effect service is kept as the GitHub-specific implementation (with Schema-validated JSON decoders, error normalization, etc.). This PR adds a provider-agnostic GitHostingCli service that acts as the interface consumed by GitManager, with a dispatcher layer that delegates to GitHubCli for GitHub repos and implements glab CLI calls inline for GitLab repos:

GitHostingCli (service contract — consumed by GitManager)
    │
    └── GitHostingCliDispatcher (runtime layer)
            │
            ├── detectHostingProvider(cwd)
            │     └── git remote get-url origin → parse hostname
            │           ├── github.com / *.github.com → "github"
            │           ├── gitlab.com / *.gitlab.com → "gitlab"
            │           └── unknown → "github" (backwards-compatible default)
            │
            ├── "github" → delegates to GitHubCli service (dependency)
            │     └── GitHubCliLive (Schema-validated gh JSON parsing)
            │
            └── "gitlab" → inline glab CLI calls
                  └── glab mr list / glab mr view / glab mr create / glab repo view

Layer wiring (serverLayers.ts):
  GitHostingCliLive ─── provides ──→ GitHubCliLive
  GitManagerLive    ─── depends on ─→ GitHostingCliLive

Detection logic

detectHostingProvider(cwd) runs git remote get-url origin, parses the URL hostname (supports both https:// and git@ formats), and matches against gitlab.com / *.gitlab.com. Results are cached per repository path so detection only runs once per cwd. Falls back to GitHub for unknown or missing remotes, preserving full backwards compatibility.

Key design decisions

  • GitHubCli is kept unchanged — all existing GitHub-specific Schema types (GitHubPullRequestSummary, GitHubRepositoryCloneUrls), JSON decoders, and the GitHubCli.test.ts unit tests from main's PR Add PR thread setup for local and worktree modes #718 are preserved as-is.
  • GitHostingCli is the superset interface — includes all 7 methods: execute, listOpenPullRequests, getPullRequest, getRepositoryCloneUrls, createPullRequest, getDefaultBranch, checkoutPullRequest.
  • GitHubCliError is a deprecated alias for GitHostingCliError (same _tag: "GitHostingCliError"), so existing error-handling code continues to work.

Changes

File Change
apps/server/src/git/Services/GitHostingCli.ts New — Provider-agnostic service contract (GitHostingCliShape, PullRequestSummary, RepositoryCloneUrls, GitHostingCli tag)
apps/server/src/git/Layers/GitHostingCliDispatcher.ts New — Runtime dispatcher: detects provider, delegates to GitHubCli for GitHub, implements glab inline for GitLab
apps/server/src/git/Errors.ts Add GitHostingCliError class; GitHubCliError kept as deprecated alias
apps/server/src/git/Layers/GitManager.ts Consume GitHostingCli instead of GitHubCli (5 call sites)
apps/server/src/git/Layers/CodexTextGeneration.ts PR prompt text made provider-agnostic
apps/server/src/serverLayers.ts Wire GitHostingCliLive layer with GitHubCliLive as dependency
apps/server/src/git/Layers/GitManager.test.ts Updated to use GitHostingCliError / GitHostingCli / PullRequestSummary types

GitLab CLI commands used

Operation Command
List open MRs glab mr list --source-branch <branch> --per-page <n> --output json
View MR glab mr view <id> --output json
Create MR glab mr create --target-branch <base> --source-branch <head> --title <title> --description <body> --yes
Checkout MR glab mr checkout <id>
Repo clone URLs glab repo view <repo> --output jsonhttp_url_to_repo, ssh_url_to_repo
Default branch glab repo view --output jsondefault_branch

Testing

  • bun lint — 0 errors
  • bun typecheck — 0 errors across all 7 packages
  • bun fmt:check — all files formatted
  • GitManager tests: all 29 pass (including main's new fork/worktree/PR thread tests)
  • GitHubCli tests: all 3 pass (Schema decoder tests from main, preserved unchanged)

Note

Add GitLab glab CLI support for merge request operations

  • Introduces a new GitHostingCli abstraction and GitHostingCliDispatcher that routes all PR/MR operations (listPullRequests, createPullRequest, checkoutPullRequest, etc.) to either gh or glab based on the repository's remote origin hostname.
  • Replaces all direct GitHubCli references in GitManager with the new GitHostingCli interface, making hosting-provider detection transparent to callers.
  • Adds hostingPlatform ('github'|'gitlab') and hostingCliAuthenticated (boolean|null) fields to GitStatusResult, surfaced from the dispatcher's cached auth checks.
  • Updates the web UI (GitActionsControl, Sidebar, pullRequestReference) to use platform-aware PR/MR labels, icons, and disabled states — including downgrading PR creation to push-only when the hosting CLI is unauthenticated.
  • Risk: all consumers of GitStatusResult must now handle the two new required fields; the GitHostingCliError tag replaces GitHubCliError (backward-compat aliases provided).

Macroscope summarized 5273959.

Introduce a provider-agnostic git hosting abstraction that automatically
detects whether a repository is hosted on GitHub or GitLab (by inspecting
the origin remote URL) and dispatches PR/MR operations to the correct CLI.

Architecture changes:
- Add GitHostingCli service contract (generic interface replacing the
  GitHub-specific GitHubCli service)
- Add GitHostingCliDispatcher layer that detects github.com vs gitlab.com
  from the origin remote URL and routes to gh or glab accordingly
- Rename GitHubCliError to GitHostingCliError (deprecated alias preserved
  for backwards compatibility)
- Update GitManager to consume the generic GitHostingCli service
- Remove standalone GitHubCli/GitLabCli layer files (logic consolidated
  into the dispatcher)

GitLab support includes:
- glab mr list (with --source-branch, --output json)
- glab mr create (with --target-branch, --source-branch, --description)
- glab repo view --output json (for default branch detection)
- Authentication and CLI-not-found error normalization

Closes pingdotgg#535
Closes pingdotgg#191
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fa34fd4e-ff28-4a22-90d3-70f4ec5b8e8a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@binbandit
Copy link
Contributor Author

@t3dotgg I know gitlab is not always the most supported platform, but this might help with user adoption. Happy to revise if you want to take another approach for this implementation

@github-actions github-actions bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Mar 9, 2026
@binbandit
Copy link
Contributor Author

@juliusmarminge I cannot add the needsjulius tag, but i think this is another one you should review before merge

@juliusmarminge
Copy link
Member

looks mostly alright, with some exceptions:

a) will conflict a bunch with #718.
b) isn't a better architecture to have separate service layers for each, pseudo:

// Service
export class GitHostingService extends ServiceMap.Service<GitHostingService, Shape>(...)

// Layers/Github
export const makeGithubService = ...

// Layers/GitLab
export const makeGitlabService = ...

then whenever we run with the GitHostingService, we do some Effect.provideService(GitHostingService, detectAndMake()

@binbandit
Copy link
Contributor Author

binbandit commented Mar 10, 2026

looks mostly alright, with some exceptions:

a) will conflict a bunch with #718. b) isn't a better architecture to have separate service layers for each, pseudo:

// Service
export class GitHostingService extends ServiceMap.Service<GitHostingService, Shape>(...)

// Layers/Github
export const makeGithubService = ...

// Layers/GitLab
export const makeGitlabService = ...

then whenever we run with the GitHostingService, we do some Effect.provideService(GitHostingService, detectAndMake()

Yes and no. I believe either direction would be viable realistically. It just comes down to preference.
If you would prefer to go that route we can.

In terms of conflicting with the other PR. We can either try and change this to work in a way that will cause less conflict, or we could merge this before that to allow for gitlab users earlier. Then i'll contribute to your branch to fix the conflicts.

Up to you with how you want to move forward.

@juliusmarminge
Copy link
Member

juliusmarminge commented Mar 10, 2026

consulted mr gpt:

A hybrid architecture is usually the cleanest.

Define provider-specific implementations, but keep a dispatcher service.

Structure:

GitHostingService (interface)

GithubGitHostingService
GitlabGitHostingService

GitHostingDispatcherService
    detectProvider()
    delegate → githubService | gitlabService

Example conceptually:

GitHostingService
    listPRs()
    createPR()

GithubGitHostingService
GitlabGitHostingService

GitHostingDispatcherService
    listPRs():
        provider = detect()
        return providerService(provider).listPRs()

Benefits:

• clean separation of providers
• easy to test
• easy to add Bitbucket later
• dispatcher remains thin
• consumers stay simple


One more improvement: cache provider detection per repo path so you don't run git each call.

Example:

Map<repoPath, provider>

Invalidate if needed.


Rule of thumb:

• Provider varies per repo/request → dispatcher
• Provider fixed per runtime → service injection

Since you're using cwd detection, your original dispatcher architecture is correct.

If you'd like, I can also show a very clean Effect-style version that keeps the dispatcher but still uses Layers elegantly.

@juliusmarminge
Copy link
Member

as for order, i need to get mine in to be able to triage faster, and i have no ability to test the glab cli rn so I'm going to merge mine first

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@binbandit Once this has been updated with the new changes I am happy to give this a quick test, we use GitLab for some of our stuff at work so I can try it there

@binbandit
Copy link
Contributor Author

binbandit commented Mar 10, 2026

@binbandit Once this has been updated with the new changes I am happy to give this a quick test, we use GitLab for some of our stuff at work so I can try it there

@Noojuno Good to go. Let me know how it goes 😊

@juliusmarminge
Copy link
Member

juliusmarminge commented Mar 10, 2026

@binbandit checkout pr is merged so feel free to get this up to date and we can get it merged (preferalby after someone can vouch for the glab working)

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@juliusmarminge @binbandit I'll give this a try to validate it when it's up to date (wasn't able to before)

@binbandit binbandit force-pushed the feat/gitlab-glab-support branch from f6e24d1 to 504481d Compare March 10, 2026 09:34
@binbandit
Copy link
Contributor Author

@juliusmarminge This now incorporates the architecture you requested.

@Noojuno It's good for you to test please ☺️

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@binbandit Just had a chance to give it a go, but it doesn't seem to work for me.

I am using macOS with glab 1.89.0 (c6fca530)

Test 1 - Creating a new branch and MR

When creating a new branch (so Commit, Push, and PR workflow) it commits and pushes the change, but errors when creating the PR. This test was recorded when using a worktree, but it happens on a regular branch too.

screen-recording-2026-03-11-1006-compressed.mp4
Screenshot 2026-03-11 at 10 10 17 AM

It then gets put in a broken state where I can't trigger the PR creation again, even though it failed the first time.

Screenshot 2026-03-11 at 10 08 43 AM

Test 2 - Auto-detecting existing MR's when loading a branch

When choosing a branch with an active PR that uses GitHub, we get an auto-detected "View PR" button state change, even if the PR was created externally from T3 Code. This doesn't happen on GitLab.

GitHub:

Screenshot 2026-03-11 at 10 16 19 AM

GitLab:

image

Other feedback

This is the more nitpicky feedback since it didn't really work for me, so I wasn't able to review the full thing.

The mix in terminology between MR and PR is a little odd, imo in an ideal world we would refer to them as MR's rather than PR's when using GitLab.

Similarly (and I think this one is more of a big deal), using the GitHub icon for the PR related actions when using GitLab is very confusing, I think this probably needs to be the GitLab icon. I work at an org that uses both, and I think displaying the GH icon would confuse people.

Happy to test again (or give any info you may need to debug the issue), so lmk!

@binbandit
Copy link
Contributor Author

binbandit commented Mar 10, 2026

@binbandit Just had a chance to give it a go, but it doesn't seem to work for me.

Oh no! I tried it myself and it seemed to work. Let me try a fresh device to ensure there is no quirks with my setup. Thanks for testing.

Your comments about the mix of MR and PR are valid. I'll see what I can do.
With the gitlab icon, ill have a hunt to see if I can source one to use.

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@binbandit Just had a chance to give it a go, but it doesn't seem to work for me.

Oh no! I tried it myself and it seemed to work. Let me try a fresh device to ensure there is no quirks with my setup. Thanks for testing

I'm about to swap over to Windows for some other testing of a project, when I get a chance I will also give it a once over there to see if I am having the same issue

# Conflicts:
#	apps/server/src/git/Layers/GitManager.ts
… changes

Merge main's cross-repository PR detection (headSelector, BranchHeadContext,
resolveRemoteRepositoryContext) into the GitLab hosting abstraction layer.
Restore all GitLab-specific changes (Icons, platform-aware UI labels, MR URL
parsing) that were stashed during the merge.
@binbandit
Copy link
Contributor Author

@Noojuno I have added the logo and the MR instead of PR. Plz check

Still sussing out the glab failure though

@binbandit
Copy link
Contributor Author

@Noojuno Just confirmed on 2 different clean machines that it works. Don't want to be the guy that says "It works on my machine"... but i think it does

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@Noojuno Just confirmed on 2 different clean machines that it works. Don't want to be the guy that says "It works on my machine"... but i think it does

Okay good to know, I'll give it a go on Windows and then start tracking down why it isn't working for me :)

…ests abstraction

findLatestPr was calling gitHostingCli.execute() with gh-specific CLI args
(pr list --head --state --json) which fail on glab since GitLab uses
completely different syntax (mr list --source-branch --all --output json).

Add listPullRequests method to GitHostingCliShape that accepts state filter
and limit, with proper glab flag translation (--all, --closed, --merged).
Add updatedAt to PullRequestSummary so the findLatestPr sort-by-recency
logic works through the abstraction layer. Remove now-dead
parsePullRequestList function from GitManager.
@binbandit
Copy link
Contributor Author

Made some hardening changes that might also help

@Noojuno
Copy link
Contributor

Noojuno commented Mar 10, 2026

@binbandit Ahh I figured it out, it was (mostly) a me issue haha.

I was using a GitLab PAT via GITLAB_TOKEN env var (for some complicated reasons), but the glab CLI process was failing due to not being authed. Once I resolved that it is all working perfectly.

We should probably handle that error state (no auth) slightly nicer than we do here and properly surface it to the user.

On a similar note, the error toast has regressed slightly and now shows the full stack trace, could probs also use some polish.

Screenshot 2026-03-11 at 12 24 25 PM

Since you also changed the UI now with the MR wording and GitLab icon, it's probably also worth adding to the PR description.

@binbandit
Copy link
Contributor Author

@juliusmarminge

Should we remove the GitHub & gitlab create PR / MR options entirely if they are not authed?

@juliusmarminge
Copy link
Member

@juliusmarminge

Should we remove the GitHub & gitlab create PR / MR options entirely if they are not authed?

probalby wanna have disabled buttons with tooltips saying they're not authed, yeaa

Add proactive auth status check for gh/glab CLIs with 60s TTL cache,
surfaced as hostingCliAuthenticated in GitStatusResult. When not
authenticated, PR/MR creation is disabled with a tooltip guiding the
user to run `gh auth login` or `glab auth login`.

Fix error toasts showing full Cause.pretty() fiber metadata by
extracting clean tagged error messages via Cause.findErrorOption().
@binbandit
Copy link
Contributor Author

@Noojuno Would love a final check please.
@juliusmarminge I think we are gucci

Comment on lines +44 to +53
const authStatusCache = new Map<HostingProvider, AuthCacheEntry>();

/**
* Check whether the hosting CLI is authenticated by running
* `gh auth status` or `glab auth status`.
*
* Uses a per-provider in-memory cache with 60s TTL to avoid
* running the check on every status poll.
*/
function checkHostingAuthStatus(provider: HostingProvider): boolean | null {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use effect/Cache instead of ad-hoc implementations. do a repo search for usage examples if you need references

statusDetails(input.cwd).pipe(
Effect.map((details) => ({
branch: details.branch,
hostingPlatform: "github" as const,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not always correct?

Comment on lines +288 to +301
runProcess(
"glab",
[
"mr",
"list",
"--source-branch",
input.headSelector,
"--per-page",
String(input.limit ?? 1),
"--output",
"json",
],
{ cwd: input.cwd, timeoutMs: DEFAULT_TIMEOUT_MS },
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put these in a GitlabCli service layer. dispatcher shoould just coordinate between the different hosting services

/**
* GitHubCliShape - Service API for executing GitHub CLI commands.
*/
export interface GitHubCliShape {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a static readonly name = 'gh' or similar and use that instead of condition ? 'gh' : 'glab' in multiple places

@binbandit
Copy link
Contributor Author

The more and more i work on this. The less I am liking the solution as a whole. I think we have gone too far in the wrong direction.

I apologise for anyone waiting on this. Checkout the branch where it is and use as is for now.

I want to take a moment to really think about this

@binbandit binbandit marked this pull request as draft March 11, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[improvement] add support for glab GitLab support for Commit+PR button

3 participants