Fix gitea-github-sync.sh: stop mirroring refs/pull/* into Gitea/GitHub

Both sync directions used `git clone --bare` for the first clone and
`git push --mirror` for the push. A bare clone pulls in every ref the
remote advertises, refs/pull/*/head included — GitHub (and Gitea,
same behavior) exposes PR refs over the same smart-HTTP endpoint a
plain bare clone reads from. --mirror then pushes every local ref
verbatim, including those, and gets rejected: Gitea's server-side
hook (and GitHub's own PR-ref protection) reserves that namespace for
itself.

  remote: error: hook declined to update refs/pull/1/head
  ! [remote rejected] refs/pull/1/head (hook declined)

Fix: scope the initial clone (now git init --bare + fetch, unified
with the repeat-sync path instead of a separate git-clone branch) and
the push to an explicit refs/heads/*:refs/heads/* + refs/tags/*:refs/tags/*
refspec in both directions, matching the refspec discipline the fetch
side already had. Added a defensive cleanup (delete any
refs/pull/*, refs/merge-requests/*, refs/changes/* found in the local
bare mirror before pushing) so a repo synced before this fix
self-heals on its next run instead of tripping the same hook forever.
This commit is contained in:
Claude
2026-08-31 18:51:53 +00:00
parent f8bfce87d9
commit 3ffcde7294
+47 -15
View File
@@ -277,17 +277,34 @@ sync_github_to_gitea() {
# old commit indefinitely, with no error at any step. Also no longer
# silencing stderr: a real auth/network failure should be visible in the
# log, not just "Failed to fetch" with no reason why.
local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}"
if [[ -d "$local_path" ]]; then
info "Fetching $full_name from GitHub..."
git -C "$local_path" fetch origin '+refs/heads/*:refs/heads/*' --prune --quiet || {
err "Failed to fetch $full_name"; return 1; }
else
info "Cloning $full_name from GitHub..."
mkdir -p "$(dirname "$local_path")"
local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}"
git clone --bare --quiet "$auth_url" "$local_path" || {
err "Failed to clone $full_name"; return 1; }
git init --bare --quiet "$local_path" || { err "Failed to init $full_name"; return 1; }
git -C "$local_path" remote add origin "$auth_url"
fi
# Explicit heads+tags refspec on BOTH the initial clone and every later
# fetch, not `git clone --bare` (which pulls every ref the remote
# advertises, refs/pull/*/head included) — GitHub exposes PR refs over
# the same smart-HTTP endpoint a plain bare clone reads from, and those
# live in a namespace Gitea's own PR system reserves for itself. A later
# `git push --mirror` (pushes every local ref verbatim) then gets
# rejected by Gitea's server-side hook — confirmed live: "hook declined
# to update refs/pull/1/head". Scoping fetch AND push to heads/tags only
# avoids ever touching that namespace in either direction.
git -C "$local_path" fetch origin \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' \
--prune --quiet || { err "Failed to fetch $full_name"; return 1; }
# Self-heals a repo synced before this fix — a stray refs/pull/* (or any
# other non-heads/tags ref) an earlier run's unscoped `clone --bare`
# already pulled in would otherwise keep tripping the same Gitea hook on
# every sync from here on, with no other way to clear it.
git -C "$local_path" for-each-ref --format='%(refname)' \
'refs/pull/*' 'refs/merge-requests/*' 'refs/changes/*' \
| xargs -r -n1 git -C "$local_path" update-ref -d
# Ensure repo exists on Gitea
local gitea_check
@@ -299,12 +316,17 @@ sync_github_to_gitea() {
>/dev/null || { err "Failed to create $repo_name on Gitea"; return 1; }
fi
# Push to Gitea
# Push to Gitea — same explicit heads+tags scoping as the fetch above,
# not --mirror (which would push refs/pull/* etc. verbatim and hit the
# same rejected-hook failure this whole fix is for). --prune still makes
# Gitea's heads/tags a true mirror of GitHub's (deletes ones GitHub no
# longer has), just without ever touching reserved ref namespaces.
local gitea_push_url="${GITEA_URL/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}"
gitea_push_url="${gitea_push_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}"
gitea_push_url="$gitea_push_url/$GITEA_USER/$repo_name.git"
git -C "$local_path" push --mirror "$gitea_push_url" --quiet || {
git -C "$local_path" push --prune --quiet "$gitea_push_url" \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' || {
err "Failed to push $full_name to Gitea"; return 1; }
ok "GitHub → Gitea: $full_name"
_log "PULL $full_name OK"
@@ -320,18 +342,26 @@ sync_gitea_to_github() {
local gitea_auth_url="${clone_url/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}"
gitea_auth_url="${gitea_auth_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}"
# See the matching comment in sync_github_to_gitea() above — same
# explicit-refspec, visible-stderr fix, same reason.
# See the matching comment in sync_github_to_gitea() above — same reason
# applies in reverse: Gitea also exposes PR refs (refs/pull/*/head) over
# its git smart-HTTP endpoint, and GitHub rejects direct pushes to that
# same reserved namespace just as Gitea's hook does. Explicit heads+tags
# refspec on the initial clone too, not `git clone --bare`.
if [[ -d "$local_path" ]]; then
info "Fetching $full_name from Gitea..."
git -C "$local_path" fetch origin '+refs/heads/*:refs/heads/*' --prune --quiet || {
err "Failed to fetch $full_name from Gitea"; return 1; }
else
info "Cloning $full_name from Gitea..."
mkdir -p "$(dirname "$local_path")"
git clone --bare --quiet "$gitea_auth_url" "$local_path" || {
err "Failed to clone $full_name from Gitea"; return 1; }
git init --bare --quiet "$local_path" || { err "Failed to init $full_name"; return 1; }
git -C "$local_path" remote add origin "$gitea_auth_url"
fi
git -C "$local_path" fetch origin \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' \
--prune --quiet || { err "Failed to fetch $full_name from Gitea"; return 1; }
# Self-heals a repo synced before this fix — see the matching comment above.
git -C "$local_path" for-each-ref --format='%(refname)' \
'refs/pull/*' 'refs/merge-requests/*' 'refs/changes/*' \
| xargs -r -n1 git -C "$local_path" update-ref -d
# Ensure repo exists on GitHub
local gh_check
@@ -343,9 +373,11 @@ sync_gitea_to_github() {
>/dev/null || { err "Failed to create $repo_name on GitHub"; return 1; }
fi
# Push to GitHub
# Push to GitHub — explicit heads+tags scoping, not --mirror. Same
# reasoning as the Gitea push above.
local github_push_url="https://$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo_name.git"
git -C "$local_path" push --mirror "$github_push_url" --quiet || {
git -C "$local_path" push --prune --quiet "$github_push_url" \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' || {
err "Failed to push $full_name to GitHub"; return 1; }
ok "Gitea → GitHub: $full_name"
_log "PUSH $full_name OK"