Skip to content

Commit cce949f

Browse files
committed
worktree(feat): Apply native checkout policy
why: Existing pinned worktrees could stay at the wrong commit, and dirty policy and retained recovery results did not reach worktree execution. what: - Resolve typed targets and use native Git policy for linked checkouts - Carry parent transport options, remotes, and explicit detach behavior - Authorize discard before included main-checkout mutations - Retain successful and failed recovery results in both CLI entrypoints - Cover native positions, state, creation, and structured output
1 parent 5c7cd47 commit cce949f

8 files changed

Lines changed: 834 additions & 335 deletions

File tree

‎CHANGES‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ worker and native process group before further work. Interrupted results
5252
retain unknown state and report discovered recovery tokens in human and
5353
machine output. CLI synchronization requires POSIX process groups.
5454

55+
#### Apply checkout policy to linked worktrees
56+
57+
{ref}`cli-worktree-sync` and {ref}`cli-sync` with `--include-worktrees`
58+
resolve typed targets, honor drift and dirty policy, and retain native
59+
recovery results. Existing tag and commit checkouts move when their target
60+
changes. Parent Git options, selected remotes, and explicit detachment apply
61+
to worktree updates. Missing worktrees use native linked-worktree creation.
62+
63+
Configured discard requires `--yes`; included worktree policies are checked
64+
before the main checkout changes. JSON and NDJSON retain exact recovery
65+
locations after restoration or conflict, and human output shows recovery
66+
identity. Read-only worktree planning never fetches.
67+
5568
#### Configure partial clones and typed backend options
5669

5770
`git`, `hg`, and `svn` blocks match their libvcs options classes. Loading

‎docs/cli/worktree/index.md‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ prune
4040

4141
Worktrees are configured as a list under a repository entry. Each worktree
4242
requires a `dir` (relative to workspace root or absolute) and exactly one
43-
ref type: `tag`, `branch`, or `commit`.
43+
ref type: `tag`, `branch`, `commit`, or native `rev`.
4444

4545
```yaml
4646
~/code/:
@@ -68,12 +68,17 @@ ref type: `tag`, `branch`, or `commit`.
6868
|-------|----------|-------------|
6969
| `dir` | yes | Worktree path (relative to workspace root or absolute) |
7070
| `tag` | one of | Tag to checkout (creates detached HEAD) |
71-
| `branch` | one of | Branch to checkout (can be updated/pulled) |
71+
| `branch` | one of | Branch to follow with fast-forward updates |
7272
| `commit` | one of | Commit SHA to checkout (creates detached HEAD) |
73+
| `rev` | one of | Native revision expression; attachment follows its resolved kind |
74+
| `remote` | no | Fetch source for a branch target |
75+
| `sync` | no | Drift and dirty policies described in {ref}`cli-worktree-sync` |
76+
| `detach` | no | Keep the resolved target detached, including a branch |
7377
| `lock` | no | Lock the worktree to prevent accidental removal |
7478
| `lock_reason` | no | Reason for locking (implies `lock: true`) |
7579

76-
Exactly one of `tag`, `branch`, or `commit` must be specified per entry.
80+
Exactly one of `tag`, `branch`, `commit`, or `rev` must be specified per entry.
81+
Worktrees inherit the parent repository's Git transport options and remotes.
7782

7883
## Integration with vcspull sync
7984

@@ -111,7 +116,7 @@ Each worktree entry emits:
111116
"action": "unchanged",
112117
"exists": true,
113118
"is_dirty": false,
114-
"detail": "tag worktree already exists",
119+
"detail": "already at configured target",
115120
"error": null
116121
}
117122
```
@@ -133,13 +138,17 @@ log aggregation.
133138

134139
vcspull refuses to touch work it could destroy:
135140

136-
**Uncommitted changes stop updates.** If a worktree has uncommitted changes,
137-
vcspull will not update or remove it — commit or stash first. These entries
138-
show as blocked (`⚠`) in {ref}`vcspull worktree list <cli-worktree-list>`.
141+
**Dirty policy governs updates.** Updates abort on local changes by default.
142+
Configure `sync.dirty: preserve` to retain and restore them, or configure
143+
`discard` and pass `--yes` to authorize their removal. See
144+
{ref}`cli-worktree-sync` for recovery output and drift policy. Worktree
145+
pruning still refuses dirty checkouts.
139146

