Release 2.2.0 — unified onboard strategy + safety hardening
Folds `josh-sync adopt` into `josh-sync onboard <target>` as the adopt strategy alongside the original reset flow. Strategy resolves by precedence: --mode flag > targets[].history_lock config (preserve|rewrite) > auto-detect via `git ls-remote --heads`. `josh-sync adopt` is kept as a thin alias. Adds the new `targets[].history_lock` config field (validated at parse time) and folds `<target>/adopt.json` state into `<target>/onboard.json` with a `.strategy` field; legacy state files are read with a backward-compat fallback. Safety hardening (from a multi-angle review of the unification): - auto-detect distinguishes auth/network failure from empty repo - resume validates --mode against the strategy saved in state - `josh-sync adopt` rejects a conflicting --mode in the forwarded args - missing import-PR lookup dies in both strategies (was WARN+continue for reset, which could create duplicate import PRs on resume) - --restart durably removes the legacy adopt.json from the state branch - adopt_branch is now a subshell function (EXIT trap can't clobber callers) - strategy value validated after resolve/load (reset|adopt) - --mode with missing/empty value dies with a usage hint - migrate-pr against an adopt-strategy target dies with a specific hint - reset importing asserts archived_url is present (no "null" → git clone) End-to-end + CLI bats coverage added (tests/unit/adopt_e2e.bats, tests/unit/cli.bats). 72 tests, shellcheck clean. Makefile dist bundle header now correctly interpolates VERSION and line count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,9 @@
|
||||
# sync Run forward and/or reverse sync
|
||||
# preflight Validate config, connectivity, auth
|
||||
# import <target> Initial import: pull subrepo into monorepo
|
||||
# adopt <target> Adopt existing subrepo history without rewriting it
|
||||
# onboard <target> Interactive onboarding (auto-picks reset|adopt strategy)
|
||||
# adopt <target> Alias for `onboard --mode=adopt`
|
||||
# reset <target> Reset subrepo to josh-filtered view
|
||||
# onboard <target> Import existing subrepo into monorepo (interactive)
|
||||
# migrate-pr <target> [PR#...] [--all] Move PRs from archived to new subrepo
|
||||
# status Show target config and sync state
|
||||
# state show|reset Manage sync state directly
|
||||
@@ -75,9 +75,13 @@ Commands:
|
||||
sync Run forward and/or reverse sync
|
||||
preflight Validate config, connectivity, auth, workflow coverage
|
||||
import <target> Initial import: pull existing subrepo into monorepo (creates PR)
|
||||
adopt <target> Adopt existing subrepo history without rewriting it
|
||||
onboard <target> Onboard a subrepo (interactive, resumable). Picks a strategy:
|
||||
reset — force-push josh-filtered history (replaces subrepo)
|
||||
adopt — non-destructive merge that keeps subrepo history
|
||||
Precedence: --mode flag > targets[].history_lock > auto-detect
|
||||
via ls-remote (heads → adopt, empty → reset).
|
||||
adopt <target> Alias for 'onboard --mode=adopt' (kept for back-compat)
|
||||
reset <target> Reset subrepo to josh-filtered view (after merging import PR)
|
||||
onboard <target> Import existing subrepo into monorepo (interactive, resumable)
|
||||
migrate-pr <target> [PR#...] [--all] Move PRs from archived to new subrepo
|
||||
status Show target config and sync state
|
||||
state show <target> [branch] Show sync state JSON
|
||||
@@ -96,6 +100,9 @@ Sync flags:
|
||||
--target NAME Filter to target(s) — comma-separated for multiple (env: JOSH_SYNC_TARGET)
|
||||
--branch BRANCH Filter to one branch
|
||||
|
||||
Onboard flags:
|
||||
--mode={reset,adopt} Override the onboarding strategy
|
||||
|
||||
Environment:
|
||||
JOSH_SYNC_TARGET Restrict to a single target name
|
||||
JOSH_SYNC_STATE_BRANCH State branch name (default: josh-sync-state)
|
||||
@@ -534,40 +541,33 @@ cmd_import() {
|
||||
done
|
||||
}
|
||||
|
||||
# ─── Adopt Command ─────────────────────────────────────────────────
|
||||
# ─── Adopt Command (alias) ─────────────────────────────────────────
|
||||
# `josh-sync adopt` is preserved as a thin alias for the unified onboard
|
||||
# command. Effectively: `josh-sync onboard --mode=adopt <args...>`.
|
||||
|
||||
cmd_adopt() {
|
||||
local config_file=".josh-sync.yml"
|
||||
local target_name=""
|
||||
local restart=false
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--config) config_file="$2"; shift 2 ;;
|
||||
--debug) export JOSH_SYNC_DEBUG=1; shift ;;
|
||||
--restart) restart=true; shift ;;
|
||||
-*) die "Unknown flag: $1" ;;
|
||||
*) target_name="$1"; shift ;;
|
||||
# Reject a conflicting --mode in the forwarded args: the alias means adopt,
|
||||
# so `josh-sync adopt … --mode=reset` is contradictory. Without this guard
|
||||
# the user-supplied --mode would silently win (last --mode in cmd_onboard's
|
||||
# parser is the one that takes effect).
|
||||
local i=1 a
|
||||
while [ $i -le $# ]; do
|
||||
a="${!i}"
|
||||
case "$a" in
|
||||
--mode=adopt) ;;
|
||||
--mode=*) die "'josh-sync adopt' is an alias for --mode=adopt; conflicting '${a}' given. Use 'josh-sync onboard <target>' instead." ;;
|
||||
--mode)
|
||||
i=$((i + 1))
|
||||
if [ $i -gt $# ] || [ "${!i}" != "adopt" ]; then
|
||||
die "'josh-sync adopt' is an alias for --mode=adopt; conflicting '--mode ${!i:-<missing>}' given. Use 'josh-sync onboard <target>' instead."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [ -z "$target_name" ]; then
|
||||
echo "Usage: josh-sync adopt <target> [--restart]" >&2
|
||||
parse_config "$config_file"
|
||||
echo "Available targets:" >&2
|
||||
echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/ /' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
parse_config "$config_file"
|
||||
|
||||
local target_json
|
||||
target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
|
||||
[ -n "$target_json" ] || die "Target '${target_name}' not found in config"
|
||||
|
||||
log "INFO" "══════ Adopt target: ${target_name} ══════"
|
||||
load_target "$target_json"
|
||||
adopt_flow "$target_json" "$restart"
|
||||
log "INFO" "(josh-sync adopt is now an alias for: josh-sync onboard --mode=adopt)"
|
||||
cmd_onboard --mode=adopt "$@"
|
||||
}
|
||||
|
||||
# ─── Reset Command ─────────────────────────────────────────────────
|
||||
@@ -743,19 +743,25 @@ cmd_onboard() {
|
||||
local config_file=".josh-sync.yml"
|
||||
local target_name=""
|
||||
local restart=false
|
||||
local mode=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--config) config_file="$2"; shift 2 ;;
|
||||
--debug) export JOSH_SYNC_DEBUG=1; shift ;;
|
||||
--restart) restart=true; shift ;;
|
||||
--mode=*) mode="${1#--mode=}"
|
||||
[ -n "$mode" ] || die "Empty --mode= (use --mode=reset|adopt)"
|
||||
shift ;;
|
||||
--mode) [ $# -ge 2 ] || die "--mode requires a value (reset|adopt)"
|
||||
mode="$2"; shift 2 ;;
|
||||
-*) die "Unknown flag: $1" ;;
|
||||
*) target_name="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$target_name" ]; then
|
||||
echo "Usage: josh-sync onboard <target> [--restart]" >&2
|
||||
echo "Usage: josh-sync onboard <target> [--mode=reset|adopt] [--restart]" >&2
|
||||
parse_config "$config_file"
|
||||
echo "Available targets:" >&2
|
||||
echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/ /' >&2
|
||||
@@ -770,7 +776,7 @@ cmd_onboard() {
|
||||
|
||||
log "INFO" "══════ Onboard target: ${target_name} ══════"
|
||||
load_target "$target_json"
|
||||
onboard_flow "$target_json" "$restart"
|
||||
onboard_flow "$target_json" "$restart" "$mode"
|
||||
}
|
||||
|
||||
# ─── Migrate PR Command ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user