Skip to content

Commit 1afcfaa

Browse files
committed
Refactor stack management into forest command group with conflict-preserving rebase
This branch consolidates stack operations (split, merge, reroot) under a unified forest command group. Replaces separate consolidate/split commands with forest subcommands that provide better UX and enable conflict-preserving rebase workflows. ## Files Changed ### Added (14 files) - `src/erk/cli/commands/forest/` - Forest command group (list, show, rename, split, merge, reroot operations) - `src/erk/core/forest_ops.py` - Core forest operations (forest membership logic, tree queries) - `src/erk/core/forest_types.py` - Forest dataclasses (ForestMetadata, Forest, ForestWorkTree) - `src/erk/core/forest_utils.py` - Forest utilities (membership calculation, worktree grouping) - `tests/commands/forest/` - Forest command tests (split, merge, reroot workflows) - `tests/core/test_forest_*.py` - Forest operations tests - `tests/fakes/forest_ops.py` - Fake forest operations for testing ### Deleted (4 files) - `src/erk/cli/commands/consolidate.py` - Replaced by forest merge command - `src/erk/cli/commands/split/` - Replaced by forest split command - `tests/cli/commands/split/` - Test consolidation ### Modified (31 files) - Git integrations (RealGit, NoopGit, PrintingGit) - New abstract methods for reroot - CLI (cli.py) - Register forest group, remove old commands - Create command - Auto forest lifecycle management - Documentation (glossary.md, erk.md) - Forest terminology, updated references ## Key Changes - **Forest command group**: Unified interface for split, merge, reroot, list, show, rename operations - **Conflict-preserving rebase**: Reroot operation saves conflict states in git history with `[CONFLICT]` commit markers - **Auto forest lifecycle**: Create command now auto-creates or joins forests based on Graphite parent/child relationships - **Git layer enhancements**: 6 new abstract methods (rebase_branch, get_conflicted_files, commit_with_message, is_rebase_in_progress, abort_rebase, get_commit_sha) - **Documentation**: Added forest glossary entries and updated integration examples ## Critical Notes - Old consolidate/split commands removed; forest commands provide complete replacement - Reroot requires Graphite enabled, clean working directory, and all branches to have worktrees - Forest names are labels only - renaming never affects worktree paths
1 parent 22a5776 commit 1afcfaa

File tree

83 files changed

+5122
-3410
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+5122
-3410
lines changed

.claude/skills/erk/references/erk.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@ repo = discover_repo_context(ctx, Path.cwd())
11271127
**List worktrees:**
11281128

11291129
```python
1130-
worktrees = ctx.git_ops.list_worktrees(repo.root)
1130+
worktrees = ctx.git.list_worktrees(repo.root)
11311131
# Returns: list[WorktreeInfo]
11321132
```
11331133

@@ -1176,7 +1176,7 @@ def test_list_worktrees():
11761176
)
11771177
ctx = ErkContext(git_ops=git_ops, ...)
11781178