140147
**Unknown refs stop creation.** If a configured tag, branch, or commit
141-
doesn't exist in the repository, the entry is reported as an error (`✗`)
142-
rather than guessed at. Fetch from the remote to make the ref available.
148+
cannot be resolved, the entry is reported as an error (`✗`). Preview and
149+
list operations only inspect local metadata. Creating a missing worktree
150+
requires a locally available target; existing worktrees can fetch during
151+
follow synchronization.
143152

144153
**Locks guard against removal.** Worktrees created with `lock: true` or
145154
`lock_reason` are locked via `git worktree lock`, preventing accidental

‎docs/cli/worktree/sync.md‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,43 @@ Sync all configured worktrees:
2222
$ vcspull worktree sync '*'
2323
```
2424

25-
The sync subcommand:
26-
- Creates missing worktrees at the configured `dir`
27-
- Pulls latest changes for `branch` worktrees
28-
- Leaves `tag` and `commit` worktrees unchanged (they're immutable)
29-
- Skips worktrees with uncommitted changes (BLOCKED)
25+
Missing worktrees require locally available target metadata and use Git's
26+
linked-worktree operation. Existing
27+
worktrees follow their configured `branch`, `tag`, `commit`, or native `rev`;
28+
changing a tag or commit target moves the checkout to that resolved commit.
29+
Branch updates are fast-forward only. A matching commit is not drift, even
30+
when its branch or tag name differs. `detach: true` keeps a branch target
31+
detached, and `remote` chooses the branch's fetch source.
32+
33+
Each worktree accepts the same `sync` policies as {ref}`cli-sync`:
34+
`drift: keep` leaves an existing checkout untouched; `drift: warn` also logs
35+
when its resolved target differs. The default `dirty: abort` blocks updates
36+
with local changes. `dirty: preserve` retains native recovery material and
37+
restores changes, including the Git index. Conflicts retain their recovery
38+
token and make the operation fail.
39+
40+
`dirty: discard` requires explicit invocation consent, including clean or
41+
missing checkouts:
42+
43+
```console
44+
$ vcspull worktree sync --yes '*'
45+
```
46+
47+
For {ref}`cli-sync` with `--include-worktrees`, the same `--yes` authorizes
48+
configured discard. Worktree consent is checked before the main checkout
49+
changes. Main and linked checkouts are synchronized serially.
50+
51+
Both commands print retained recovery identity after success or failure.
52+
JSON and NDJSON include update/preservation states, ordered errors,
53+
conflicts, and the exact token location required by libvcs recovery. Tokens
54+
remain retained until explicitly released through libvcs; synchronization
55+
does not automatically restore interrupted operations into their source.
3056

3157
## Dry run
3258

33-
Preview what would happen without making changes:
59+
Preview local target metadata without fetching or changing checkouts. An
60+
unavailable target produces an error plan; execution may resolve it after
61+
fetching:
3462

3563
```console
3664
$ vcspull worktree sync --dry-run '*'

‎src/vcspull/_internal/sync.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from libvcs.sync.hg import HgOptions
1515
from libvcs.sync.svn import SvnOptions
1616

17+
from vcspull import exc
1718
from vcspull.validator import validate_working_copy
1819

1920

@@ -91,3 +92,10 @@ def sync_result_data(result: SyncResult) -> dict[str, t.Any]:
9192
{"step": error.step, "message": error.message} for error in result.errors
9293
],
9394
}
95+
96+
97+
def require_discard_authorization(policy: SyncPolicy, *, allow_discard: bool) -> None:
98+
"""Require explicit invocation consent before constructing a discard backend."""
99+
if policy.dirty == "discard" and not allow_discard:
100+
message = "dirty: discard requires --yes before synchronization"
101+
raise exc.VCSPullException(message)

0 commit comments

Comments
 (0)