Wiki: Stacked-MRs

Stacked-MRs

Stacked merge requests

Two or more merge requests where each targets the source branch of the one below it, down to main. Review and merge them one layer at a time; the forge moves what is above onto main as each layer lands.

What a stack is

  main ─── A (feat-a)   !159  feat-a → main
             └── B (feat-b)   !160  feat-b → feat-a     stacked on !159
                   └── C (feat-c)   !161  feat-c → feat-b     stacked on !160

The rule is one sentence: B is stacked on A when B's target branch is A's source branch, both are open, and both are in the same repository. Nothing is stored and no flag exists. The stack is a fact about branches that the forge reads whenever it shows a merge request, so it is never out of date and cannot be forgotten to be set.

Why

  • Keep working on a change that depends on one still in review, instead of waiting for it to merge or carrying both in one branch.
  • Each merge request holds one reviewable change. The diff of B is B's commits only, not A's underneath it.
  • Reviews and checks sit on the layer they were made on and stay there when the layer below merges.

The workflow is branches

There is no stack command. Branch from the branch below, push, and open the merge request against it:

git checkout -b feat-a main    && ...commit... && git push -u origin feat-a
gitbay mr create --source feat-a --target main   --title "A"
git checkout -b feat-b feat-a  && ...commit... && git push -u origin feat-b
gitbay mr create --source feat-b --target feat-a --title "B"

The second mr create answers with a line the first did not:

created krz/gitbay!160 (feat-b -> feat-a)
stacked on !159 A

mr show carries stacked_on (the merge request below) and stacked (the ones above); mr list rows carry stacked_on; the merge request page says "Stacked on !159" in the header and "Builds on this: !161" below it.

Changing a lower layer is a rebase you do yourself. Amend feat-a, then git rebase feat-a on feat-b and each layer above, and force-push them. A force-push stales the reviews on that layer when it changes the layer's diff, the same as on any merge request; a rebase that carries the same change keeps them. The server never rewrites your commits: it holds no signing key, and a rebase it performed would land commits nobody signed.

Merging

Merge from the bottom. When A merges, every merge request stacked on it is retargeted onto what A merged into, with a system comment:

retargeted from feat-a to main: !159 merged

Reviews on the retargeted merge request are kept. After a fast-forward or a merge commit, A's commits are on main, so B's diff against main is the diff its reviewers approved; there is nothing to stale.

That is also why a stack constrains the strategy. A squash or rebase merge of A puts different commits on main than the ones B builds on, and B's diff would carry A's changes a second time. So while anything is stacked on a merge request, --strategy squash and --strategy rebase are refused:

!159 is stacked on by !160; a squash merge rewrites the commits they
build on. Merge with --strategy ff or merge, or merge the stack into
feat-a first

The second option is real: merging B into feat-a while A is open is allowed and collapses B into A, whose head moves as on any push to its branch. Closing A without merging leaves the stack alone; its branch still exists and B still targets it.

Merge gates apply per layer as on any merge request: required approvals, resolved threads, green checks, and require_signed_commits, which under a stack already forces fast-forward.

Where it works

  • CLI and bare SSH: everything above.
  • Web: the header shows the stack both ways. Creating a merge request against a branch that is another's source works from the form; the stack appears once it exists.
  • iOS: renders the retarget comment; no stack view yet.
  • Same repository only. A fork's branch is not something another merge request can target, so a stack cannot cross a fork.

See Parity for the row.

A stack that merged

The six merge requests that shipped v1.6.0 were the first stack merged on gitbay.org, one commit each, on 2026-09-02:

MR source target
!159 stack-1-reaper main
!160 stack-2-runners stack-1-reaper
!161 stack-3-healthz stack-2-runners
!162 stack-4-monitor stack-3-healthz
!163 stack-5-lfs stack-4-monitor
!164 stack-6-verify stack-5-lfs

Each was merged with mr merge --strategy ff in that order. After each one, the next reported target_ref: main and no stacked_on, with the comment naming the merge — retargeted from stack-1-reaper to main: !159 merged on !160, and so on up to !164. No mr retarget was typed. main ended with the six commits in order on top of the commit that had added stacking.

One thing the run exposed: each fast-forward queued the commit's CI jobs again on main, although the same commit had just passed them on its branch. That is #90.