GitLab users get a raw deal on stacked diffs. Most of the tools — Graphite, ghstack, Reviewable’s stacks — are GitHub-only. stkd is one of the few stacked-diff tools that supports GitLab natively, including self-hosted GitLab. This guide shows you how the workflow lands on GitLab specifically.
What “stacked” means on GitLab
GitLab merge requests work the same way GitHub PRs do at the git level: an MR is a request to merge source_branch into target_branch. To stack MRs, you simply make each MR’s target branch be the previous MR’s source branch, instead of main:
main
└── feature/auth-base MR !101 (target: main)
└── feature/auth-oauth MR !102 (target: feature/auth-base)
└── feature/auth-2fa MR !103 (target: feature/auth-oauth)
GitLab’s UI handles this pattern fine — each MR’s diff shows just the changes against its target, which is what you want. When !101 merges, !102’s target is automatically retargeted to main (GitLab has had this “auto-update target branch” behaviour since 14.0).
The catch: doing this by hand is tedious. Every time you fix something on feature/auth-base, you have to git rebase --onto feature/auth-oauth and feature/auth-2fa, then force-push both, then visit GitLab and re-trigger the pipelines. That’s exactly what stkd automates.
A worked example — GitLab edition
# Authenticate stkd against GitLab.com (one-time)
gt auth --gitlab
# Or against self-hosted GitLab
gt auth --gitlab-host=gitlab.example.com
stkd will open a browser for OAuth, or accept a personal access token with the api scope. The token is stored using your OS keychain — no plaintext files.
Now the standard flow:
cd path/to/your/repo
gt repo init
# Build the stack
gt create feature/auth-base
# … edit, commit …
gt create feature/auth-oauth
# … edit, commit …
gt create feature/auth-2fa
# … edit, commit …
# Push everything and open MRs in one go
gt submit --stack
Output (against GitLab):
✓ Pushed feature/auth-base
✓ Pushed feature/auth-oauth
✓ Pushed feature/auth-2fa
✓ MR !101 opened (feature/auth-base → main)
✓ MR !102 opened (feature/auth-oauth → feature/auth-base)
✓ MR !103 opened (feature/auth-2fa → feature/auth-oauth)
Each MR has its dependencies cross-linked in the description, and stkd adds the same canonical “Stacked PRs” comment GitHub users get. Reviewers can see the entire stack at a glance.
Auto-restack on GitLab
This is where the hand-rolled approach falls apart and stkd shines. Say a reviewer asks for a fix on the bottom MR:
gt down # navigate to feature/auth-base
# … fix it, commit …
gt restack # restacks oauth, 2fa on top
gt submit --stack # force-pushes both upper branches
stkd uses --force-with-lease, so you can’t clobber a teammate’s push without explicit override. Each pushed branch retriggers its MR pipeline, and GitLab’s “MR is out of date with target branch” warning clears automatically.
When MR !101 lands
GitLab’s auto-update target branch feature handles the easy case for you — once !101 merges to main, GitLab automatically retargets !102 from feature/auth-base to main. You don’t need stkd’s help for that step.
What stkd does help with: cleaning up the merged branch locally, and re-running gt restack so your local feature/auth-oauth is rebased onto the new main instead of the now-deleted feature/auth-base:
gt sync # fetches, detects !101 merged, deletes local feature/auth-base,
# rebases oauth + 2fa onto main, refreshes MR metadata.
One command. The whole stack stays consistent.
Self-hosted GitLab
If your team runs its own GitLab instance, stkd is the only stacked-diff CLI I know of that handles this cleanly. Set the host once:
gt auth --gitlab-host=gitlab.example.com
stkd uses the v4 REST API (every GitLab version since 11.0 supports it). The required token scope is api on a personal access token, plus standard repo access to whatever projects you stack into.
If your GitLab is behind a corporate proxy or uses a self-signed cert:
STKD_HTTPS_PROXY=http://corp-proxy:3128 \
STKD_CA_BUNDLE=/etc/ssl/corp-bundle.pem \
gt submit --stack
Both env vars are honored by every stkd command.
Pipelines and merge trains
Two GitLab features interact with stacked MRs in non-obvious ways:
CI pipelines
Every push to a branch in the stack triggers its MR pipeline. If you gt restack and gt submit --stack, N pipelines start at once. On a small instance you can swamp the runners.
stkd offers gt submit --stack --serial to push branches one at a time and wait for each pipeline to clear before pushing the next. Useful on capacity-constrained runners; unnecessary on GitLab.com (whose shared runners scale).
Merge trains
GitLab merge trains land MRs in queue order, sequentially. When you gt land --stack, stkd:
- Adds
!101to the merge train. - Waits for
!101to merge. - Adds
!102(whose target was just auto-retargeted tomain) to the merge train. - Waits for
!102to merge. - Continues until the stack is drained.
Total wall-clock time is roughly the same as merging four sequential non-stacked MRs — but you authored them in parallel.
Gotchas
A few specific GitLab quirks worth knowing:
- Required approvals across stacks. If your project requires N approvals on every MR, every MR in the stack needs N approvals. stkd doesn’t (and shouldn’t) override that. Set the stack’s bottom MRs as priorities for review.
- Squash on merge changes commit SHAs. If your project enforces “squash and merge,” the merged commit on
mainhas a different SHA than the original tip offeature/auth-base. stkd handles this correctly duringgt sync(it matches by tree, not by SHA), but other tooling that watches for specific SHAs may need a hint. - Protected branches. If
mainis protected and only the merge bot can push to it, that’s fine — stkd never pushes tomaindirectly, only to feature branches. The merge happens through GitLab’s API. - Approval reset on push. Some projects configure “reset approvals when commits are added.” Be aware that
gt restackfollowed bygt submit --stackmay reset approvals on the rebased MRs.
Getting started
# Install
brew install neul-labs/tap/stkd # or: cargo install stkd-cli
# Authenticate (one-time per host)
gt auth --gitlab # GitLab.com
gt auth --gitlab-host=gitlab.example.com # self-hosted
# Pick a repo and try it
cd ~/code/your-repo
gt repo init
gt create feature/first-stack-branch
If you’ve been waiting for a stacked-diff tool that takes GitLab seriously, this is it. The whole CLI grammar is documented in the stkd docs, and the migration guide covers moving over from Graphite if that’s where you’re coming from.
Related reading
- What are stacked diffs?
- Migrating from Graphite
- Reviewing stacked PRs
- Self-hosting the stkd web dashboard — Graphite-web-style team view, on your own infra.