feat(josh-sync): rename --subfolder now git mv's the actual directory

Previously --subfolder only repointed .josh-sync.yml, leaving the working
tree out of sync with the new path until a later filter-change
reconciliation happened to catch it. Now validates the old subfolder
exists and is tracked by git (proof it's actually managed by josh-sync)
and the new one doesn't already exist, then git mv's it — staged, not
committed, alongside the config edit. Idempotent on resume. Bumps to
v2.4.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNXgcFWVgSh3wkQS55cCX6
This commit is contained in:
2026-07-17 11:58:55 +01:00
co-authored by Claude Sonnet 5
parent be4aef588d
commit e5403e4300
6 changed files with 217 additions and 9 deletions
+5 -2
View File
@@ -687,14 +687,17 @@ josh-sync rename billing --name payments --dry-run # preview only, no writes
josh-sync rename billing --name payments --yes # skip the confirmation prompt
```
What it does, in order: resolves the target (and detects a resumed run if the config already shows the new name), validates the new name/URL, checks for recent sync activity on the target (best-effort concurrency heuristic — warns/aborts unless `--force`), refuses to proceed if state already exists under the new name (never forceable), edits `.josh-sync.yml` and re-validates it, then moves every file under `<target>/` on `josh-sync-state` to the new prefix in one commit.
What it does, in order: resolves the target (and detects a resumed run if the config already shows the new name), validates the new name/URL, checks for recent sync activity on the target (best-effort concurrency heuristic — warns/aborts unless `--force`), refuses to proceed if state already exists under the new name (never forceable), validates the subfolder move if `--subfolder` was given (see below), edits `.josh-sync.yml` and re-validates it, `git mv`s the subfolder in the monorepo working tree, then moves every file under `<target>/` on `josh-sync-state` to the new prefix in one commit.
**`--subfolder` moves the actual directory.** `josh-sync rename` must be run from inside the monorepo working tree (same as where `.josh-sync.yml` lives). Before writing anything, it checks that the old subfolder exists **and** is tracked by git (`git ls-files` under that path is non-empty — proof it's actually "managed by josh-sync", not a stale/misconfigured path) and that the new subfolder doesn't already exist. It then runs `git mv <old> <new>` — staged, like the config edit, but **not committed**; review the combined diff (moved files + `.josh-sync.yml`) and commit it yourself. This is idempotent: if you already moved the directory by hand (or a prior run got partway through), a re-run detects the new path already existing and the old one gone, and skips straight to updating state.
**What it does NOT do:**
- Rename or move the repository on the git host — do that first (Gitea/GitHub UI or API), then run `josh-sync rename` to update josh-sync's own bookkeeping.
- Rename `auto-sync/mono-*`/`auto-sync/subrepo-*` conflict/staging branches — they never carry the target name, so there's nothing to rename. An abandoned onboarding's `auto-sync/import-<old-name>-*` branch (which does carry the name) is only flagged with a warning; merge or delete it manually.
- Guarantee `.josh-sync.yml`'s comments survive byte-for-byte — the edit is a scoped `yq -i`, which may reflow surrounding formatting.
- Commit or push the config edit / directory move to the monorepo — both are left staged in your working tree for review, same as any other local change.
**Concurrency**: the heuristic checks for a `josh-sync-state` commit under the target within the last 5 minutes. It's best-effort — a sync that starts after the check but before rename's own push can still race. `--force` bypasses only this check; a destination-name conflict or unreachable new URL always hard-fails.
**Concurrency**: the heuristic checks for a `josh-sync-state` commit under the target within the last 5 minutes. It's best-effort — a sync that starts after the check but before rename's own push can still race. `--force` bypasses only this check; a destination-name conflict, an unreachable new URL, or a subfolder-move validation failure always hard-fails.
## Troubleshooting