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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
- **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
|
### Docs
|
||||||
|
|
||||||
- Expanded troubleshooting guide for "Josh rejected push" with root cause analysis and prevention advice.
|
- Expanded troubleshooting guide for "Josh rejected push" with root cause analysis and prevention advice.
|
||||||
- Added ADR-011: Linearize fallback for reverse sync.
|
- Added ADR-011: Linearize fallback for reverse sync.
|
||||||
|
- Added recommended Git workflow section to the guide.
|
||||||
|
|
||||||
## 1.2.0
|
## 1.2.0
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ _sync_direction() {
|
|||||||
echo "::error::Target ${target_name}, branch ${branch}: josh rejected push — check proxy logs"
|
echo "::error::Target ${target_name}, branch ${branch}: josh rejected push — check proxy logs"
|
||||||
continue
|
continue
|
||||||
fi
|
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)
|
# Update state (only on success)
|
||||||
local new_state
|
local new_state
|
||||||
|
|||||||
39
lib/sync.sh
39
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 ──────────────────────────────
|
# ─── Reverse Sync: Subrepo → Monorepo ──────────────────────────────
|
||||||
#
|
#
|
||||||
# Always creates a PR on the monorepo — never pushes directly.
|
# 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() {
|
reverse_sync() {
|
||||||
local mono_branch="$SYNC_BRANCH_MONO"
|
local mono_branch="$SYNC_BRANCH_MONO"
|
||||||
@@ -265,15 +267,27 @@ reverse_sync() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Find new human commits (excludes bot commits from forward sync)
|
# 4. Find new human commits (excludes bot commits from forward sync)
|
||||||
# Uses --ancestry-path to restrict to the direct lineage and avoid
|
# Uses merge-base + --ancestry-path to restrict to the direct lineage.
|
||||||
# leaking old history through reconciliation merge parents.
|
# merge-base is needed because mono-filtered may have advanced past the
|
||||||
local human_commits
|
# last forward sync (any monorepo commit, even outside the subfolder,
|
||||||
human_commits=$(git log --ancestry-path "mono-filtered/${mono_branch}..HEAD" \
|
# causes josh to create a new filtered commit). Using mono-filtered
|
||||||
--oneline --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "")
|
# 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
|
if [ -z "$human_commits" ]; then
|
||||||
log "INFO" "No new human commits in subrepo — nothing to sync"
|
log "INFO" "No new human commits in subrepo — nothing to sync"
|
||||||
echo "skip"
|
echo "skip-dirty"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -301,9 +315,16 @@ reverse_sync() {
|
|||||||
git checkout -b "tmp-linearize-$$" "mono-filtered/${mono_branch}" 2>/dev/null \
|
git checkout -b "tmp-linearize-$$" "mono-filtered/${mono_branch}" 2>/dev/null \
|
||||||
|| die "Failed to create linearize branch"
|
|| 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
|
local commit_shas
|
||||||
commit_shas=$(git log --ancestry-path "mono-filtered/${mono_branch}..${original_head}" \
|
if [ -n "$base" ]; then
|
||||||
--reverse --format="%H" --invert-grep --grep="^${BOT_TRAILER}:" 2>/dev/null || echo "")
|
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
|
local sha
|
||||||
for sha in $commit_shas; do
|
for sha in $commit_shas; do
|
||||||
|
|||||||
Reference in New Issue
Block a user