Skip to content
20 changes: 15 additions & 5 deletions docs/src/content/docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,26 @@ A repo-level variable like `{{ default_branch }}` needs no deferral: it is ident
```toml
[aliases]
up = '''
git fetch --all --prune && wt step for-each -- sh -c '
git fetch --all --prune; wt step for-each -- sh -c '
git rev-parse --verify -q @{u} >/dev/null || exit 0
g=$(git rev-parse --git-dir)
test -d "$g/rebase-merge" -o -d "$g/rebase-apply" && exit 0
git update-index --refresh -q >/dev/null || true
git rebase @{u} --no-autostash || git rebase --abort
rebasing() { test -d "$g/rebase-merge" || test -d "$g/rebase-apply"; }
rebasing && exit 0
git diff --quiet HEAD || { git merge --ff-only --no-autostash @{u}; exit 0; }
git rebase @{u} --no-autostash || { rebasing && git rebase --abort; }
''''
```

`wt up` fetches all remotes, then iterates every worktree: skip if no upstream, skip if mid-rebase, refresh the index to drop stale stat entries, then rebase and auto-abort on conflict. It rebases onto git-native `@{u}` rather than a `{{ … }}` template, so git resolves each worktree's own upstream and there is nothing to defer.
`wt up` fetches every remote, then brings each worktree up to date with its upstream: skip if there is no upstream or a rebase is already in progress, fast-forward if a tracked file is modified or staged, otherwise rebase, aborting on conflict. It rebases onto git-native `@{u}` rather than a `{{ … }}` template, so git resolves each worktree's own upstream and there is nothing to defer.

A sweep finds each worktree in whatever state you left it, so most of the script is guards:

- `git fetch --all` exits non-zero when any single remote fails. With `&&`, one remote with lapsed credentials is enough to skip the whole sweep, so even worktrees whose refs fetched fine go unrebased. With `;` the sweep runs on what did fetch, and the fetch error still prints.
- `git rebase` refuses to run in a worktree with a modified or staged tracked file, whether or not there is anything to rebase — so a worktree you are editing would fail the sweep. `git merge --ff-only` is the piece of the rebase git will still do there: it advances a branch that is simply behind, and otherwise changes nothing — a diverged branch, or an incoming file that collides with your edits, leaves the worktree exactly as it was, with the reason printed. Untracked files trigger neither the refusal nor the `git diff` guard, so a worktree carrying only new files still rebases.
Comment thread
max-sixty marked this conversation as resolved.
Resolved
- `rebasing` guards the abort as well as the skip. `git rebase` fails two ways: conflicting partway, which leaves a rebase in progress to abort, and refusing to start, which leaves nothing. After a refusal, an unconditional `git rebase --abort` would answer `fatal: no rebase in progress` — exit 128 in place of git's own message and exit code.
- Both arms pass `--no-autostash`, because a global `rebase.autostash` or `merge.autostash` breaks what each arm relies on. An autostash that pops with conflicts leaves the markers in the worktree and still exits 0, so the sweep would report success on a worktree it had just left in conflict — and an autostashed tree is momentarily clean, so a fast-forward that should have refused goes through and the collision lands on the pop instead.

The sweep exits non-zero only when a worktree fails for a reason worth stopping on. That matters where the alias is a hook step, since a non-zero step stops the rest of the pipeline.
Comment thread
max-sixty marked this conversation as resolved.
Resolved

### Recipe: move or copy in-progress changes to a new worktree

Expand Down
20 changes: 15 additions & 5 deletions plugins/worktrunk/skills/worktrunk/reference/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,26 @@ A repo-level variable like `{{ default_branch }}` needs no deferral: it is ident
```toml
[aliases]
up = '''
git fetch --all --prune && wt step for-each -- sh -c '
git fetch --all --prune; wt step for-each -- sh -c '
git rev-parse --verify -q @{u} >/dev/null || exit 0
g=$(git rev-parse --git-dir)
test -d "$g/rebase-merge" -o -d "$g/rebase-apply" && exit 0
git update-index --refresh -q >/dev/null || true
git rebase @{u} --no-autostash || git rebase --abort
rebasing() { test -d "$g/rebase-merge" || test -d "$g/rebase-apply"; }
rebasing && exit 0
git diff --quiet HEAD || { git merge --ff-only --no-autostash @{u}; exit 0; }
git rebase @{u} --no-autostash || { rebasing && git rebase --abort; }
''''
```

`wt up` fetches all remotes, then iterates every worktree: skip if no upstream, skip if mid-rebase, refresh the index to drop stale stat entries, then rebase and auto-abort on conflict. It rebases onto git-native `@{u}` rather than a `{{ … }}` template, so git resolves each worktree's own upstream and there is nothing to defer.
`wt up` fetches every remote, then brings each worktree up to date with its upstream: skip if there is no upstream or a rebase is already in progress, fast-forward if a tracked file is modified or staged, otherwise rebase, aborting on conflict. It rebases onto git-native `@{u}` rather than a `{{ … }}` template, so git resolves each worktree's own upstream and there is nothing to defer.

A sweep finds each worktree in whatever state you left it, so most of the script is guards:

- `git fetch --all` exits non-zero when any single remote fails. With `&&`, one remote with lapsed credentials is enough to skip the whole sweep, so even worktrees whose refs fetched fine go unrebased. With `;` the sweep runs on what did fetch, and the fetch error still prints.
- `git rebase` refuses to run in a worktree with a modified or staged tracked file, whether or not there is anything to rebase — so a worktree you are editing would fail the sweep. `git merge --ff-only` is the piece of the rebase git will still do there: it advances a branch that is simply behind, and otherwise changes nothing — a diverged branch, or an incoming file that collides with your edits, leaves the worktree exactly as it was, with the reason printed. Untracked files trigger neither the refusal nor the `git diff` guard, so a worktree carrying only new files still rebases.
- `rebasing` guards the abort as well as the skip. `git rebase` fails two ways: conflicting partway, which leaves a rebase in progress to abort, and refusing to start, which leaves nothing. After a refusal, an unconditional `git rebase --abort` would answer `fatal: no rebase in progress` — exit 128 in place of git's own message and exit code.
- Both arms pass `--no-autostash`, because a global `rebase.autostash` or `merge.autostash` breaks what each arm relies on. An autostash that pops with conflicts leaves the markers in the worktree and still exits 0, so the sweep would report success on a worktree it had just left in conflict — and an autostashed tree is momentarily clean, so a fast-forward that should have refused goes through and the collision lands on the pop instead.

The sweep exits non-zero only when a worktree fails for a reason worth stopping on. That matters where the alias is a hook step, since a non-zero step stops the rest of the pipeline.

### Recipe: move or copy in-progress changes to a new worktree

Expand Down
20 changes: 15 additions & 5 deletions skills/worktrunk/reference/extending.md

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

Loading