Files
svn-git-server/svn_mirror/gitea.py
T

225 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Gitea integration push/pull between canonical repo and Gitea's on-disk repo."""
import logging
import os
import subprocess
from pathlib import Path
from typing import Optional
from .mirror import Mirror
logger = logging.getLogger(__name__)
class GiteaError(Exception):
pass
def _get_gitea_repo_path(mirror: Mirror) -> Path:
"""Return the filesystem path to Gitea's bare repo for this mirror."""
try:
return Path(mirror.config.gitea.repo_dir)
except Exception as e:
raise GiteaError(str(e))
# ─── Git subprocess helper ─────────────────────────────────────
def _git(*args: str, timeout: int = 300, check: bool = True) -> str:
"""Run a git command (uses CWD for repo discovery)."""
try:
r = subprocess.run(
["git"] + list(args),
capture_output=True,
text=True,
timeout=timeout,
env={**os.environ, "LC_ALL": "C"},
)
except FileNotFoundError:
raise GiteaError("git not found on PATH")
except subprocess.TimeoutExpired:
raise GiteaError(f"git timed out: {' '.join(args)}")
if check and r.returncode != 0:
raise GiteaError(
f"git failed (exit {r.returncode}): "
f"git {' '.join(args)}\n{r.stderr.strip()}"
)
return r.stdout.strip() if r.returncode == 0 else ""
# ─── Push canonical → Gitea ───────────────────────────────────
def push_to_gitea(mirror: Mirror) -> bool:
"""Sync canonical → Gitea via ``git fetch`` from Gitea's side.
``git fetch`` transfers objects + updates refs without triggering
Gitea's ``pre-receive`` hook, then fires ``post-receive`` manually
for the UI to refresh.
Returns True if anything changed.
"""
gitea_path = _get_gitea_repo_path(mirror)
if not gitea_path.exists():
logger.warning("Gitea repo not found at %s skipping push", gitea_path)
return False
canonical = str(mirror.canonical_dir)
# Grab old refs so we can detect what changed
old_refs: Dict[str, str] = {}
out = _git("--git-dir", str(gitea_path), "for-each-ref",
"--format=%(objectname) %(refname)",
"refs/heads/", "refs/tags/",
timeout=30, check=False)
for line in (out or "").splitlines():
line = line.strip()
if not line:
continue
h, r = line.split(None, 1)
old_refs[r] = h
# Gitea fetches from canonical — this transfers objects + refs,
# and DOES NOT trigger Gitea's pre-receive hook.
_git(
"--git-dir", str(gitea_path), "fetch", "--prune",
canonical,
"+refs/heads/*:refs/heads/*",
"+refs/tags/*:refs/tags/*",
timeout=300,
)
# Build the changelist for post-receive
changed: list = []
cur_refs: dict = {}
out = _git("--git-dir", str(gitea_path), "for-each-ref",
"--format=%(objectname) %(refname)",
"refs/heads/", "refs/tags/",
timeout=30, check=False)
for line in (out or "").splitlines():
line = line.strip()
if not line:
continue
h, r = line.split(None, 1)
cur_refs[r] = h
old = old_refs.get(r, "0" * 40)
if h != old:
changed.append((old, h, r))
# Detect deletions (refs present before fetch but gone now)
for r, old_h in old_refs.items():
if r not in cur_refs:
changed.append((old_h, "0" * 40, r))
if not changed:
logger.debug("No new commits to push to Gitea")
return False
logger.info("Synced %d ref(s) to Gitea", len(changed))
_trigger_gitea_post_receive(changed, mirror.config.gitea)
return True
def _trigger_gitea_post_receive(changed: list, gitea_cfg):
"""Fire Gitea's post-receive hook so the UI refreshes immediately."""
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")
lines = "\n".join(f"{o} {n} {r}" for o, n, r in changed)
r = subprocess.run(
[hook_bin, "hook", "--config", config_path, "post-receive"],
input=lines.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 ──────────────────────────────────
def fetch_from_gitea(mirror: Mirror, ref: Optional[str] = None) -> bool:
"""Fetch refs from Gitea's bare repo into the canonical repo.
If *ref* is given (e.g. ``refs/heads/master``), only that ref is
fetched. Otherwise all heads are fetched.
Returns True if anything new was fetched.
"""
gitea_path = _get_gitea_repo_path(mirror)
if not gitea_path.exists():
raise GiteaError(f"Gitea repo not found at {gitea_path}")
canonical = str(mirror.canonical_dir)
if ref:
refspec = f"+{ref}:refs/remotes/gitea/{ref.removeprefix('refs/heads/')}"
else:
refspec = "+refs/heads/*:refs/remotes/gitea/*"
logger.info("Fetching %s → canonical …", gitea_path)
_git(
"--git-dir", canonical, "fetch", "--prune",
str(gitea_path), refspec,
timeout=300,
)
logger.debug("Fetch from Gitea complete")
# Fast-forward local branches from remote-tracking refs
if not ref:
_ff_local_branches(canonical)
return True
def _ff_local_branches(git_dir: str):
"""Fast-forward ``refs/heads/*`` from ``refs/remotes/gitea/*``."""
out = _git("--git-dir", git_dir, "for-each-ref",
"--format=%(refname)", "refs/remotes/gitea/",
timeout=30, check=False)
for rt_ref in out.splitlines():
rt_ref = rt_ref.strip()
if not rt_ref:
continue
branch = rt_ref.removeprefix("refs/remotes/gitea/")
local_ref = f"refs/heads/{branch}"
# Check if local branch exists
local_hash = _git("--git-dir", git_dir, "rev-parse", "--verify",
"--quiet", local_ref, timeout=15, check=False)
if not local_hash:
_git("--git-dir", git_dir, "update-ref", local_ref, rt_ref,
timeout=15)
logger.debug("Created new branch %s from Gitea", local_ref)
continue
# Fast-forward if possible
rt_hash = _git("--git-dir", git_dir, "rev-parse", "--verify",
rt_ref, timeout=15, check=False)
if not rt_hash or rt_hash == local_hash:
continue
base = _git("--git-dir", git_dir, "merge-base", local_hash, rt_ref,
timeout=15, check=False)
if base == local_hash:
_git("--git-dir", git_dir, "update-ref", local_ref, rt_ref,
timeout=15)
logger.debug("Fast-forward %s from Gitea", local_ref)