Files
josh-sync/docs/adr/013-non-destructive-adoption.md
SBPro dc2cdea360 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>
2026-05-27 03:32:04 +01:00

2.8 KiB

ADR-013: Non-destructive adoption merge for existing subrepos

Status: Accepted Date: 2026-04 Update (2026-05): The standalone josh-sync adopt <target> command described below has been folded into josh-sync onboard <target> as the adopt strategy (selectable via --mode=adopt, targets[].history_lock: preserve, or auto-detection when the subrepo has any branches). josh-sync adopt is retained as a thin CLI alias. The state file moved from <target>/adopt.json to <target>/onboard.json with a .strategy field; the legacy file is still read for backward compatibility. See the guide's Onboarding section.

Context

Existing subrepos often already have real history, open PRs, and developer clones. The original import then reset onboarding path establishes Josh-compatible history by force-pushing the Josh-filtered monorepo history onto the subrepo. That is correct for empty replacement repos, but it rewrites the active subrepo branch and invalidates local clones.

We need a path for active subrepos where history must remain available on the same branch and where open PR branches should stay based on the existing history.

Decision

Add josh-sync adopt <target> as a separate workflow from onboard and reset. Adoption imports the subrepo content into the monorepo, waits for the import PR to merge, then creates one merge commit on each configured subrepo branch:

  1. First parent: Josh-filtered monorepo HEAD
  2. Second parent: existing subrepo HEAD
  3. Tree: Josh-filtered monorepo tree

Josh follows first-parent history back to the monorepo, while Git still keeps the old subrepo history reachable through parent 2. The push is a normal fast-forward push from the existing subrepo HEAD to the adoption merge commit. No force-push is used.

Before creating the adoption merge, josh-sync requires the existing subrepo tree to match the Josh-filtered monorepo tree. If the trees differ, adoption aborts and the user must merge the import PR or reconcile the branch contents first.

Adoption state is stored separately at <target>/adopt.json on the josh-sync-state branch.

Consequences

Positive:

  • Existing subrepo history remains on the active branch.
  • Existing clones can fast-forward instead of hard-resetting or re-cloning.
  • Open PR branches remain based on reachable history.
  • Josh-compatible ancestry is established without a destructive rewrite.

Negative:

  • Adoption adds one synthetic merge commit to each adopted subrepo branch.
  • Strict "no subrepo commit at all" adoption is impossible if the existing subrepo branch must become connected to the Josh-filtered history without a rewrite. A commit is needed to join the two histories.
  • Tree equality is strict. If the monorepo import differs from the subrepo content, adoption stops until the user resolves the mismatch.