- Add v1.1.0 and v1.2.0 changelog entries - Add exclude field to config reference and example config - Add ADRs documenting all major design decisions - Fix step numbering in reverse_sync() - Fix action.yml to copy VERSION file - Add dist/ and .env to .gitignore - Use refs/tags/ format for Nix flake tag refs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
54 lines
3.0 KiB
Markdown
54 lines
3.0 KiB
Markdown
# ADR-007: Reconciliation Merge for Filter Changes
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-02
|
|
|
|
## Context
|
|
|
|
When the josh filter changes (e.g., adding exclude patterns), josh-proxy recomputes the entire filtered history with new SHAs. The subrepo's existing history (based on the old filter) shares no common ancestor with the new filtered history. A naive forward sync would see "unrelated histories" and fail.
|
|
|
|
### Alternatives considered
|
|
|
|
1. **Force-push to subrepo**: Replace subrepo history with the new filtered view (same as `josh-sync reset`). Destructive — all local clones become invalid, open PRs are orphaned, developers must re-clone.
|
|
|
|
2. **Cherry-pick new commits**: Identify commits that exist in the new filtered history but not the old, cherry-pick them onto the subrepo. Complex — the "same" commit has different SHAs in old vs new filtered history. No reliable way to match them.
|
|
|
|
3. **Reconciliation merge commit**: Create a merge commit on the subrepo that has both the new filtered HEAD and the old subrepo HEAD as parents, using the new filtered tree. This establishes shared ancestry without rewriting history.
|
|
|
|
## Decision
|
|
|
|
When josh-sync detects a filter change (stored filter in state differs from current `$JOSH_FILTER`), create a reconciliation merge commit using `git commit-tree`.
|
|
|
|
### How it works
|
|
|
|
1. Clone subrepo (has old history)
|
|
2. Fetch josh-proxy filtered view (has new history)
|
|
3. If trees are identical → skip (filter change had no effect on content)
|
|
4. Create merge commit: `git commit-tree <josh-tree> -p <josh-head> -p <subrepo-head>`
|
|
5. Push with `--force-with-lease`
|
|
|
|
The merge commit uses the josh-filtered tree (new content) and has two parents:
|
|
- **Parent 1**: josh-filtered HEAD (new filter history) — must be first (see ADR-008)
|
|
- **Parent 2**: subrepo HEAD (old filter history) — preserves old history as a side branch
|
|
|
|
### Detection
|
|
|
|
Filter change is detected by comparing the stored `josh_filter` in sync state with the current `$JOSH_FILTER`. For pre-v1.2 state (no filter stored), the old filter is derived as `:/<subfolder>`.
|
|
|
|
As a reactive fallback, `forward_sync()` also detects unrelated histories via `git merge-base` and falls back to reconciliation.
|
|
|
|
## Consequences
|
|
|
|
**Positive:**
|
|
- Non-destructive — old history is preserved as parent 2 of the merge
|
|
- Developers don't need to re-clone the subrepo
|
|
- Open PRs on the subrepo remain valid (they're based on commits that are still ancestors)
|
|
- Automatic — no manual intervention needed when changing exclude patterns
|
|
- Force-with-lease protects against concurrent changes during reconciliation
|
|
|
|
**Negative:**
|
|
- The merge commit is synthetic (created by bot, not a real merge of concurrent work)
|
|
- Parent ordering is critical — wrong order breaks josh's reverse mapping (see ADR-008)
|
|
- The reconciliation merge contains a bot trailer, so reverse sync correctly ignores it
|
|
- If the subrepo has diverged significantly (manual commits during filter change), the reconciliation merge may produce unexpected tree content (uses josh-filtered tree unconditionally)
|