"#"
This commit is contained in:
55
docs/adr/013-non-destructive-adoption.md
Normal file
55
docs/adr/013-non-destructive-adoption.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# ADR-013: Non-destructive adoption merge for existing subrepos
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-04
|
||||
|
||||
## 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.
|
||||
@@ -18,3 +18,4 @@ This directory contains Architecture Decision Records (ADRs) for josh-sync. Each
|
||||
| [010](010-onboard-checkpoint-resume.md) | Onboard workflow with checkpoint/resume | Accepted |
|
||||
| [011](011-linearize-fallback-reverse.md) | Linearize fallback for reverse sync | Accepted |
|
||||
| [012](012-explicit-monorepo-url.md) | Explicit `monorepo_url` (drop first-target-host inference) | Accepted |
|
||||
| [013](013-non-destructive-adoption.md) | Non-destructive adoption merge for existing subrepos | Accepted |
|
||||
|
||||
@@ -224,14 +224,40 @@ For a new monorepo before import, preflight may warn that subfolders don't exist
|
||||
|
||||
## Step 5: Import Existing Subrepos
|
||||
|
||||
This is the critical onboarding step. There are two approaches:
|
||||
This is the critical onboarding step. There are three approaches:
|
||||
|
||||
- **`josh-sync onboard`** (recommended) — interactive, resumable, preserves open PRs
|
||||
- **Manual `import` → merge → `reset`** — lower-level, for automation or when there are no open PRs to preserve
|
||||
- **`josh-sync adopt`** (recommended for active existing subrepos) — non-destructive, resumable, preserves existing subrepo history
|
||||
- **`josh-sync onboard`** — destructive replacement-repo workflow for teams that intentionally archive the old repo
|
||||
- **Manual `import` → merge → `reset`** — lower-level destructive path for automation or empty replacement repos
|
||||
|
||||
### Option A: Onboard (recommended)
|
||||
### Option A: Adopt (recommended for active subrepos)
|
||||
|
||||
The `onboard` command walks you through the entire process interactively, with checkpoint/resume at every step.
|
||||
Use `adopt` when the subrepo already exists, developers already clone it, or open PR branches should remain based on existing history.
|
||||
|
||||
```bash
|
||||
josh-sync adopt billing
|
||||
```
|
||||
|
||||
The command will:
|
||||
1. **Import** — copies current subrepo content into the monorepo and creates import PRs (one per branch)
|
||||
2. **Wait for merge** — shows PR numbers and waits for you to merge them
|
||||
3. **Verify trees** — requires the existing subrepo tree to match the Josh-filtered monorepo tree
|
||||
4. **Adopt** — creates a merge commit on the subrepo with Josh-filtered HEAD as parent 1 and existing subrepo HEAD as parent 2
|
||||
5. **Push** — pushes the adoption merge with a normal fast-forward push, never force-push
|
||||
|
||||
The adoption merge preserves the old subrepo history while giving Josh a first-parent path back to the monorepo. If interrupted, re-run `josh-sync adopt billing` to resume. Use `--restart` to start over.
|
||||
|
||||
After adoption, developers can update existing clones with a normal fast-forward:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout main
|
||||
git merge --ff-only origin/main
|
||||
```
|
||||
|
||||
### Option B: Onboard with replacement repo
|
||||
|
||||
The `onboard` command walks through the destructive replacement-repo process interactively, with checkpoint/resume at every step.
|
||||
|
||||
**Before you start:**
|
||||
|
||||
@@ -272,13 +298,13 @@ josh-sync migrate-pr billing 5 8 12
|
||||
|
||||
PR migration works by fetching the diff from the archived repo's PR, applying it to the new repo, and creating a new PR. File content is identical after reset, so patches apply cleanly.
|
||||
|
||||
### Option B: Manual import → merge → reset
|
||||
### Option C: Manual import → merge → reset
|
||||
|
||||
Use this when the subrepo has no open PRs to preserve, or for scripted automation.
|
||||
Use this for scripted automation or when you intentionally want to replace subrepo history.
|
||||
|
||||
> Do this **one target at a time** to keep PRs reviewable.
|
||||
|
||||
#### 5b-1. Import
|
||||
#### 5c-1. Import
|
||||
|
||||
```bash
|
||||
josh-sync import billing
|
||||
@@ -293,13 +319,13 @@ This:
|
||||
|
||||
Review the import PR — check for leaked credentials, environment-specific config, or files that shouldn't be in the monorepo.
|
||||
|
||||
#### 5b-2. Merge the import PR
|
||||
#### 5c-2. Merge the import PR
|
||||
|
||||
Merge the PR using your Git platform's UI. This lands the subrepo content into the monorepo's main branch.
|
||||
|
||||
> At this point, the monorepo has the content but the histories are disconnected. Sync will **not** work until you complete the reset step.
|
||||
|
||||
#### 5b-3. Reset
|
||||
#### 5c-3. Reset
|
||||
|
||||
```bash
|
||||
josh-sync reset billing
|
||||
@@ -326,7 +352,7 @@ git checkout stage && git reset --hard origin/stage # repeat for each branch
|
||||
|
||||
Or simply delete and re-clone the subrepo. Local-only branches (not pushed to the remote) will be lost either way.
|
||||
|
||||
#### 5b-4. Repeat for each target
|
||||
#### 5c-4. Repeat for each target
|
||||
|
||||
```
|
||||
For each target:
|
||||
@@ -337,13 +363,13 @@ For each target:
|
||||
|
||||
### Verify
|
||||
|
||||
After all targets are imported and reset (whichever option you used):
|
||||
After all targets are adopted or reset:
|
||||
|
||||
```bash
|
||||
# Check all targets show state
|
||||
josh-sync status
|
||||
|
||||
# Test forward sync — should return "skip" (trees are identical after reset)
|
||||
# Test forward sync — should return "skip" (trees are identical after adoption/reset)
|
||||
josh-sync sync --forward --target billing
|
||||
|
||||
# Test reverse sync — should return "skip" (no new human commits)
|
||||
@@ -620,9 +646,12 @@ To add a new subrepo after initial setup:
|
||||
1. Add the target to `.josh-sync.yml`
|
||||
2. Update the forward workflow's `paths:` list to include the new subfolder
|
||||
3. Commit and push
|
||||
4. Import the target:
|
||||
4. Import or adopt the target:
|
||||
```bash
|
||||
# Recommended: interactive onboard (preserves open PRs)
|
||||
# Recommended for an existing active subrepo
|
||||
josh-sync adopt new-target
|
||||
|
||||
# Replacement-repo workflow
|
||||
josh-sync onboard new-target
|
||||
|
||||
# Or manual: import → merge PR → reset
|
||||
|
||||
Reference in New Issue
Block a user