Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
# Issue-Migration Gitea -> GitLab (gitops#48). Stdlib-only.
|
|
#
|
|
# Eigenschaften:
|
|
# - Dry-Run per Default; --execute schreibt wirklich
|
|
# - idempotent: Marker "<!-- gitea-migration: OWNER/REPO#N -->" in der
|
|
# GitLab-Beschreibung; bereits migrierte Issues werden uebersprungen
|
|
# - Kommentare werden als Notes uebernommen (Original-Autor + -Zeit im Text,
|
|
# da die API-Autorschaft auf den Token-User laeuft — bekannter, akzeptierter
|
|
# Verlust, siehe gitops#48)
|
|
# - Zeitstempel bleiben erhalten: der Admin-Token darf created_at setzen
|
|
# - geschlossene Issues werden nach Anlage geschlossen
|
|
# - Labels werden namensgleich uebernommen (Gruppen-Labels muessen existieren)
|
|
# - PRs werden ausgelassen (Gitea listet sie im Issue-Endpunkt mit)
|
|
#
|
|
# Aufruf:
|
|
# python3 migrate.py sorb/thread-net-git 18 # Dry-Run
|
|
# python3 migrate.py sorb/thread-net-git 18 --execute
|
|
import json
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
GITEA = "https://rohana.axion1337.de"
|
|
GITLAB = "https://git.lab"
|
|
GITEA_TOKEN = open("/Users/thore/.config/gitea-rohana/token").read().strip()
|
|
GITLAB_TOKEN = open("/Users/thore/.config/gitlab-lab/token").read().strip()
|
|
|
|
|
|
def gitea(path):
|
|
req = urllib.request.Request(f"{GITEA}/api/v1{path}",
|
|
headers={"Authorization": f"token {GITEA_TOKEN}"})
|
|
return json.loads(urllib.request.urlopen(req).read())
|
|
|
|
|
|
def gitlab(method, path, payload=None):
|
|
data = urllib.parse.urlencode(payload).encode() if payload else None
|
|
req = urllib.request.Request(f"{GITLAB}/api/v4{path}", data=data,
|
|
headers={"PRIVATE-TOKEN": GITLAB_TOKEN}, method=method)
|
|
return json.loads(urllib.request.urlopen(req).read())
|
|
|
|
|
|
def main():
|
|
repo = sys.argv[1]
|
|
project = sys.argv[2]
|
|
execute = "--execute" in sys.argv
|
|
|
|
# Bestehende Migrations-Marker im Zielprojekt einsammeln (Idempotenz)
|
|
existing = set()
|
|
page = 1
|
|
while True:
|
|
batch = gitlab("GET", f"/projects/{project}/issues?per_page=100&page={page}&scope=all&state=all")
|
|
if not batch:
|
|
break
|
|
for i in batch:
|
|
desc = i.get("description") or ""
|
|
for line in desc.splitlines():
|
|
if line.startswith("<!-- gitea-migration: "):
|
|
existing.add(line.strip())
|
|
page += 1
|
|
|
|
# Gitea-Issues (alle Zustaende), PRs ausfiltern
|
|
issues = []
|
|
page = 1
|
|
while True:
|
|
batch = gitea(f"/repos/{repo}/issues?state=all&type=issues&limit=50&page={page}")
|
|
if not batch:
|
|
break
|
|
issues.extend(batch)
|
|
page += 1
|
|
issues.sort(key=lambda i: i["number"]) # aufsteigend = chronologische Anlage
|
|
|
|
print(f"{repo}: {len(issues)} Issues gefunden, Ziel-Projekt {project}, "
|
|
f"{'EXECUTE' if execute else 'DRY-RUN'}")
|
|
|
|
for i in issues:
|
|
n = i["number"]
|
|
marker = f"<!-- gitea-migration: {repo}#{n} -->"
|
|
if marker in existing:
|
|
print(f" #{n} bereits migriert - uebersprungen")
|
|
continue
|
|
labels = ",".join(l["name"] for l in i.get("labels", []))
|
|
comments = gitea(f"/repos/{repo}/issues/{n}/comments") if i.get("comments") else []
|
|
footer = (f"\n\n---\n*Migriert aus Gitea [{repo}#{n}]({GITEA}/{repo}/issues/{n}) "
|
|
f"— dort erstellt am {i['created_at'][:10]} von {i['user']['login']}.*\n{marker}")
|
|
print(f" #{n} [{i['state']}] {i['title'][:60]} | labels: {labels or '-'} | "
|
|
f"{len(comments)} kommentare")
|
|
if not execute:
|
|
continue
|
|
new = gitlab("POST", f"/projects/{project}/issues", {
|
|
"title": i["title"],
|
|
"description": (i.get("body") or "") + footer,
|
|
"labels": labels,
|
|
"created_at": i["created_at"],
|
|
})
|
|
for c in comments:
|
|
gitlab("POST", f"/projects/{project}/issues/{new['iid']}/notes", {
|
|
"body": f"**{c['user']['login']} am {c['created_at'][:16].replace('T', ' ')} (Gitea):**\n\n{c['body']}",
|
|
"created_at": c["created_at"],
|
|
})
|
|
if i["state"] == "closed":
|
|
gitlab("PUT", f"/projects/{project}/issues/{new['iid']}", {"state_event": "close"})
|
|
print(f" -> GitLab #{new['iid']} angelegt ({i['state']})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|