Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,11 @@ Before merging, the stack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
before failing if checks are not ready.
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
before failing if merge readiness is not reached.

By default, a branch failure skips that branch's upstack descendants,
but independent sibling branches continue.
Expand All @@ -414,12 +416,12 @@ Use --fail-fast to stop the queue after the first branch failure.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-branch-check`: Skip stale base validation before merging.
* `--fail-fast`: Stop the merge queue after the first branch failure.
* `--branch=NAME`: Branch whose stack to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice stack restack {#gs-stack-restack}

Expand Down Expand Up @@ -717,7 +719,7 @@ This command acts as a local merge queue:
it merges one Change Request,
waits for that merge to finish,
restacks and updates the next Change Request,
waits for its CI checks to pass,
waits for merge readiness on the updated Change Request,
and then repeats the process.

For a stack like this:
Expand All @@ -735,13 +737,15 @@ Before merging, the downstack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
(default: 30m, 0 means fail immediately if not ready).

Between merges, the command waits for each merge
to complete, restacks and updates the next PR,
waits for CI checks on the updated PR,
waits for merge readiness on the updated PR,
and syncs merged branch cleanup.

Use --no-wait for single branch merging
Expand All @@ -751,12 +755,12 @@ when you don't want to wait for the merge to propagate.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-wait`: Skip polling for a single branch merge to propagate.
* `--no-branch-check`: Skip stale base validation before merging.
* `--branch=NAME`: Branch to start merging from

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice downstack edit {#gs-downstack-edit}

Expand Down Expand Up @@ -1226,16 +1230,18 @@ Use --branch to merge a different branch.
The branch must be based directly on trunk.
To merge a stacked branch, use 'gs downstack merge'.

Before merging, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait.
Before merging, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait.

**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--branch=NAME`: Branch to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice branch submit {#gs-branch-submit}

Expand Down
1 change: 1 addition & 0 deletions doc/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ nav:
- guide/concepts.md
- guide/branch.md
- guide/cr.md
- guide/worktrees.md
- guide/limits.md
- guide/troubleshooting.md
- guide/internals.md
Expand Down
178 changes: 178 additions & 0 deletions doc/src/guide/worktrees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
title: Parallel worktrees
icon: octicons/git-branch-16
description: >-
Work on a stack from several worktrees at once with anchors, and take the
whole repository for yourself with exclusive mode.
---

# Parallel worktrees

<!-- gs:version unreleased -->

A single git-spice repository can be worked on from several
[Git worktrees](https://git-scm.com/docs/git-worktree) at once:
one per agent, one for CI, one for you.
The worktrees are the coordination mechanism.
Git already refuses to check out the same branch in two worktrees,
so each worktree owns the slice of the stack it has checked out,
and git-spice makes that ownership legible
instead of layering a separate lock on top.

This page covers two halves of that model:

- **[Anchors](#anchors)** let many processes share one stack in parallel,
each scoped to its own region.
- **[Exclusive mode](#exclusive-mode)** lets one process take the whole
repository to reorganize the stack without contention.

## Anchors

An **anchor** is the branch a worktree is anchored at:
the lower boundary of the region that worktree works on.
Stacks created in the worktree build on top of the anchor,
so `gs repo sync` and restacks in different worktrees
never contend on a single shared trunk checkout.

There are two flavors:

- A **root anchor** is a per-worktree pointer branch that tracks the same
remote trunk as the main checkout.
Because it is its own branch, each worktree can update its view of the
trunk independently.
- An **internal anchor** pins a worktree at an existing tracked branch
owned by another worktree:
a dependent worktree, for building on top of work that is still in flight.

### Creating a worktree

Use $$gs anchor create$$ to create a worktree and its anchor in one step.

```freeze language="terminal" float="right"
{green}${reset} gs anchor create ../feat
{green}INF{reset} Created worktree at ../feat
{green}INF{reset} Created anchor feat tracking main
```

By default this creates a root anchor:
a pointer branch named after the worktree directory
(or set with `--name`) that tracks the remote trunk.

```bash
gs anchor create ../feat
```

Pass `-b`/`--branch` to also create and check out a tracked branch
stacked on the anchor, ready to work on:

```bash
gs anchor create ../feat -b feat-login
```

### Dependent worktrees

To build on top of a branch that is still under review in another worktree,
anchor the new worktree on that branch with `--anchor`:

```bash
gs anchor create ../feat-ui --anchor feat-login -b feat-ui
```

The branch named by `--anchor` must already be tracked by git-spice.
The new worktree's stack is based on it,
so syncing the dependency forward flows into the dependent work.

!!! note

Use `--no-anchor` to skip the anchor entirely and start the worktree
in detached `HEAD` at the current trunk commit,
matching plain `git worktree add` behavior.

### Managing worktrees

| Command | Purpose |
|---------|---------|
| $$gs anchor list$$ | List the registered anchors, the worktree that owns each, and whether it is a root or internal anchor. |
| $$gs anchor track$$ | Register an existing worktree's anchor branch with git-spice. |
| $$gs anchor rm$$ | Remove a worktree and dissolve its anchor. |

$$gs anchor rm$$ refuses to remove a worktree with uncommitted changes
unless `--force` is given.
Run it from a different worktree (for example, the primary checkout):
a worktree cannot remove itself.
A worktree may own only one anchor,
so $$gs anchor track$$ refuses to register a second anchor for it.

## Exclusive mode

Anchors keep parallel processes out of each other's way,
but sometimes you need to step back and reorganize the whole stack:
re-order branches, fold several together, or run an interactive rebase
across the trunk.
Those operations touch branches that other worktrees have checked out,
which Git will refuse.

**Exclusive mode** hands the entire repository to a single process.
$$gs repo park$$ records every linked worktree in a durable manifest
and removes its directory; the branches themselves are left untouched,
so the whole graph stays reachable from the primary checkout.
You reorganize freely, then $$gs repo restore$$ re-creates the worktrees
at their branches' current tips.

```freeze language="terminal"
{green}${reset} gs repo park
{green}INF{reset} Parked worktree ../feat
{green}INF{reset} Parked worktree ../feat-ui
{green}INF{reset} Parked 2 worktree(s); repository is in exclusive mode
{gray}# reorganize the stack here{reset}
{green}${reset} gs repo restore
{green}INF{reset} Restored worktree ../feat
{green}INF{reset} Restored worktree ../feat-ui
{green}INF{reset} Restored 2 worktree(s); exclusive mode cleared
```

The manifest is written **before** any worktree is removed,
and records each worktree's path, branch, `HEAD` commit, and anchor.
If a park or restore is interrupted partway through,
re-running the same command finishes the job:
park resumes from where it stopped,
and restore skips worktrees that already exist.
Nothing is lost to a `Ctrl-C` mid-reorganization.

A reorganization may delete or rename a parked branch —
an interactive rebase that drops it, for example.
git-spice cannot know where that work landed in the rewritten graph,
so $$gs repo restore$$ does not guess:
it restores every other worktree,
stays in exclusive mode,
and reports the missing branch.
Recover by re-creating the branch and re-running $$gs repo restore$$,
or discard that worktree with `gs repo restore --forget <path>`.
Each parked commit is preserved under `refs/gs-park/`
so it is not lost to garbage collection before you recover it.
A worktree parked in detached `HEAD` is restored at its recorded commit.

While the repository is parked, $$gs anchor create$$ is refused —
new worktrees would race with the reorganization in progress.
Run $$gs repo restore$$ first.

!!! warning "Uncommitted changes"

$$gs repo park$$ refuses to park a worktree with uncommitted changes.
Commit them first, or pass `--force` to discard them.

### Wrapping a single command

To take exclusive mode for just one command,
use $$gs repo exclusive$$.
It parks the worktrees, runs the command, and always restores them
afterward — even if the command fails.

Separate the command from git-spice's own flags with `--`:

```bash
gs repo exclusive -- git rebase -i main
```

This is the safe way to run a one-off reorganization
without leaving the repository parked if something goes wrong.
2 changes: 1 addition & 1 deletion internal/handler/sync/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ func TestHandler_SyncTrunk_deletesCurrentBranchInAnchorWorktree(t *testing.T) {

mockRestack := NewMockRestackHandler(ctrl)
mockRestack.EXPECT().
RestackBranch(gomock.Any(), "child").
RestackBranch(gomock.Any(), "child", gomock.Nil()).
Return(nil)

mockAutostash := NewMockAutostashHandler(ctrl)
Expand Down
12 changes: 6 additions & 6 deletions internal/handler/sync/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading