#!/usr/bin/env python3
"""Gleicht Uebersetzungen zwischen zwei Branches ab -- Eintrag fuer Eintrag.
Unser Arbeitsmodell: gearbeitet wird in 3.15, von dort wird nach 3.14
zurueckgespiegelt. Dieses Skript macht den Abgleich nachvollziehbar, statt
eine Datei pauschal zu ueberschreiben.
Warum nicht einfach pomerge? pomerge kennt nur Quelle und Ziel. Es sieht
nicht, WER einen Eintrag zuletzt angefasst hat, und ueberschreibt deshalb
auch Arbeit, die nur im Zielbranch existiert. Dieses Skript vergleicht
beide Seiten gegen ihren gemeinsamen Vorfahren und unterscheidet damit:
nur die Quelle hat sich bewegt -> wird uebertragen
nur das Ziel hat sich bewegt -> bleibt unangetastet
beide haben sich bewegt -> Konflikt, wird gemeldet
Quelle ist leer oder fuzzy -> nie uebertragen
Der letzte Punkt ist wichtig: ein msgmerge kann eine Uebersetzung im
Zielbranch entfernt haben. Mit --leere-ziele werden genau diese Luecken
aus der Quelle wieder gefuellt.
Aufruf (im Repo, das ZIEL muss ausgecheckt sein):
python tools/sync_branches.py --von 3.15 # Trockenlauf
python tools/sync_branches.py --von 3.15 --apply
python tools/sync_branches.py --von 3.15 --leere-ziele --apply
python tools/sync_branches.py --von 3.15 --nur reference,library
python tools/sync_branches.py --von 3.15 -o konflikte.md
Ohne --apply wird nichts geschrieben. Konflikte werden nie automatisch
entschieden -- sie landen im Bericht und gehoeren von Hand geprueft.
"""
import argparse
import os
import subprocess
import sys
import tempfile
try:
import polib
except ImportError:
sys.exit("polib fehlt. Im aktiven venv installieren: pip install polib")
def git(*args, repo="."):
e = subprocess.run(["git", "--no-optional-locks", *args], cwd=repo,
capture_output=True, text=True)
return e.stdout if e.returncode == 0 else None
def karte(text):
"""{msgid: msgstr} aus einem PO-Text; fuzzy zaehlt als unuebersetzt."""
with tempfile.NamedTemporaryFile("w", suffix=".po", encoding="utf-8",
delete=False) as f:
f.write(text)
tmp = f.name
try:
po = polib.pofile(tmp)
return {e.msgid: ("" if "fuzzy" in e.flags else e.msgstr)
for e in po if not e.obsolete and e.msgid}
finally:
os.unlink(tmp)
def po_dateien(ref):
aus = git("ls-tree", "-r", "--name-only", ref) or ""
return {z for z in aus.split("\n") if z.endswith(".po")}
def main() -> None:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--von", required=True, metavar="BRANCH",
help="Quellbranch, aus dem uebertragen wird (z. B. 3.15)")
p.add_argument("--apply", action="store_true",
help="tatsaechlich schreiben (ohne dies nur ein Trockenlauf)")
p.add_argument("--leere-ziele", action="store_true", dest="leere_ziele",
help="auch Eintraege fuellen, die im Ziel leer sind, "
"obwohl sich die Quelle nicht bewegt hat")
p.add_argument("--nur", help="nur diese Pfadanfaenge, kommagetrennt")
p.add_argument("-o", "--ausgabe", metavar="DATEI",
help="Konfliktbericht als Markdown schreiben")
args = p.parse_args()
ziel = (git("rev-parse", "--abbrev-ref", "HEAD") or "").strip()
if not ziel:
sys.exit("Kein Git-Repository -- bitte im python-docs-de-Verzeichnis starten.")
if ziel == args.von:
sys.exit(f"Quelle und Ziel sind derselbe Branch ({ziel}).")
# Nur geaenderte .po-Dateien sind gefaehrlich: sie wuerden mit den
# uebertragenen Eintraegen vermischt. Unversionierte Dateien und
# geaenderte Skripte stoeren nicht.
schmutzig = [z[3:] for z in (git("status", "--porcelain", "--untracked-files=no")
or "").splitlines() if z[3:].endswith(".po")]
if schmutzig and args.apply:
sys.exit(f"{len(schmutzig)} .po-Datei(en) sind ungespeichert geaendert, "
f"z. B. {schmutzig[0]}.\nBitte erst committen oder stashen.")
vorfahr = (git("merge-base", ziel, args.von) or "").strip()
if not vorfahr:
sys.exit(f"Kein gemeinsamer Vorfahr von {ziel} und {args.von} gefunden.")
dateien = sorted(po_dateien(ziel) & po_dateien(args.von))
if args.nur:
anfaenge = tuple(a.strip() for a in args.nur.split(",") if a.strip())
dateien = [d for d in dateien if d.startswith(anfaenge)]
print(f"{ziel}