Files
JMR-devandClaude Opus 5.5 f672019c91 Add the GitHub to Gitea migration scripts
These moved every non-fork JMR-dev repository from GitHub into this Gitea
instance and made GitHub a push mirror. They are kept for re-runs and
for rotating the mirror PAT, which expires and fails silently.

- migrate.py    full one-time migration (code, issues, PRs, releases,
                wiki, LFS); skips anything already on Gitea
- verify.py     compares branch/tag shas, issue/PR/release counts, and
                the archived and visibility flags; read-only
- repoint.py    re-points local clones' JMR-dev remotes at Gitea,
                following GitHub renames; dry run unless --apply
- pushmirror.py sync-on-commit push mirrors to GitHub, created only when
                every GitHub branch and tag already matches Gitea, so
                the first force-push/prune cannot delete anything

Tokens come from the environment, sourced from Secret Manager. None
appear in these files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:16:18 +07:00

86 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Re-point local clones' remotes from GitHub (JMR-dev) to Gitea.
Only remotes whose URL is a JMR-dev GitHub repository that exists on Gitea are
changed. Third-party remotes (upstreams, other owners) and JMR-dev repos that
were not migrated (forks) are left untouched and reported. GitHub renames are
followed via the API redirect. Every change is appended to a TSV so it can be
reverted with `git remote set-url <remote> <old-url>`.
Usage: GITEA_TOKEN=... repoint.py <dirs-file> <workspace-root> <changes.tsv> [--apply]
Without --apply it only prints the plan.
"""
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
OWNER = "JMR-dev"
GITEA = "https://gitea.jasonmross.dev"
GITEA_SSH = "ssh://git@gitea.jasonmross.dev:2222"
GH_URL = re.compile(r"^(?:git@github\.com:|ssh://git@github\.com/|https://github\.com/)([^/]+)/(.+?)(?:\.git)?/?$")
def git(cwd, *args):
return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
def gitea_has(name):
req = urllib.request.Request(f"{GITEA}/api/v1/repos/{OWNER}/{name}",
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["name"]
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
def canonical(name):
"""Follow a GitHub rename: the API redirects old names to the new repo."""
r = subprocess.run(["gh", "api", f"repos/{OWNER}/{name}", "--jq", ".name"], capture_output=True, text=True)
return r.stdout.strip() or name
def main():
dirs_file, root, changes_file, *flags = sys.argv[1:]
apply = "--apply" in flags
out = open(changes_file, "a") if apply else None
for rel in open(dirs_file).read().split():
d = os.path.normpath(os.path.join(root, rel))
remotes = git(d, "remote").stdout.split()
for remote in remotes:
url = git(d, "remote", "get-url", remote).stdout.strip()
m = GH_URL.match(url)
if not m:
print(f"SKIP {rel:38} {remote:9} not a GitHub URL: {url}")
continue
owner, name = m.groups()
if owner != OWNER:
print(f"SKIP {rel:38} {remote:9} third-party ({owner}/{name})")
continue
target = gitea_has(canonical(name))
if not target:
print(f"SKIP {rel:38} {remote:9} {owner}/{name} is not on Gitea (fork, not migrated)")
continue
new = f"{GITEA_SSH}/{OWNER}/{target}.git"
if url == new:
print(f"OK {rel:38} {remote:9} already {new}")
continue
print(f"{'CHANGE' if apply else 'PLAN '} {rel:38} {remote:9} {url} -> {new}")
if apply:
r = git(d, "remote", "set-url", remote, new)
if r.returncode:
print(f" !! set-url failed: {r.stderr.strip()}")
continue
out.write(f"{d}\t{remote}\t{url}\t{new}\n")
out.flush()
if __name__ == "__main__":
main()