Browse documentation
Repositories and Changes
Submit and stacking
How com submit turns work into a reviewable Change, and how to stack dependent changes from a Composal-native checkout or a plain Git clone.
On this page
com submit turns local work into a reviewable Change. It never rewrites the
target branch itself — landing does that, separately, once review and CI gates pass. A
stack is just what you get when one submitted Change is built directly on another:
Composal tracks the relation and lands the stack in dependency order, and where the repository
is GitHub-synced, projects each Change to its own pull request with the parent's branch
as the base rather than one flat diff against trunk.
There are two ways to reach com submit, and this page covers both:
- A Composal-native checkout — cloned with
com clone, working against the JJ change graph directly. This is the deep, first-class path: stacking is implicit in how you build commits. - A plain Git clone — no native Composal workspace at all, just
git cloneand an ordinary branch.com submitstill works here, and branches can still stack, but the relationship has to be named explicitly.
The submit model
Regardless of which mode you're in, com submit does the same three things: import the
exact content you built, create or update the Composal Change and its patchset, and (where the
repository is GitHub-synced) project a matching pull request. Re-submitting the same work
appends a patchset to the existing Change rather than creating a new one — com submit is
safe to run again after a small fix, a rebase, or a retried network call.
Compare patchsets
Use the P1/P2 slider on a Change's Diff tab to compare submitted revisions. For native JJ patchsets, Composal replays the older patchset onto the newer patchset's base before comparing them, so unrelated changes introduced by a rebase stay out of the diff. If that replay conflicts, the comparison reports an error.
Use Other Refs to select bookmarks, commits, or patchset bases for a direct tree comparison. Its labels distinguish a patchset from its base; moving the slider switches back to a patchset comparison. Selecting two patchsets in Other Refs also uses the patchset comparison.
Stacking from a Composal-native checkout
In a Composal-native checkout there is no separate "stack" command to create one. A stack is
just the ordinary JJ change graph: com new starts the next change on top of wherever
you are, so a chain of com new calls is already a stack before you submit anything.
com new main -m "feat: add the storage layer"
# ...edit files...
com new -m "feat: use the storage layer in the API"
# ...edit files...
com submit -r 'main..@' --target main
com submit -r 'main..@' submits every change in that range as its own Change, each one
still carrying its place in the graph. Composal numbers parents before children, so the change
numbers themselves read in dependency order. Submitting a single change at a time works
the same way — com submit --target main from @ submits just the current change, and
Composal still detects its parent from the JJ graph if that parent was submitted too.
Inspect and manage a stack's identity with com stack:
com stack list # active graph-derived stacks in this repository
com stack show '#42' # the stack a given Change belongs to
com stack name my-feature # give a stack a stable bookmark alias
If the target branch moves while your stack is still open, com sync fetches it and
restacks your submitted changes on top, then resubmits — pass --no-restack to pull and
resubmit without rewriting anything locally, and --restack to say so explicitly. A
GitHub-synced repository projects each Change in the stack to its own pull request, base
branch chained to the parent's branch automatically.
Submitting (and stacking) from a plain Git clone
com submit also works from an ordinary git clone — no .jj directory, no native Composal
workspace. Push a branch, then submit it:
git clone https://github.com/<org>/<repo>.git
git checkout -b feature/widget
# ...commit work...
git push origin feature/widget
com submit --target main
Composal re-fetches the exact branch head itself (it does not trust a locally-computed diff),
imports the commits between the target and your branch, and creates the Change and
pull request. Push again and re-run com submit to append a patchset — the branch is
adopted, never rewritten out from under you.
Stacking is explicit in this mode. There is no local graph for Composal to read a parent
out of, so name the parent branch with --target:
git checkout -b feature/widget-styling feature/widget
# ...commit work built on top of feature/widget...
git push origin feature/widget-styling
com submit --target feature/widget
Composal resolves feature/widget to the Change that already adopted it, and the new Change
inherits that Change's landing target rather than trying to land onto feature/widget
itself — the pull request Composal creates uses feature/widget as its base, so it renders as
a genuinely stacked PR, not a flat diff against main.
Two things this mode requires that a Composal-native checkout does not:
- Submit bottom-up. The parent branch has to already be submitted (and pushed) before you submit a branch stacked on it — Composal needs to find an existing Change for the parent to attach to.
- Rebase forward, don't fall behind. A stacked branch has to be built on its parent's
current pushed head. If the parent branch has moved since you branched from it,
com submitrejects the child with astack_parent_not_at_tiperror rather than importing a stale diff — rebase your branch onto the parent's latest commit, push, and submit again.
There is no local restack for this mode: when a parent branch changes, git rebase the
child branch yourself and push, the same as you would with any other Git remote.
Commands, side by side
| Task | Composal-native checkout | Plain Git clone |
|---|---|---|
| Start the next change in a stack | com new | git checkout -b <branch> <parent-branch> |
| Submit one change | com submit --target main | com submit --target main |
| Submit a whole stack at once | com submit -r 'main..@' --target main | not available — submit each branch after its parent |
| Stack on another submitted change | automatic (JJ parent) | com submit --target <parent-branch> |
| Update after a small fix | edit, then com submit again | commit, git push, then com submit again |
| Pull the target and reconcile | com sync --target main | git pull on the target, then resubmit |
| Inspect a stack | com stack show '#42' | com change show '#42' (walk parent_change) |
| Land | com land '#42' | com land '#42' |
Landing a stack
Landing is identical once a Change exists, in either mode: com land '#N' requests
server-verified landing for that Change and its submitted-together closure — every open
ancestor it depends on lands first, in order. com land '#N' --dry-run prints the
computed closure and gate state without requesting anything; --when-ready queues the
change and lets the server land it once CI and approvals clear instead of refusing while
they're outstanding.
A child cannot land ahead of its parent. If the parent is still open, landing the child lands the whole stack up to that point in one queued request — you do not need to land each Change in a stack by hand.
If the queue reports that a candidate became stale, its error identifies the changed
target commit, ref, ref version, candidate base, or preceding queue entries. A preceding
entry may have left the queue or acquired a new validation or candidate. Run
com land '#N' --dry-run to check the current landing gates, then retry
com land '#N' to build a candidate against the current target and queue.
Queue health over time
The merge queue's Queue health panel uses the selected 7, 14, or 30-day reporting window. It shows merge conflicts, operational errors, retries, candidate rebuilds, and worker recoveries by UTC day, with a breakdown of failure categories. A rebuild can be normal when an earlier queue entry or candidate input changes.
Landing success is the share of attempts that landed among landed, ejected, and needs-attention attempts. Cancellations are excluded. Retrying starts another attempt, so it does not erase the earlier failure. Recoverable errors indicate that the queue deferred work for another attempt; worker recoveries indicate that processing resumed after a worker lease expired.
Health history starts when reporting is enabled. It can lag recent activity; when analytics is unavailable, the panel uses live recorded events and labels the fallback. Existing queue timing charts may contain older history.