Add linearize fallback for reverse sync and workflow guide (v1.3)
When josh-proxy rejects a reverse sync push due to unmappable merge commits, fall back to linearizing: cherry-pick regular commits individually, squash only the merge commits via cherry-pick -m 1. Also adds a recommended Git workflow section to the guide explaining where cross-branch merges should happen (monorepo) vs feature work (subrepo), and expands troubleshooting for the "josh rejected push" error with root cause analysis and prevention advice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -518,6 +518,60 @@ Bot commits include a git trailer like `Josh-Sync-Origin: forward/main/2024-02-1
|
||||
|
||||
Sync state is stored as JSON files on an orphan branch (`josh-sync-state`), one file per target/branch. This tracks the last-synced commit SHAs and timestamps to avoid re-syncing the same changes.
|
||||
|
||||
## Recommended Git Workflow
|
||||
|
||||
Josh-proxy maps commits through a filter per-branch. It handles linear history and simple merges (short-lived feature branches) without issues. However, it **cannot map merge commits whose parents were created on the subrepo side** — because those commits were never pushed through josh and have no monorepo-side mapping.
|
||||
|
||||
This means cross-branch merges (e.g., `stage` → `main`) must happen on the monorepo side, where josh can filter the result cleanly.
|
||||
|
||||
### The rule
|
||||
|
||||
**The monorepo owns branch topology. The subrepo owns feature development.**
|
||||
|
||||
### What to do where
|
||||
|
||||
| Action | Where to do it | Why |
|
||||
|--------|---------------|-----|
|
||||
| Feature branch → `main` | **Subrepo** (PR, any merge strategy) | Short-lived branch with clean lineage — josh handles it |
|
||||
| `stage` → `main` (promotion) | **Monorepo** | Cross-branch merge — forward sync propagates the result to both subrepo branches |
|
||||
| `main` → `stage` (catch-up) | **Monorepo** | Same reason — avoids criss-cross merge history on subrepo |
|
||||
| Hotfix to `main` | **Either side** | Single commit or small PR — works everywhere |
|
||||
| Config/CI changes (monorepo-only) | **Monorepo** | Not synced to subrepo (use `exclude` for monorepo-only files) |
|
||||
|
||||
### Feature development (subrepo)
|
||||
|
||||
This is the primary workflow for subrepo developers:
|
||||
|
||||
1. Create a feature branch from `main` (or whichever synced branch)
|
||||
2. Develop, commit, push
|
||||
3. Open a PR targeting `main` on the subrepo
|
||||
4. Merge the PR (merge commit, squash, or rebase — all work)
|
||||
5. Reverse sync picks up the new commits and creates a PR on the monorepo
|
||||
|
||||
Any merge strategy works because the feature branch lineage stays within josh's mapped history.
|
||||
|
||||
### Cross-branch merges (monorepo)
|
||||
|
||||
When promoting `stage` to `main`, or catching up `stage` with `main`:
|
||||
|
||||
1. Open a PR on the **monorepo** merging `stage` → `main` (or `main` → `stage`)
|
||||
2. Review and merge on the monorepo
|
||||
3. Forward sync propagates the result to the subrepo's `main` and `stage` branches
|
||||
|
||||
This works because josh does the filtering — it computes the subrepo view from the monorepo merge result, rather than trying to reconstruct a monorepo merge from subrepo commits.
|
||||
|
||||
### What to avoid
|
||||
|
||||
- **Don't merge `stage` into `main` on the subrepo with a merge commit.** The merge parents include commits created on the subrepo side (forward sync merges, criss-cross merges) that josh has no mapping for. Josh rejects the push with a 500 error.
|
||||
- **Don't merge `main` into `stage` on the subrepo.** Creates criss-cross merge history that causes the same josh mapping failure when `stage` is later merged back.
|
||||
- **Don't rebase synced branches on the subrepo.** This rewrites commit SHAs that josh has already mapped, breaking the sync relationship.
|
||||
|
||||
### If you must merge cross-branch on the subrepo
|
||||
|
||||
Use a **squash merge**. A squash merge produces a single commit with one parent — josh can always map it. You lose the individual commit history on the target branch, but the sync goes through cleanly.
|
||||
|
||||
As a safety net, josh-sync v1.3+ automatically falls back to linearizing the history when josh rejects a push — cherry-picking regular commits individually and squashing only the problematic merge commits. See the [troubleshooting section](#josh-rejected-push-reverse-sync) and [ADR-011](adr/011-linearize-fallback-reverse.md).
|
||||
|
||||
## Excluding Files from Sync
|
||||
|
||||
Some files in the monorepo subfolder may not belong in the subrepo (e.g., monorepo-specific CI configs, internal tooling). The `exclude` config field removes these at the josh-proxy layer — excluded files never appear in the subrepo.
|
||||
@@ -616,7 +670,35 @@ Normal: the subrepo changed while sync was running. The next sync run will pick
|
||||
|
||||
### "Josh rejected push" (reverse sync)
|
||||
|
||||
Josh-proxy couldn't map the push back to the monorepo. Check josh-proxy logs, verify the josh filter is correct. May indicate a history divergence — consider running `josh-sync reset <target>`.
|
||||
Josh-proxy couldn't map the push back to the monorepo. This has two common causes:
|
||||
|
||||
#### Merge commits with unmappable parents
|
||||
|
||||
**Symptom:** Josh returns `500 Internal Server Error` with a message like:
|
||||
|
||||
```
|
||||
rejecting merge with 2 parents:
|
||||
"Merge pull request 'stage' (#30) from stage into main" (c4fa3c9...)
|
||||
1) "Merge branch 'auto-sync/import-...'" (4bf8704...)
|
||||
2) "Merge branch 'main' into stage" (d021654...)
|
||||
```
|
||||
|
||||
**Cause:** The subrepo has a merge commit whose parents josh-proxy cannot trace through its filter. This typically happens when:
|
||||
- A long-lived branch (e.g., `stage`) is merged into `main` via a merge commit (not squash)
|
||||
- That branch contains auto-sync merge commits or other history that doesn't exist in josh's filtered view
|
||||
- Someone merges `main` into a feature/staging branch and then merges it back — the criss-cross parents confuse josh's mapping
|
||||
|
||||
**Automatic handling (v1.3+):** josh-sync automatically falls back to linearizing the history when the direct push fails. Regular commits are cherry-picked individually (preserving authorship and messages), while merge commits are squashed into single commits via `cherry-pick -m 1`. The PR notes when this fallback was used. See [ADR-011](adr/011-linearize-fallback-reverse.md).
|
||||
|
||||
**Prevention:** In josh-synced subrepos, prefer **squash merges** when merging long-lived branches (stage, develop) into the synced branch. Squash merges produce a single commit with no merge parents, which josh can always map. Regular feature branch merges (short-lived, no auto-sync history) are usually fine.
|
||||
|
||||
**Manual resolution (if automatic fallback also fails):** This indicates a more fundamental history issue. Options:
|
||||
1. Cherry-pick the desired changes manually onto a clean branch and push through josh
|
||||
2. Run `josh-sync reset <target>` to re-establish history (destructive — all subrepo clones must re-fetch)
|
||||
|
||||
#### Filter or path mismatch
|
||||
|
||||
Josh-proxy couldn't map the push due to an incorrect filter. Check josh-proxy logs, verify the `josh_filter` or `subfolder` in `.josh-sync.yml` is correct, and ensure the subfolder exists in the monorepo.
|
||||
|
||||
### Import PR shows "No changes"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user