Import josh-sync from subrepo/main
Sync-Origin: import/josh-sync/20260528-041502
This commit is contained in:
573
lib/sync.sh
Normal file
573
lib/sync.sh
Normal file
@@ -0,0 +1,573 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/sync.sh — Sync algorithms: forward, reverse, import, reset
|
||||
#
|
||||
# All four operations:
|
||||
# 1. Create a temp work dir with cleanup trap
|
||||
# 2. Perform git operations
|
||||
# 3. Return a status string via stdout
|
||||
#
|
||||
# Requires: lib/core.sh, lib/config.sh, lib/auth.sh, lib/state.sh sourced
|
||||
# Expects: SYNC_BRANCH_MONO, SYNC_BRANCH_SUBREPO, JOSH_FILTER, etc.
|
||||
|
||||
# ─── Forward Sync: Monorepo → Subrepo ──────────────────────────────
|
||||
#
|
||||
# Returns: fresh | skip | clean | lease-rejected | conflict | unrelated
|
||||
|
||||
forward_sync() {
|
||||
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" "=== Forward sync: mono/${mono_branch} → subrepo/${subrepo_branch} ==="
|
||||
|
||||
# 1. Clone the monorepo subfolder through josh (filtered view)
|
||||
log "INFO" "Cloning filtered monorepo via josh-proxy..."
|
||||
git clone "$(josh_auth_url)" \
|
||||
--branch "$mono_branch" --single-branch \
|
||||
"${work_dir}/filtered" || die "Failed to clone through josh-proxy"
|
||||
|
||||
cd "${work_dir}/filtered" || exit
|
||||
git config user.name "$BOT_NAME"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
|
||||
local mono_head
|
||||
mono_head=$(git rev-parse HEAD)
|
||||
log "INFO" "Mono filtered HEAD: ${mono_head:0:12}"
|
||||
|
||||
# 2. Record subrepo HEAD before any operations (the "lease")
|
||||
local subrepo_sha
|
||||
subrepo_sha=$(subrepo_ls_remote "$subrepo_branch")
|
||||
log "INFO" "Subrepo HEAD (lease): ${subrepo_sha:-(empty)}"
|
||||
|
||||
# 3. Handle fresh push (subrepo branch doesn't exist)
|
||||
if [ -z "$subrepo_sha" ]; then
|
||||
log "INFO" "Subrepo branch doesn't exist — doing fresh push"
|
||||
git push "$(subrepo_auth_url)" "HEAD:refs/heads/${subrepo_branch}" \
|
||||
|| die "Failed to push to subrepo"
|
||||
echo "fresh"
|
||||
return
|
||||
fi
|
||||
|
||||
# 4. Fetch subrepo for comparison
|
||||
git remote add subrepo "$(subrepo_auth_url)"
|
||||
git fetch subrepo "$subrepo_branch"
|
||||
|
||||
# 5. Compare trees — skip if identical
|
||||
local mono_tree subrepo_tree
|
||||
mono_tree=$(git rev-parse 'HEAD^{tree}')
|
||||
# shellcheck disable=SC1083 # {tree} is git syntax, not shell brace expansion
|
||||
subrepo_tree=$(git rev-parse "subrepo/${subrepo_branch}^{tree}" 2>/dev/null || echo "none")
|
||||
|
||||
if [ "$mono_tree" = "$subrepo_tree" ]; then
|
||||
log "INFO" "Trees identical — nothing to sync"
|
||||
echo "skip"
|
||||
return
|
||||
fi
|
||||
|
||||
# 6. Attempt merge: start from subrepo state, merge mono changes
|
||||
git checkout -B sync-attempt "subrepo/${subrepo_branch}" >&2
|
||||
|
||||
if git merge --no-commit --no-ff "$mono_head" >&2 2>&1; then
|
||||
# Clean merge
|
||||
if git diff --cached --quiet; then
|
||||
log "INFO" "Merge empty — skip"
|
||||
git merge --abort 2>/dev/null || true
|
||||
echo "skip"
|
||||
return
|
||||
fi
|
||||
|
||||
git commit -m "Sync from monorepo $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
${BOT_TRAILER}: forward/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
|
||||
|
||||
# 7. Push with force-with-lease (explicit SHA)
|
||||
if git push \
|
||||
--force-with-lease="refs/heads/${subrepo_branch}:${subrepo_sha}" \
|
||||
"$(subrepo_auth_url)" \
|
||||
"HEAD:refs/heads/${subrepo_branch}"; then
|
||||
|
||||
log "INFO" "Forward sync complete"
|
||||
echo "clean"
|
||||
else
|
||||
log "WARN" "Force-with-lease rejected — subrepo changed during sync"
|
||||
echo "lease-rejected"
|
||||
fi
|
||||
|
||||
else
|
||||
# Check: unrelated histories (filter change) vs normal merge conflict
|
||||
if ! git merge-base "subrepo/${subrepo_branch}" "$mono_head" >/dev/null 2>&1; then
|
||||
log "INFO" "No common ancestor — histories are unrelated (filter change?)"
|
||||
echo "unrelated"
|
||||
return
|
||||
fi
|
||||
|
||||
# Normal merge conflict
|
||||
local conflicted
|
||||
conflicted=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "(unknown)")
|
||||
git merge --abort
|
||||
|
||||
log "WARN" "Merge conflict on: ${conflicted}"
|
||||
|
||||
# Push mono state as a conflict branch for PR
|
||||
local ts
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
local conflict_branch="auto-sync/mono-${mono_branch}-${ts}"
|
||||
git checkout -B "$conflict_branch" "$mono_head" >&2
|
||||
git push "$(subrepo_auth_url)" "${conflict_branch}"
|
||||
|
||||
# Create PR on subrepo
|
||||
local pr_body conflicted_list
|
||||
# shellcheck disable=SC2001
|
||||
conflicted_list=$(echo "$conflicted" | sed 's/^/- /')
|
||||
pr_body="## Sync Conflict
|
||||
|
||||
Monorepo \`${mono_branch}\` has changes that conflict with \`${subrepo_branch}\`.
|
||||
|
||||
**Conflicted files:**
|
||||
${conflicted_list}
|
||||
|
||||
Please resolve and merge this PR to complete the sync."
|
||||
|
||||
create_pr "${SUBREPO_API}" "${SUBREPO_TOKEN}" \
|
||||
"$subrepo_branch" "$conflict_branch" \
|
||||
"[Sync Conflict] mono/${mono_branch} → ${subrepo_branch}" \
|
||||
"$pr_body" \
|
||||
|| die "Failed to create conflict PR on subrepo (check SUBREPO_TOKEN)"
|
||||
|
||||
log "INFO" "Conflict PR created on subrepo"
|
||||
echo "conflict"
|
||||
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: josh-proxy HEAD (first parent) + subrepo HEAD, with josh-proxy's tree
|
||||
# Josh follows first-parent traversal — josh-filtered MUST be first so josh can map
|
||||
# the history back to the monorepo. Old subrepo history hangs off parent 2.
|
||||
local merge_commit
|
||||
merge_commit=$(git commit-tree "$josh_tree" \
|
||||
-p "$josh_head" \
|
||||
-p "$subrepo_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.
|
||||
# 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"
|
||||
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" "=== Reverse sync: subrepo/${subrepo_branch} → mono/${mono_branch} ==="
|
||||
|
||||
local force_mode="${SYNC_REVERSE_FORCE:-0}"
|
||||
|
||||
# 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"
|
||||
|
||||
# 2. Fetch monorepo's filtered view for comparison
|
||||
git remote add mono-filtered "$(josh_auth_url)"
|
||||
git fetch mono-filtered "$mono_branch" || die "Failed to fetch from josh-proxy"
|
||||
|
||||
# 3. Compare trees — skip if subrepo matches josh-filtered view
|
||||
local subrepo_tree josh_tree
|
||||
# shellcheck disable=SC1083 # {tree} is git syntax, not shell brace expansion
|
||||
subrepo_tree=$(git rev-parse "HEAD^{tree}")
|
||||
# shellcheck disable=SC1083
|
||||
josh_tree=$(git rev-parse "mono-filtered/${mono_branch}^{tree}")
|
||||
|
||||
if [ "$subrepo_tree" = "$josh_tree" ]; then
|
||||
log "INFO" "Subrepo tree matches josh-filtered view — nothing to sync"
|
||||
echo "skip"
|
||||
return
|
||||
fi
|
||||
|
||||
# 4. Find new human commits (excludes bot commits from forward sync)
|
||||
# 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
|
||||
if [ "$force_mode" = "1" ]; then
|
||||
log "WARN" "No human commits found — force mode: continuing anyway"
|
||||
else
|
||||
log "INFO" "No new human commits in subrepo — nothing to sync"
|
||||
echo "skip-dirty"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
log "INFO" "New human commits to sync:"
|
||||
echo "$human_commits" >&2
|
||||
|
||||
# 5. Push through josh to a staging branch
|
||||
local ts
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
local staging_branch="auto-sync/subrepo-${subrepo_branch}-${ts}"
|
||||
local push_ref="HEAD"
|
||||
local linearized=false
|
||||
|
||||
if ! git push -o "base=${mono_branch}" "$(josh_auth_url)" "${push_ref}:refs/heads/${staging_branch}" 2>/dev/null; then
|
||||
# Josh rejects pushes when the history contains merge commits whose
|
||||
# parents it cannot map (e.g., merges of staging/feature branches that
|
||||
# include auto-sync history). Fall back to linearizing: cherry-pick
|
||||
# regular commits as-is, squash only the problematic merge commits.
|
||||
# See ADR-011.
|
||||
log "WARN" "Direct push failed — linearizing history (cherry-pick + squash merges)"
|
||||
log "INFO" "─── linearize start ───"
|
||||
|
||||
local original_head
|
||||
original_head=$(git rev-parse HEAD)
|
||||
|
||||
git checkout -b "tmp-linearize-$$" "mono-filtered/${mono_branch}" >/dev/null 2>&1 \
|
||||
|| 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
|
||||
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
|
||||
local parent_count
|
||||
parent_count=$(git cat-file -p "$sha" | grep -c "^parent ")
|
||||
local short_msg
|
||||
short_msg=$(git log -1 --format="%s" "$sha")
|
||||
|
||||
if [ "$parent_count" -le 1 ]; then
|
||||
# Regular commit — cherry-pick to preserve message and authorship
|
||||
if git cherry-pick "$sha" >/dev/null 2>&1; then
|
||||
log "INFO" " cherry-pick ${sha:0:7}: ${short_msg}"
|
||||
else
|
||||
log "WARN" " conflict ${sha:0:7} — applying patch from parent"
|
||||
git cherry-pick --abort 2>/dev/null
|
||||
local msg author_name author_email author_date
|
||||
msg=$(git log -1 --format="%B" "$sha")
|
||||
author_name=$(git log -1 --format="%an" "$sha")
|
||||
author_email=$(git log -1 --format="%ae" "$sha")
|
||||
author_date=$(git log -1 --format="%aI" "$sha")
|
||||
# Diff from the commit's own parent — NOT from HEAD (which is
|
||||
# the linearize branch and may have a different base).
|
||||
git diff "${sha}^" "$sha" | git apply --index >/dev/null 2>&1 \
|
||||
|| die "Failed to apply diff for ${sha:0:7}"
|
||||
GIT_AUTHOR_NAME="$author_name" GIT_AUTHOR_EMAIL="$author_email" \
|
||||
GIT_AUTHOR_DATE="$author_date" \
|
||||
git commit -m "$msg" >/dev/null 2>&1 || die "Failed to commit ${sha:0:7}"
|
||||
log "INFO" " applied ${sha:0:7}: ${short_msg}"
|
||||
fi
|
||||
else
|
||||
# Merge commit — cherry-pick relative to first parent (squashes the merge)
|
||||
local msg author_name author_email author_date
|
||||
msg=$(git log -1 --format="%B" "$sha")
|
||||
author_name=$(git log -1 --format="%an" "$sha")
|
||||
author_email=$(git log -1 --format="%ae" "$sha")
|
||||
author_date=$(git log -1 --format="%aI" "$sha")
|
||||
|
||||
if git cherry-pick -m 1 "$sha" >/dev/null 2>&1; then
|
||||
log "INFO" " squash ${sha:0:7}: ${short_msg}"
|
||||
else
|
||||
# Cherry-pick failed — distinguish empty result (changes already
|
||||
# applied by a prior cherry-pick) from a real conflict.
|
||||
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
|
||||
# Working tree clean — the merge changes are already present
|
||||
git cherry-pick --abort 2>/dev/null || git reset --hard HEAD >/dev/null 2>&1
|
||||
log "INFO" " skip ${sha:0:7}: ${short_msg} (already applied)"
|
||||
else
|
||||
# Real conflict — apply diff from first parent (what the merge
|
||||
# actually introduced, NOT from HEAD which would capture all
|
||||
# tree differences).
|
||||
git cherry-pick --abort 2>/dev/null
|
||||
git diff "${sha}^1" "$sha" | git apply --index >/dev/null 2>&1 \
|
||||
|| die "Failed to apply merge diff for ${sha:0:7}"
|
||||
GIT_AUTHOR_NAME="$author_name" GIT_AUTHOR_EMAIL="$author_email" \
|
||||
GIT_AUTHOR_DATE="$author_date" \
|
||||
git commit -m "$msg" >/dev/null 2>&1 || die "Failed to commit merge ${sha:0:7}"
|
||||
log "INFO" " squash ${sha:0:7}: ${short_msg}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "INFO" "─── linearize done ────"
|
||||
|
||||
push_ref="HEAD"
|
||||
linearized=true
|
||||
|
||||
git push -o "base=${mono_branch}" "$(josh_auth_url)" \
|
||||
"${push_ref}:refs/heads/${staging_branch}" \
|
||||
|| { log "ERROR" "Josh rejected linearized push — check josh-proxy logs"; echo "josh-rejected"; return; }
|
||||
fi
|
||||
|
||||
log "INFO" "Pushed to staging branch via josh: ${staging_branch}${linearized:+ (linearized)}"
|
||||
|
||||
# 6. Publish: force-push directly (--force) or create PR (default)
|
||||
if [ "$force_mode" = "1" ]; then
|
||||
log "WARN" "Force mode: pushing ${staging_branch} directly to mono/${mono_branch} — bypassing PR"
|
||||
local mono_dir="${work_dir}/monorepo"
|
||||
git clone "$(mono_auth_url)" \
|
||||
--branch "$staging_branch" --single-branch \
|
||||
"$mono_dir" || die "Failed to clone monorepo staging branch for force-push"
|
||||
git -C "$mono_dir" push --force "$(mono_auth_url)" \
|
||||
"HEAD:refs/heads/${mono_branch}" \
|
||||
|| die "Failed to force-push to mono/${mono_branch}"
|
||||
log "INFO" "Force-pushed mono/${staging_branch} → mono/${mono_branch}"
|
||||
echo "forced"
|
||||
else
|
||||
local pr_body
|
||||
local linearize_note=""
|
||||
if [ "$linearized" = true ]; then
|
||||
linearize_note="
|
||||
> **Note:** Merge commits were squashed during sync because the subrepo
|
||||
> history contained merges that josh-proxy cannot map (see ADR-011).
|
||||
> Regular commits are preserved individually."
|
||||
fi
|
||||
pr_body="## Subrepo changes
|
||||
|
||||
New commits from subrepo \`${subrepo_branch}\`:
|
||||
|
||||
\`\`\`
|
||||
${human_commits}
|
||||
\`\`\`
|
||||
${linearize_note}
|
||||
**Review checklist:**
|
||||
- [ ] Changes scoped to synced subfolder
|
||||
- [ ] No leaked credentials or environment-specific config
|
||||
- [ ] CI passes"
|
||||
|
||||
create_pr "${MONOREPO_API}" "${GITEA_TOKEN}" \
|
||||
"$mono_branch" "$staging_branch" \
|
||||
"[Subrepo Sync] ${subrepo_branch} → ${mono_branch}" \
|
||||
"$pr_body" \
|
||||
|| die "Failed to create PR on monorepo (check GITEA_TOKEN)"
|
||||
|
||||
log "INFO" "Reverse sync PR created on monorepo"
|
||||
echo "pr-created"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Initial Import: Subrepo → Monorepo (first time) ───────────────
|
||||
#
|
||||
# Used when a subrepo already has content and you're adding it to the
|
||||
# monorepo for the first time. Creates a PR.
|
||||
# Usage: initial_import [clone_url_override]
|
||||
# clone_url_override — if set, clone from this URL instead of subrepo_auth_url()
|
||||
# (used by onboard to clone from the archived repo)
|
||||
# Returns: skip | pr-created
|
||||
|
||||
initial_import() {
|
||||
local clone_url="${1:-$(subrepo_auth_url)}"
|
||||
local mono_branch="$SYNC_BRANCH_MONO"
|
||||
local subrepo_branch="$SYNC_BRANCH_SUBREPO"
|
||||
local subfolder
|
||||
subfolder=$(echo "$JOSH_SYNC_TARGETS" | jq -r --arg n "$JOSH_SYNC_TARGET_NAME" \
|
||||
'.[] | select(.name == $n) | .subfolder')
|
||||
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" "=== Initial import: subrepo/${subrepo_branch} → mono/${mono_branch}:${subfolder}/ ==="
|
||||
|
||||
# 1. Clone monorepo (directly, not through josh — we need the real repo)
|
||||
git clone "$(mono_auth_url)" \
|
||||
--branch "$mono_branch" --single-branch \
|
||||
"${work_dir}/monorepo" || die "Failed to clone monorepo"
|
||||
|
||||
# 2. Clone subrepo (or archived repo when clone_url is overridden)
|
||||
git clone "$clone_url" \
|
||||
--branch "$subrepo_branch" --single-branch \
|
||||
"${work_dir}/subrepo" || die "Failed to clone subrepo"
|
||||
|
||||
local file_count
|
||||
file_count=$(find "${work_dir}/subrepo" -not -path '*/.git/*' -not -path '*/.git' -type f | wc -l | tr -d ' ')
|
||||
log "INFO" "Subrepo has ${file_count} files"
|
||||
|
||||
# 3. Copy subrepo content into monorepo subfolder
|
||||
cd "${work_dir}/monorepo" || exit
|
||||
git config user.name "$BOT_NAME"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
|
||||
local ts
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
local staging_branch="auto-sync/import-${JOSH_SYNC_TARGET_NAME}-${ts}"
|
||||
git checkout -B "$staging_branch" >&2
|
||||
|
||||
mkdir -p "$subfolder"
|
||||
rsync -a --exclude='.git' "${work_dir}/subrepo/" "${subfolder}/"
|
||||
git add "$subfolder"
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
log "INFO" "No changes — subfolder already matches subrepo"
|
||||
echo "skip"
|
||||
return
|
||||
fi
|
||||
|
||||
git commit -m "Import ${JOSH_SYNC_TARGET_NAME} from subrepo/${subrepo_branch}
|
||||
|
||||
${BOT_TRAILER}: import/${JOSH_SYNC_TARGET_NAME}/${ts}" >&2
|
||||
|
||||
# 4. Push branch
|
||||
git push origin "$staging_branch" || die "Failed to push import branch"
|
||||
log "INFO" "Pushed import branch: ${staging_branch}"
|
||||
|
||||
# 5. Create PR on monorepo
|
||||
local pr_body
|
||||
pr_body="## Initial import
|
||||
|
||||
Importing existing subrepo \`${subrepo_branch}\` (${file_count} files) into \`${subfolder}/\`.
|
||||
|
||||
**Review checklist:**
|
||||
- [ ] Content looks correct
|
||||
- [ ] No leaked credentials or environment-specific config
|
||||
- [ ] CI passes"
|
||||
|
||||
create_pr "${MONOREPO_API}" "${GITEA_TOKEN}" \
|
||||
"$mono_branch" "$staging_branch" \
|
||||
"[Import] ${JOSH_SYNC_TARGET_NAME}: ${subrepo_branch}" \
|
||||
"$pr_body" \
|
||||
|| die "Failed to create PR on monorepo (check GITEA_TOKEN)"
|
||||
|
||||
log "INFO" "Import PR created on monorepo"
|
||||
echo "pr-created"
|
||||
}
|
||||
|
||||
# ─── Subrepo Reset: Force-push josh-filtered view to subrepo ───────
|
||||
# Run this AFTER merging an import PR to establish shared josh history.
|
||||
# Returns: reset
|
||||
|
||||
subrepo_reset() {
|
||||
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" "=== Subrepo reset: josh-filtered mono/${mono_branch} → subrepo/${subrepo_branch} ==="
|
||||
|
||||
# 1. Clone monorepo through josh (filtered view — this IS the shared history)
|
||||
log "INFO" "Cloning filtered monorepo via josh-proxy..."
|
||||
git clone "$(josh_auth_url)" \
|
||||
--branch "$mono_branch" --single-branch \
|
||||
"${work_dir}/filtered" || die "Failed to clone through josh-proxy"
|
||||
|
||||
cd "${work_dir}/filtered" || exit
|
||||
|
||||
local head_sha
|
||||
head_sha=$(git rev-parse HEAD)
|
||||
log "INFO" "Josh-filtered HEAD: ${head_sha:0:12}"
|
||||
|
||||
# 2. Force-push to subrepo (replaces subrepo history with josh-filtered history)
|
||||
log "WARN" "Force-pushing to subrepo/${subrepo_branch} — this replaces subrepo history"
|
||||
git push --force "$(subrepo_auth_url)" "HEAD:refs/heads/${subrepo_branch}" \
|
||||
|| die "Failed to force-push to subrepo"
|
||||
|
||||
log "INFO" "Subrepo reset complete — histories are now linked through josh"
|
||||
echo "reset"
|
||||
}
|
||||
Reference in New Issue
Block a user