1179-
worktrees = ctx.git_ops.list_worktrees(Path("/repo"))
1179+
worktrees = ctx.git.list_worktrees(Path("/repo"))
11801180
assert len(worktrees) == 2
11811181
```
11821182

@@ -1189,7 +1189,7 @@ Commands behave differently based on current directory:
11891189
repo = discover_repo_context(ctx, Path.cwd())
11901190

11911191
# Different behavior in root vs worktree
1192-
current_branch = ctx.git_ops.get_current_branch(Path.cwd())
1192+
current_branch = ctx.git.get_current_branch(Path.cwd())
11931193
if current_branch == repo.default_branch:
11941194
# In root worktree
11951195
else:
@@ -1203,7 +1203,7 @@ Check if Graphite is available:
12031203
```python
12041204
if ctx.global_config and ctx.global_config.use_graphite:
12051205
# Use Graphite features
1206-
parent = ctx.graphite_ops.get_parent_branch(branch)
1206+
parent = ctx.graphite.get_parent_branch(branch)
12071207
```
12081208

12091209
### GitHub Integration

docs/agent/glossary.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,66 @@ A **managed worktree** created and maintained by the erk tool.
3838

3939
**Example**: `erk create my-feature` creates both a git worktree and an erk.
4040

41+
### Forest
42+
43+
A **named collection of worktrees** belonging to the same Graphite stack.
44+
45+
**Purpose**: Enables unified stack management operations like split, merge, and reroot.
46+
47+
**Key characteristics**:
48+
49+
- Forest names are pure labels stored in `forests.toml` metadata
50+
- Renaming a forest NEVER moves worktrees or changes filesystem paths
51+
- Worktree paths remain stable: `~/.erk/repos/<repo>/worktrees/<original-name>/`
52+
- Membership determined automatically by Graphite stack structure
53+
54+
**Lifecycle**:
55+
56+
- Created automatically when `erk create <name>` called from trunk
57+
- Joined automatically when `erk create -s <name>` called from forest member
58+
- Deleted automatically during `erk sync` when empty (zero worktrees)
59+
60+
**Example**: A forest named "auth" might contain worktrees `auth-redesign`, `add-oauth`, and `add-2fa`.
61+
62+
### Forest Membership
63+
64+
Automatic assignment of a worktree to a forest based on Graphite parent/child relationships.
65+
66+
**Rules**:
67+
68+
- A worktree can only belong to one forest at a time
69+
- If branch's parent is trunk → new forest OR join existing if siblings present
70+
- If branch's parent is in forest → join that forest
71+
72+
**Example**: Creating branch `add-oauth` from `auth-redesign` automatically adds it to the `auth` forest.
73+
74+
### Empty Forest
75+
76+
A forest with zero existing worktrees, automatically deleted during `erk sync`.
77+
78+
**Detection**: Checked by comparing forest metadata against actual worktree directories.
79+
80+
**Cleanup**: Automatic and silent during sync operations.
81+
82+
### Reroot
83+
84+
Conflict-preserving rebase operation for an entire forest.
85+
86+
**Purpose**: Rebase all branches in a stack while preserving conflict states in git history.
87+
88+
**Benefits**:
89+
90+
- Permanent audit trail of conflicts and resolutions
91+
- Better PR reviewability
92+
- Easier debugging of complex merges
93+
94+
**Commit format**:
95+
96+
- `[CONFLICT] Rebase conflicts from <parent> (<sha>)`
97+
- `[RESOLVED] Fix rebase conflicts from <parent> (<sha>)`
98+
99+
**Example**: `erk forest reroot auth` rebases all branches in the auth forest.
100+
41101
### Repo Root
42102

43103
The main git repository directory containing `.git/` directory.

src/erk/cli/cli.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from erk.cli.commands.checkout import checkout_cmd
44
from erk.cli.commands.completion import completion_group
55
from erk.cli.commands.config import config_group
6-
from erk.cli.commands.consolidate import consolidate_cmd
76
from erk.cli.commands.create import create
87
from erk.cli.commands.current import current_cmd
98
from erk.cli.commands.delete import del_cmd, delete_cmd
109
from erk.cli.commands.down import down_cmd
10+
from erk.cli.commands.forest import forest_group
1111
from erk.cli.commands.goto import goto_cmd
1212
from erk.cli.commands.init import init_cmd
1313
from erk.cli.commands.land_stack import land_stack
@@ -16,7 +16,6 @@
1616
from erk.cli.commands.prepare_cwd_recovery import prepare_cwd_recovery_cmd
1717
from erk.cli.commands.rename import rename_cmd
1818
from erk.cli.commands.shell_integration import hidden_shell_cmd
19-
from erk.cli.commands.split import split_cmd
2019
from erk.cli.commands.status import status_cmd
2120
from erk.cli.commands.sync import sync_cmd
2221
from erk.cli.commands.up import up_cmd
@@ -37,12 +36,12 @@ def cli(ctx: click.Context) -> None:
3736

3837
# Register all commands
3938
cli.add_command(completion_group)
40-
cli.add_command(consolidate_cmd)
4139
cli.add_command(create)
4240
cli.add_command(current_cmd)
4341
cli.add_command(down_cmd)
4442
cli.add_command(checkout_cmd)
4543
cli.add_command(checkout_cmd, name="co") # Alias
44+
cli.add_command(forest_group)
4645
cli.add_command(goto_cmd)
4746
cli.add_command(land_stack)
4847
cli.add_command(up_cmd)
@@ -55,7 +54,6 @@ def cli(ctx: click.Context) -> None:
5554
cli.add_command(del_cmd)
5655
cli.add_command(rename_cmd)
5756
cli.add_command(config_group)
58-
cli.add_command(split_cmd)
5957
cli.add_command(sync_cmd)
6058
cli.add_command(hidden_shell_cmd)
6159
cli.add_command(prepare_cwd_recovery_cmd)

0 commit comments

Comments
 (0)