diff --git a/vendor/ai-stack/gitea-github-sync.sh b/vendor/ai-stack/gitea-github-sync.sh index cd7f5b2..c07d78f 100755 --- a/vendor/ai-stack/gitea-github-sync.sh +++ b/vendor/ai-stack/gitea-github-sync.sh @@ -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"