feat(josh-sync): add rename command for safe target renames
Adds `josh-sync rename <target>` (PE-12): safely rename a sync target's name, subfolder, and/or subrepo_url in one resumable operation, editing .josh-sync.yml and migrating its state-branch files atomically instead of orphaning them. Bumps josh-sync to v2.3.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNXgcFWVgSh3wkQS55cCX6
This commit is contained in:
@@ -73,3 +73,89 @@ write_state() {
|
||||
|
||||
git worktree remove "$tmp_dir" 2>/dev/null || rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
# ─── Target-Prefix Migration (used by `rename`) ────────────────────
|
||||
# Generic helpers over the whole "<target_name>/" directory on the state
|
||||
# branch — agnostic to which command wrote the individual files inside it
|
||||
# (this file's per-branch state, or onboard.sh's onboard.json/adopt.json).
|
||||
|
||||
# List every path under <target_name>/ on the freshly-fetched state branch.
|
||||
# One relative path per line (e.g. "billing/main.json"); empty if none —
|
||||
# including when the state branch doesn't exist yet at all.
|
||||
state_list_target_files() {
|
||||
local target_name="$1"
|
||||
git fetch origin "$STATE_BRANCH" 2>/dev/null || true
|
||||
git rev-parse -q --verify "origin/${STATE_BRANCH}" >/dev/null 2>&1 || return 0
|
||||
git ls-tree -r --name-only "origin/${STATE_BRANCH}" -- "${target_name}/" 2>/dev/null
|
||||
}
|
||||
|
||||
# Move every file under <old_name>/ to <new_name>/ (basename preserved), and
|
||||
# — when new_filter is non-empty — force `.last_forward.josh_filter` to
|
||||
# new_filter in each moved JSON file that has that key (whatever value it
|
||||
# currently holds; we don't need to know the prior value, which keeps this
|
||||
# safe to re-run mid-rename after the config has already been edited).
|
||||
# Files without that key (e.g. onboard.json) pass through untouched. Both
|
||||
# the move and the content rewrite land in ONE commit + push.
|
||||
#
|
||||
# No-op (returns 0, no commit) if <old_name>/ has no files.
|
||||
# Dies before mutating anything if any destination path already exists.
|
||||
#
|
||||
# Usage: state_migrate_target_prefix <old_name> <new_name> [new_filter]
|
||||
state_migrate_target_prefix() {
|
||||
local old_name="$1" new_name="$2" new_filter="${3:-}"
|
||||
local files
|
||||
files=$(state_list_target_files "$old_name")
|
||||
[ -n "$files" ] || return 0
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
git worktree add "$tmp_dir" "origin/${STATE_BRANCH}" 2>/dev/null \
|
||||
|| { rm -rf "$tmp_dir"; die "Failed to check out ${STATE_BRANCH}"; }
|
||||
|
||||
# The migration itself runs in a subshell so a mid-flight `exit` (from a
|
||||
# collision `die`, or any unexpected failure) can't skip the worktree
|
||||
# cleanup below. Its exit status is captured explicitly and re-raised via
|
||||
# an unconditional `exit` — relying on the caller's `set -e` to propagate a
|
||||
# failure out of a subshell is NOT reliable (e.g. bats' `run` disables
|
||||
# errexit while capturing output), so this must not depend on it.
|
||||
local migrate_status=0
|
||||
(
|
||||
cd "$tmp_dir" || exit 1
|
||||
local f base dest
|
||||
|
||||
# Pre-flight collision check across all files before mutating anything.
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
base="${f#"${old_name}"/}"
|
||||
dest="${new_name}/${base}"
|
||||
if [ "$f" != "$dest" ] && [ -e "$dest" ]; then
|
||||
die "Destination '${dest}' already exists on ${STATE_BRANCH} — refusing to overwrite"
|
||||
fi
|
||||
done <<< "$files"
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
base="${f#"${old_name}"/}"
|
||||
dest="${new_name}/${base}"
|
||||
if [ "$f" != "$dest" ]; then
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
git mv "$f" "$dest"
|
||||
fi
|
||||
if [ -n "$new_filter" ]; then
|
||||
jq --arg new "$new_filter" \
|
||||
'if has("last_forward") and (.last_forward | has("josh_filter")) then .last_forward.josh_filter = $new else . end' \
|
||||
"$dest" > "${dest}.tmp" && mv "${dest}.tmp" "$dest"
|
||||
git add "$dest"
|
||||
fi
|
||||
done <<< "$files"
|
||||
|
||||
if ! git diff --cached --quiet 2>/dev/null; then
|
||||
git -c user.name="$BOT_NAME" -c user.email="$BOT_EMAIL" \
|
||||
commit -m "state: rename ${old_name} -> ${new_name}"
|
||||
git push origin "HEAD:${STATE_BRANCH}" \
|
||||
|| die "Failed to push state migration for ${old_name} -> ${new_name}"
|
||||
fi
|
||||
) || migrate_status=$?
|
||||
git worktree remove "$tmp_dir" 2>/dev/null || rm -rf "$tmp_dir"
|
||||
[ "$migrate_status" -eq 0 ] || exit "$E_GENERAL"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user