Stacked Changes
Josh supports a stacked-changes workflow where a series of commits on a local branch can each be pushed as a separate, independently-reviewable unit. This is useful when working on a larger feature that is best reviewed in smaller, logical steps.
This feature is separate from Josh’s filtering functionality. It works with any
repository accessible via the josh CLI, regardless of whether you are working with a
filtered view of a monorepo or a plain repository.
Concepts
In a stacked changes workflow, each commit on your local branch represents one
self-contained change. When you push with --split or --stack, Josh creates a
separate git ref for each qualifying commit.
A commit qualifies for a separate ref — and an automatic PR, when forge integration is configured — only if both of the following are true:
- It has a change ID in the commit message footer (see below).
- Its author email matches the email configured in
user.emailin your git config.
Commits without a change ID, or authored by someone else, are silently skipped and are not pushed as individual changes.
Change IDs
A change ID is a short, stable identifier that you add manually to the footer of a commit message, using either of these footers:
Change: my-feature-part-1
or the Gerrit-compatible form:
Change-Id: I1234abcd...
The change ID must not contain @. It must be unique within the stack. It is what
allows josh push to match a commit to an existing PR across rebases and amends —
so once you have assigned an ID to a change, keep it stable.
Example commit message:
Add input validation to the login form
Validates that the email field is non-empty and well-formed before
submission. Returns an error message inline without clearing the form.
Change: login-form-validation
Push modes
There are two stacked push modes:
-
--split— Each commit is pushed as a minimal, independent diff. Josh strips away the context of earlier commits in the stack so that each change can be applied on its own. This is the recommended mode for GitHub PR stacks, because each PR shows only its own diff even before earlier PRs are merged. -
--stack— Each commit is pushed with its full upstream context preserved. The history of earlier commits is kept intact. Use this when reviewers need the full context of the stack to understand each individual change.
Workflow
1. Write your commits
Work on your feature normally, writing one commit per logical step. Add a Change:
footer to each commit you want to submit for review:
$ git commit -m "Add validation for input fields
Change: input-validation"
$ git commit -m "Wire validation into the form component
Change: form-wiring"
$ git commit -m "Add tests for form validation
Change: validation-tests"
Commits without a Change: footer are included in the push to the base branch but
do not get their own ref or PR.
2. Push as a stack
josh push --split
For each qualifying commit Josh pushes a ref under
refs/heads/@changes/<base>/<author>/<change-id>. With GitHub forge integration
enabled, a pull request is created (or updated) for each of these refs automatically.
The first change in the stack targets the repository’s default branch. Each subsequent PR targets the branch of the change before it. Intermediate PRs are automatically marked as draft until the changes before them are merged.
3. Iterate
After receiving review feedback, amend or rebase your commits as needed, keeping the
Change: footers intact:
git rebase -i HEAD~3 # edit commits, preserve Change: footers
josh push --split # re-push; existing PRs are updated, not recreated
As long as the change ID in the footer is preserved through your edits, josh push
updates the correct existing PRs rather than creating new ones.
4. Merge
Once a PR is approved and its required checks pass, merge it through the forge’s normal UI. Then sync your local branch to account for the merged commit:
josh pull --rebase --autostash
This rebases your remaining local commits on top of the updated upstream state.
--autostash ensures any uncommitted changes are preserved across the operation. After
pulling, the next josh push --split will retarget and promote the next PR in the stack
from draft to ready for review.
Without forge integration
josh push --split and josh push --stack work without
forge integration. Josh still pushes the individual
@changes/… refs to the upstream repository; you can then create pull requests from
them manually, or use them as part of a custom review workflow.