From b4eaa47ef6c8a7e9d61191e68f8662c0e6fa5d11 Mon Sep 17 00:00:00 2001 From: SBPro Date: Tue, 17 Mar 2026 14:07:20 +0300 Subject: [PATCH] Fix reverse sync missing human commits when mono-filtered advances --ancestry-path returns empty when mono-filtered/main has advanced past the last forward sync (any monorepo commit causes josh to create a new filtered SHA not in the subrepo's ancestry). Use git merge-base to find the last connected point instead. Also return skip-dirty (not skip) when trees differ but no human commits found, preventing state from being updated on false skips which would permanently lose unsynced commits. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 6 ++++++ bin/josh-sync | 4 ++++ lib/sync.sh | 39 ++++++++++++++++++++++++++++++--------- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d27db..331843f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,16 @@ - **Linearize fallback for reverse sync**: When josh-proxy rejects a push due to unmappable merge commits, reverse sync automatically falls back to linearizing the history — cherry-picking regular commits individually and squashing only the problematic merge commits via `cherry-pick -m 1`. Preserves individual commit granularity while handling complex merge topologies that josh cannot map. PR body notes when linearization was used. +### Fixes + +- **Reverse sync human commit detection**: Fixed `--ancestry-path` returning empty when `mono-filtered` has advanced past the last forward sync. Any monorepo commit (even outside the subfolder) causes josh to create a new filtered commit, breaking the ancestry path to subrepo HEAD. Now uses `git merge-base` to find the last connected point. +- **State not updated on false skip**: When trees differ but no human commits are found, reverse sync now returns `skip-dirty` instead of `skip`. State is not updated, so the system retries on the next run instead of silently dropping commits. + ### Docs - Expanded troubleshooting guide for "Josh rejected push" with root cause analysis and prevention advice. - Added ADR-011: Linearize fallback for reverse sync. +- Added recommended Git workflow section to the guide. ## 1.2.0 diff --git a/bin/josh-sync b/bin/josh-sync index cc5414b..54f3906 100755 --- a/bin/josh-sync +++ b/bin/josh-sync @@ -259,6 +259,10 @@ _sync_direction() { echo "::error::Target ${target_name}, branch ${branch}: josh rejected push — check proxy logs" continue fi + if [ "$result" = "skip-dirty" ]; then + log "WARN" "Trees differ but no human commits — skipping state update (will retry)" + continue + fi # Update state (only on success) local new_state diff --git a/lib/sync.sh b/lib/sync.sh index 95c2e51..dbf2238 100644 --- a/lib/sync.sh +++ b/lib/sync.sh @@ -226,7 +226,9 @@ ${BOT_TRAILER}: filter-change/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)") # ─── Reverse Sync: Subrepo → Monorepo ────────────────────────────── # # Always creates a PR on the monorepo — never pushes directly. -# Returns: skip | pr-created | josh-rejected +# Returns: skip | skip-dirty | pr-created | josh-rejected +# skip — trees identical, nothing to do +# skip-dirty — trees differ but no human commits found (don't update state) reverse_sync() { local mono_branch="$SYNC_BRANCH_MONO" @@ -265,15 +267,27 @@ reverse_sync() { fi # 4. Find new human commits (excludes bot commits from forward sync) - # Uses --ancestry-path to restrict to the direct lineage and avoid - # leaking old history through reconciliation merge parents. - local human_commits - human_commits=$(git log --ancestry-path "mono-filtered/${mono_branch}..HEAD" \ - --oneline --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + # Uses merge-base + --ancestry-path to restrict to the direct lineage. + # merge-base is needed because mono-filtered may have advanced past the + # last forward sync (any monorepo commit, even outside the subfolder, + # causes josh to create a new filtered commit). Using mono-filtered + # directly with --ancestry-path returns empty when it's not an ancestor + # of HEAD. merge-base always finds the last connected point. + local base human_commits + base=$(git merge-base "mono-filtered/${mono_branch}" HEAD 2>/dev/null || echo "") + + if [ -n "$base" ]; then + human_commits=$(git log --ancestry-path "${base}..HEAD" \ + --oneline --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + else + # No common ancestor (first sync or fully diverged) — skip ancestry-path + human_commits=$(git log "mono-filtered/${mono_branch}..HEAD" \ + --oneline --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + fi if [ -z "$human_commits" ]; then log "INFO" "No new human commits in subrepo — nothing to sync" - echo "skip" + echo "skip-dirty" return fi @@ -301,9 +315,16 @@ reverse_sync() { git checkout -b "tmp-linearize-$$" "mono-filtered/${mono_branch}" 2>/dev/null \ || die "Failed to create linearize branch" + # Reuse merge-base from step 4 (same reasoning: mono-filtered may have + # advanced past the last forward sync, breaking --ancestry-path). local commit_shas - commit_shas=$(git log --ancestry-path "mono-filtered/${mono_branch}..${original_head}" \ - --reverse --format="%H" --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + if [ -n "$base" ]; then + commit_shas=$(git log --ancestry-path "${base}..${original_head}" \ + --reverse --format="%H" --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + else + commit_shas=$(git log "mono-filtered/${mono_branch}..${original_head}" \ + --reverse --format="%H" --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "") + fi local sha for sha in $commit_shas; do