Auto-reconcile subrepo history when josh filter changes

When the exclude list changes, josh-proxy recomputes filtered history
with new SHAs, breaking common ancestry with the subrepo. Instead of
requiring a manual reset (force-push), forward sync now detects the
filter change and creates a reconciliation merge commit that connects
the old and new histories — no force-push, no re-clone needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 10:40:08 +03:00
parent d7f8618b38
commit 22bd59a9d7
3 changed files with 97 additions and 3 deletions

View File

@@ -208,10 +208,20 @@ _sync_direction() {
fi
fi
# Run sync
# Check for filter change (forward only — reverse uses same filter)
local result
if [ "$direction" = "forward" ]; then
local prev_filter
prev_filter=$(echo "$state" | jq -r '.last_forward.josh_filter // empty')
if [ -n "$prev_filter" ] && [ "$prev_filter" != "$JOSH_FILTER" ]; then
log "WARN" "Josh filter changed — reconciling histories"
log "INFO" "Old: ${prev_filter}"
log "INFO" "New: ${JOSH_FILTER}"
result=$(reconcile_filter_change)
else
result=$(forward_sync)
fi
else
result=$(reverse_sync)
fi
@@ -240,8 +250,9 @@ _sync_direction() {
--arg s_sha "${subrepo_sha_now:-}" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg status "$result" \
--arg filter "$JOSH_FILTER" \
--argjson prev "$state" \
'$prev + {last_forward: {mono_sha:$m_sha, subrepo_sha:$s_sha, timestamp:$ts, status:$status}}')
'$prev + {last_forward: {mono_sha:$m_sha, subrepo_sha:$s_sha, timestamp:$ts, status:$status, josh_filter:$filter}}')
else
local mono_sha_now
mono_sha_now=$(git rev-parse "origin/${branch}" 2>/dev/null || echo "")

View File

@@ -573,6 +573,10 @@ Josh uses `::` patterns inside `:exclude[...]`:
No extra files to generate or commit — the exclusion is embedded directly in the josh-proxy URL.
### Changing the exclude list
You can safely add or remove patterns from `exclude` at any time. When josh-sync detects that the filter has changed since the last sync, it automatically creates a reconciliation merge commit on the subrepo that connects the old and new histories — no manual reset or force-push required. Developers do not need to re-clone the subrepo.
## Adding a New Target
To add a new subrepo after initial setup:

View File

@@ -128,6 +128,85 @@ ${BOT_TRAILER}: forward/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
fi
}
# ─── Filter Change Reconciliation ─────────────────────────────────
# When the josh filter changes (e.g., exclude patterns added/removed),
# josh-proxy recomputes filtered history with new SHAs. This creates a
# merge commit on the subrepo that connects old and new histories,
# re-establishing shared ancestry without a destructive force-push.
# Returns: reconciled | lease-rejected
reconcile_filter_change() {
local mono_branch="$SYNC_BRANCH_MONO"
local subrepo_branch="$SYNC_BRANCH_SUBREPO"
local work_dir
work_dir=$(mktemp -d)
# shellcheck disable=SC2064 # Intentional early expansion — work_dir is local
trap "rm -rf '$work_dir'" EXIT
log "INFO" "=== Filter change reconciliation: ${mono_branch} ==="
# 1. Clone subrepo
git clone "$(subrepo_auth_url)" \
--branch "$subrepo_branch" --single-branch \
"${work_dir}/subrepo" || die "Failed to clone subrepo"
cd "${work_dir}/subrepo" || exit
git config user.name "$BOT_NAME"
git config user.email "$BOT_EMAIL"
local subrepo_head
subrepo_head=$(git rev-parse HEAD)
log "INFO" "Subrepo HEAD: ${subrepo_head:0:12}"
# 2. Fetch josh-proxy filtered view (new filter)
git remote add josh-filtered "$(josh_auth_url)"
git fetch josh-filtered "$mono_branch" || die "Failed to fetch from josh-proxy"
local josh_head josh_tree
josh_head=$(git rev-parse "josh-filtered/${mono_branch}")
# shellcheck disable=SC1083 # {tree} is git syntax, not shell brace expansion
josh_tree=$(git rev-parse "josh-filtered/${mono_branch}^{tree}")
log "INFO" "Josh-proxy HEAD (new filter): ${josh_head:0:12}"
# 3. Check if trees are already identical (filter change had no effect)
local subrepo_tree
# shellcheck disable=SC1083
subrepo_tree=$(git rev-parse "HEAD^{tree}")
if [ "$josh_tree" = "$subrepo_tree" ]; then
log "INFO" "Trees identical after filter change — no reconciliation needed"
echo "skip"
return
fi
# 4. Create merge commit: subrepo HEAD + josh-proxy HEAD, with josh-proxy's tree
local merge_commit
merge_commit=$(git commit-tree "$josh_tree" \
-p "$subrepo_head" \
-p "$josh_head" \
-m "Sync: filter configuration updated
${BOT_TRAILER}: filter-change/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)")
git reset --hard "$merge_commit" >&2
log "INFO" "Created reconciliation merge: ${merge_commit:0:12}"
# 5. Record lease and push
local subrepo_sha
subrepo_sha=$(subrepo_ls_remote "$subrepo_branch")
if git push \
--force-with-lease="refs/heads/${subrepo_branch}:${subrepo_sha}" \
"$(subrepo_auth_url)" \
"HEAD:refs/heads/${subrepo_branch}"; then
log "INFO" "Filter change reconciled — shared ancestry re-established"
echo "reconciled"
else
log "WARN" "Force-with-lease rejected — subrepo changed during reconciliation"
echo "lease-rejected"
fi
}
# ─── Reverse Sync: Subrepo → Monorepo ──────────────────────────────
#
# Always creates a PR on the monorepo — never pushes directly.