Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. It has a change ID in the commit message footer (see below).
  2. Its author email matches the email configured in user.email in 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.