# ADR-011: Linearize Fallback for Reverse Sync **Status:** Accepted **Date:** 2026-03 ## Context Josh-proxy rejects reverse sync pushes when the subrepo history contains merge commits whose parents it cannot map through the filter. This happens when: 1. A long-lived branch (e.g., `stage`) is merged into `main` via a merge commit 2. That branch contains auto-sync merge commits or criss-cross merges with `main` 3. Josh encounters the merge commit, tries to map both parents through the filter, and fails with a 500 error This is a legitimate subrepo workflow — teams merge staging branches into main regularly. The sync tool should handle it without requiring teams to change their Git workflow. ### Alternatives considered 1. **Fail and log**: The pre-v1.3 behavior. Leaves the sync stuck until someone manually intervenes. Bad for unattended cron-based sync. 2. **Squash all commits into one**: Create a single `commit-tree` with the diff between josh-filtered base and subrepo HEAD. Simple but destroys all commit granularity — 10 unrelated commits appear as one blob on the monorepo PR. 3. **Rewrite subrepo history**: Rebase or filter-branch the subrepo to remove problematic merges. Breaks the sync relationship with the monorepo (josh's SHA mapping becomes invalid) and forces all developers to re-clone. 4. **Linearize: cherry-pick regular commits, squash only merges**: Walk the human commits in order, cherry-pick non-merge commits as-is, and use `cherry-pick -m 1` for merge commits. Preserves individual commit granularity for regular commits; only the problematic merge commits lose their multi-parent structure. ## Decision When the direct push through josh-proxy fails, fall back to option 4: linearize the history by cherry-picking onto the josh-filtered base. ### How it works 1. Direct `git push` through josh-proxy is attempted first (existing behavior) 2. If it fails, create a temporary branch from `mono-filtered/` 3. Walk human commits (oldest-first, bot commits excluded) from the ancestry path: - **Regular commits** (≤1 parent): `git cherry-pick ` — preserves author, date, and message - **Merge commits** (>1 parent): `git cherry-pick -m 1 ` — applies the merge's diff relative to its first parent as a single commit 4. If cherry-pick conflicts (rare — usually due to ordering issues), fall back to `git diff | git apply` with the original author metadata 5. Push the linearized branch through josh-proxy 6. PR body includes a note explaining that merge commits were squashed ### What is preserved - Individual non-merge commits (author, date, message, diff) - The net effect of each merge commit (as a squashed single commit) - The original commit list in the PR body for reference ### What is lost - The multi-parent structure of merge commits (they become single-parent) - The distinction between "changes introduced by the merge" vs "changes from each parent" — the merge is represented as its diff from first parent ## Consequences **Positive:** - Reverse sync no longer gets stuck on merge commits — handles the common case automatically - Non-merge commits retain full granularity (no unnecessary squashing) - Subrepo history is untouched — no rewriting, no broken sync relationship - The PR body documents when linearization was used, so reviewers know **Negative:** - Merge commit semantics are lost on the monorepo side (they appear as regular commits) - If a merge commit's changes conflict with a prior cherry-picked commit during linearization, the diff-apply fallback may produce a subtly different result than the original merge resolution - Adds complexity to the reverse sync path (two code paths: direct push and linearize fallback) **Risk mitigation:** - The direct push is always tried first — linearization only activates when josh rejects the push - The monorepo PR is still reviewed by humans before merging - The original commit SHAs are listed in the PR body for cross-referencing