refactor: bypass Gitea pre-receive via update-ref + manual post-receive

This commit is contained in:
2026-06-24 13:33:45 +02:00
parent d123e80586
commit 811e3748d4
+78 -21
View File
@@ -4,7 +4,7 @@ import logging
import os import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Dict, Optional
from .mirror import Mirror from .mirror import Mirror
@@ -52,9 +52,12 @@ def _git(*args: str, timeout: int = 300, check: bool = True) -> str:
def push_to_gitea(mirror: Mirror) -> bool: def push_to_gitea(mirror: Mirror) -> bool:
"""Push all heads and tags from the canonical repo to Gitea's bare repo. """Sync canonical refs to Gitea's bare repo via direct ref updates.
Returns True if anything was pushed. Uses ``git update-ref`` to bypass Gitea's pre-receive hook, then
fires Gitea's ``post-receive`` hook manually so the UI refreshes.
Returns True if anything changed.
""" """
gitea_path = _get_gitea_repo_path(mirror) gitea_path = _get_gitea_repo_path(mirror)
if not gitea_path.exists(): if not gitea_path.exists():
@@ -63,41 +66,95 @@ def push_to_gitea(mirror: Mirror) -> bool:
canonical = str(mirror.canonical_dir) canonical = str(mirror.canonical_dir)
# Check if there's anything to push by comparing refs # Collect canonical refs
out = _git("--git-dir", canonical, "for-each-ref", out = _git("--git-dir", canonical, "for-each-ref",
"--format=%(objectname) %(refname)", "--format=%(objectname) %(refname)",
"refs/heads/", "refs/tags/", "refs/heads/", "refs/tags/",
timeout=30, check=False) timeout=30, check=False)
if out: canonical_refs: Dict[str, str] = {}
has_new = False for line in (out or "").splitlines():
for line in out.splitlines():
line = line.strip() line = line.strip()
if not line: if not line:
continue continue
obj_hash, ref = line.split(None, 1) obj_hash, ref = line.split(None, 1)
canonical_refs[ref] = obj_hash
changed: list = []
for ref, obj_hash in canonical_refs.items():
gitea_hash = _git("--git-dir", str(gitea_path), "rev-parse", gitea_hash = _git("--git-dir", str(gitea_path), "rev-parse",
"--verify", "--quiet", ref, "--verify", "--quiet", ref,
timeout=30, check=False) timeout=15, check=False)
if obj_hash != gitea_hash: if obj_hash != gitea_hash:
has_new = True if gitea_hash:
break _git("--git-dir", str(gitea_path), "update-ref", ref,
if not has_new: obj_hash, gitea_hash, timeout=15)
else:
_git("--git-dir", str(gitea_path), "update-ref", ref,
obj_hash, timeout=15)
changed.append((gitea_hash or "0" * 40, obj_hash, ref))
logger.debug("Updated %s in Gitea", ref)
# Prune refs in Gitea not in canonical
gitea_out = _git("--git-dir", str(gitea_path), "for-each-ref",
"--format=%(objectname) %(refname)",
"refs/heads/", "refs/tags/",
timeout=30, check=False)
for line in (gitea_out or "").splitlines():
line = line.strip()
if not line:
continue
obj_hash, ref = line.split(None, 1)
if ref not in canonical_refs:
_git("--git-dir", str(gitea_path), "update-ref", "-d", ref,
obj_hash, timeout=15)
changed.append((obj_hash, "0" * 40, ref))
logger.debug("Pruned %s from Gitea", ref)
if not changed:
logger.debug("No new commits to push to Gitea") logger.debug("No new commits to push to Gitea")
return False return False
logger.info("Pushing canonical → Gitea (%s) …", gitea_path) logger.info("Synced %d ref(s) to Gitea", len(changed))
# Use `git push --prune` with explicit refspecs to mirror branches and tags _trigger_gitea_post_receive(gitea_path, mirror.config.gitea)
_git(
"--git-dir", canonical, "push", "--prune",
str(gitea_path),
"+refs/heads/*:refs/heads/*",
"+refs/tags/*:refs/tags/*",
timeout=300,
)
logger.info("Push to Gitea complete")
return True return True
def _trigger_gitea_post_receive(gitea_path: Path, gitea_cfg):
"""Fire Gitea's post-receive hook so the UI refreshes immediately."""
# Find the gitea hook binary
candidates = ("/usr/local/bin/gitea", "/usr/bin/gitea", "gitea")
hook_bin = None
for c in candidates:
if subprocess.run(["which", c], capture_output=True, check=False).returncode == 0:
hook_bin = c
break
if not hook_bin:
logger.debug("gitea binary not found skipping post-receive hook")
return
try:
config_path = os.environ.get("GITEA_CONFIG", "/etc/gitea/app.ini")
# Build stdin with the ref updates that would normally arrive
# Format: "<old-hash> <new-hash> <refname>\n"
data = _git("--git-dir", str(gitea_path), "for-each-ref",
"--format=%(objectname) %(objectname) %(refname)",
"refs/heads/", "refs/tags/",
timeout=15, check=False)
r = subprocess.run(
[hook_bin, "hook", "--config", config_path, "post-receive"],
input=(data or "").encode(),
capture_output=True,
timeout=60,
)
if r.returncode != 0:
err = (r.stderr or b"").decode(errors="replace").strip()
logger.warning("Gitea post-receive hook failed: %s", err)
else:
logger.debug("Gitea post-receive hook fired")
except Exception as e:
logger.warning("Could not fire Gitea post-receive hook: %s", e)
# ─── Fetch Gitea → canonical ────────────────────────────────── # ─── Fetch Gitea → canonical ──────────────────────────────────