Commit c502cff530

c502cff53074f87a2d8cd37e87d2cfd0f71bf4b8

parent: f2bffdcbb5

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-02 03:41 UTC

Stacked merge requests: a page, linked from Home, Users and FAQ

Ref krz/gitbay#87

Layout: unified · split

FAQ.org +5
@@ -4,6 +4,11 @@
44 (and light write) — mint a browser session with =gitbay web login=.
55- Pull requests? :: Merge requests: =gitbay mr create= from a branch or
66 fork; merges are ff/merge/squash/rebase with signature policy.
7- Stacked PRs? :: Target another open merge request's source branch.
8 The forge reads the stack from the branches, retargets what is above
9 when the one below merges, and refuses squash or rebase under a stack.
10 No stack command; the workflow is branches. See
11 [[Stacked-MRs][Stacked merge requests]].
712- Moving from GitHub? :: =gitbay repo import= (git data) then
813 =gitbay repo import-issues= (history); =repo mirror= covers the
914 transition window.
Home.org +1
@@ -5,6 +5,7 @@ CLI-first git forge: SSH is the API, the web is a rendering.
55- [[Quickstart][Quickstart]] — clone, push, first issue
66- [[FAQ][FAQ]] — common questions from GitHub migrants
77- [[Users][User guide]] — accounts, keys, verified commits, repos, issues, MRs
8- [[Stacked-MRs][Stacked merge requests]] — dependent merge requests, merged one layer at a time
89- [[Admin][Admin guide]] — install, configuration reference, backup, security
910- [[API][API and webhooks]] — the JSON API contract, tokens, payloads
1011- [[Roadmap][Roadmap]] — status, phases, decisions, what is not planned
Stacked-MRs.org added +130
@@ -0,0 +1,130 @@
1#+title: Stacked merge requests
2
3Two or more merge requests where each targets the source branch of the
4one below it, down to =main=. Review and merge them one layer at a time;
5the forge moves what is above onto =main= as each layer lands.
6
7* What a stack is
8
9#+begin_example
10 main ─── A (feat-a) !159 feat-a → main
11 └── B (feat-b) !160 feat-b → feat-a stacked on !159
12 └── C (feat-c) !161 feat-c → feat-b stacked on !160
13#+end_example
14
15The rule is one sentence: =B= is stacked on =A= when =B='s target branch
16is =A='s source branch, both are open, and both are in the same
17repository. Nothing is stored and no flag exists. The stack is a fact
18about branches that the forge reads whenever it shows a merge request,
19so it is never out of date and cannot be forgotten to be set.
20
21* Why
22
23- Keep working on a change that depends on one still in review, instead
24 of waiting for it to merge or carrying both in one branch.
25- Each merge request holds one reviewable change. The diff of =B= is
26 =B='s commits only, not =A='s underneath it.
27- Reviews and checks sit on the layer they were made on and stay there
28 when the layer below merges.
29
30* The workflow is branches
31
32There is no stack command. Branch from the branch below, push, and open
33the merge request against it:
34
35#+begin_src sh
36git checkout -b feat-a main && ...commit... && git push -u origin feat-a
37gitbay mr create --source feat-a --target main --title "A"
38git checkout -b feat-b feat-a && ...commit... && git push -u origin feat-b
39gitbay mr create --source feat-b --target feat-a --title "B"
40#+end_src
41
42The second =mr create= answers with a line the first did not:
43
44#+begin_example
45created krz/gitbay!160 (feat-b -> feat-a)
46stacked on !159 A
47#+end_example
48
49=mr show= carries =stacked_on= (the merge request below) and =stacked=
50(the ones above); =mr list= rows carry =stacked_on=; the merge request
51page says "Stacked on !159" in the header and "Builds on this: !161"
52below it.
53
54Changing a lower layer is a rebase you do yourself. Amend =feat-a=,
55then =git rebase feat-a= on =feat-b= and each layer above, and
56force-push them. A force-push stales the reviews on that layer, the
57same as on any merge request. The server never rewrites your commits:
58it holds no signing key, and a rebase it performed would land commits
59nobody signed.
60
61* Merging
62
63Merge from the bottom. When =A= merges, every merge request stacked on
64it is retargeted onto what =A= merged into, with a system comment:
65
66#+begin_example
67retargeted from feat-a to main: !159 merged
68#+end_example
69
70Reviews on the retargeted merge request are kept. After a fast-forward
71or a merge commit, =A='s commits are on =main=, so =B='s diff against
72=main= is the diff its reviewers approved; there is nothing to stale.
73
74That is also why a stack constrains the strategy. A squash or rebase
75merge of =A= puts different commits on =main= than the ones =B= builds
76on, and =B='s diff would carry =A='s changes a second time. So while
77anything is stacked on a merge request, =--strategy squash= and
78=--strategy rebase= are refused:
79
80#+begin_example
81!159 is stacked on by !160; a squash merge rewrites the commits they
82build on. Merge with --strategy ff or merge, or merge the stack into
83feat-a first
84#+end_example
85
86The second option is real: merging =B= into =feat-a= while =A= is open
87is allowed and collapses =B= into =A=, whose head moves as on any push
88to its branch. Closing =A= without merging leaves the stack alone; its
89branch still exists and =B= still targets it.
90
91Merge gates apply per layer as on any merge request: required
92approvals, resolved threads, green checks, and =require_signed_commits=,
93which under a stack already forces fast-forward.
94
95* Where it works
96
97- CLI and bare SSH: everything above.
98- Web: the header shows the stack both ways. Creating a merge request
99 against a branch that is another's source works from the form; the
100 stack appears once it exists.
101- iOS: renders the retarget comment; no stack view yet.
102- Same repository only. A fork's branch is not something another merge
103 request can target, so a stack cannot cross a fork.
104
105See [[Parity][Parity]] for the row.
106
107* A stack that merged
108
109The six merge requests that shipped v1.6.0 were the first stack merged
110on gitbay.org, one commit each, on 2026-09-02:
111
112| MR | source | target |
113|------+-----------------+-----------------|
114| !159 | stack-1-reaper | main |
115| !160 | stack-2-runners | stack-1-reaper |
116| !161 | stack-3-healthz | stack-2-runners |
117| !162 | stack-4-monitor | stack-3-healthz |
118| !163 | stack-5-lfs | stack-4-monitor |
119| !164 | stack-6-verify | stack-5-lfs |
120
121Each was merged with =mr merge --strategy ff= in that order. After each
122one, the next reported =target_ref: main= and no =stacked_on=, with the
123comment naming the merge — =retargeted from stack-1-reaper to main:
124!159 merged= on !160, and so on up to !164. No =mr retarget= was typed.
125=main= ended with the six commits in order on top of the commit that
126had added stacking.
127
128One thing the run exposed: each fast-forward queued the commit's CI jobs
129again on =main=, although the same commit had just passed them on its
130branch. That is [[https://gitbay.org/krz/gitbay/issues/90][#90]].
Users.org +6
@@ -333,6 +333,12 @@ approvals required, a =CODEOWNERS= file on the target branch (root or
333333=.gitbay/=) additionally demands an approval from an owner of every
334334owned changed file — gitignore-style patterns, last match wins.
335335
336A merge request whose target is another open merge request's source
337branch is stacked on it: =mr create= says so, =mr show= carries
338=stacked_on= and =stacked=, and merging the lower one retargets the
339upper onto =main= with its reviews kept. See [[Stacked-MRs][Stacked
340merge requests]].
341
336342Review threads anchor to diff lines:
337343
338344#+begin_src sh