See More

#!/usr/bin/env python3 """Vergleicht die Uebersetzungen zweier Branches Datei fuer Datei. Fuer jede .po-Datei wird verglichen, wie viele Eintraege in beiden Branches denselben msgstr haben. Der Prozentwert bezieht sich auf die Anzahl der Eintraege im *neueren* Branch (Vorgabe 3.15), weil der die Zielversion ist: Aehnlichkeit = identische Eintraege / Eintraege in 3.15 Ein Eintrag gilt als identisch, wenn msgid (samt msgctxt) und msgstr in beiden Branches exakt uebereinstimmen -- auch dann, wenn beide leer sind. Obsolete Eintraege (#~) und der Kopfblock bleiben aussen vor. Aufruf: python branch_diff.py # 3.14 gegen 3.15 python branch_diff.py 3.14 main python branch_diff.py --csv bericht.csv python branch_diff.py --nur-abweichungen Es wird nichts ausgecheckt und nichts geaendert -- die Inhalte kommen ueber "git show" direkt aus den Branches. """ import argparse import csv import os import subprocess import sys import tempfile try: import polib except ImportError: sys.exit("polib fehlt. Im aktiven venv installieren: pip install polib") AUSSCHLUSS = (".venv/", ".git/") KLASSEN = [ (100.0, "deckungsgleich"), (80.0, "ueber 80 %"), (50.0, "ueber 50 %"), (30.0, "ueber 30 %"), (0.0, "unter 30 %"), ] def git(*args: str) -> str: """git aufrufen und stdout zurueckgeben.""" return subprocess.run( ["git", *args], capture_output=True, text=True, check=True ).stdout def po_dateien(branch: str) -> dict: """Alle .po-Dateien eines Branches als {pfad: blob-hash}.""" aus = {} for zeile in git("ls-tree", "-r", branch).splitlines(): kopf, pfad = zeile.split("\t", 1) _modus, art, blob = kopf.split() if art != "blob" or not pfad.endswith(".po"): continue if pfad.startswith(AUSSCHLUSS): continue aus[pfad] = blob return aus def eintraege(blob: str) -> dict: """Blob-Inhalt parsen: {(msgctxt, msgid): msgstr}.""" inhalt = git("cat-file", "blob", blob) # polib will einen Dateipfad -- also kurz zwischenspeichern. with tempfile.NamedTemporaryFile( "w", suffix=".po", encoding="utf-8", delete=False ) as f: f.write(inhalt) tmp = f.name try: po = polib.pofile(tmp) return {(e.msgctxt, e.msgid): e.msgstr for e in po if not e.obsolete} finally: os.unlink(tmp) def klasse_von(prozent: float) -> str: for grenze, name in KLASSEN: if prozent >= grenze: return name return KLASSEN[-1][1] def uebersetzt(werte) -> int: return sum(1 for v in werte if v.strip()) def main() -> None: p = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) p.add_argument("alt", nargs="?", default="3.14", help="aelterer Branch") p.add_argument("neu", nargs="?", default="3.15", help="neuerer Branch") p.add_argument("--csv", metavar="DATEI", help="Ergebnis zusaetzlich als CSV") p.add_argument( "--nur-abweichungen", action="store_true", help="deckungsgleiche Dateien in der Tabelle weglassen", ) args = p.parse_args() alt_dateien = po_dateien(args.alt) neu_dateien = po_dateien(args.neu) alle = sorted(set(alt_dateien) | set(neu_dateien)) print(f"{len(alle)} Dateien werden verglichen ...", file=sys.stderr) zeilen = [] for nr, pfad in enumerate(alle, 1): if nr % 50 == 0: print(f" {nr}/{len(alle)}", file=sys.stderr) a_blob = alt_dateien.get(pfad) n_blob = neu_dateien.get(pfad) if a_blob is None: n = eintraege(n_blob) zeilen.append((pfad, 0, len(n), 0, uebersetzt(n.values()), 0, None, f"nur in {args.neu}")) continue if n_blob is None: a = eintraege(a_blob) zeilen.append((pfad, len(a), 0, uebersetzt(a.values()), 0, 0, None, f"nur in {args.alt}")) continue if a_blob == n_blob: # Gleicher Blob -- Inhalt garantiert identisch, kein Parsen noetig. a = eintraege(a_blob) u = uebersetzt(a.values()) zeilen.append((pfad, len(a), len(a), u, u, len(a), 100.0, "deckungsgleich")) continue a = eintraege(a_blob) n = eintraege(n_blob) gleich = sum(1 for k, v in n.items() if k in a and a[k] == v) prozent = 100.0 * gleich / len(n) if n else 0.0 zeilen.append((pfad, len(a), len(n), uebersetzt(a.values()), uebersetzt(n.values()), gleich, prozent, klasse_von(prozent))) # --- Tabelle ----------------------------------------------------------- rang = {name: i for i, (_g, name) in enumerate(KLASSEN)} rang[f"nur in {args.neu}"] = 90 rang[f"nur in {args.alt}"] = 91 zeilen.sort(key=lambda z: (rang.get(z[7], 99), -(z[6] or 0), z[0])) kopf = (f"| Datei | Eintraege {args.alt} | Eintraege {args.neu} | " f"uebers. {args.alt} | uebers. {args.neu} | identisch | " f"Aehnlichkeit | Klasse |") print() print(kopf) print("|---|---:|---:|---:|---:|---:|---:|---|") for pfad, ea, en, ua, un, gl, pz, kl in zeilen: if args.nur_abweichungen and kl == "deckungsgleich": continue pzt = "—" if pz is None else f"{pz:.1f} %" print(f"| `{pfad}` | {ea} | {en} | {ua} | {un} | {gl} | {pzt} | {kl} |") # --- Zusammenfassung --------------------------------------------------- print("\n### Zusammenfassung\n") print("| Klasse | Dateien |") print("|---|---:|") for _grenze, name in KLASSEN: anzahl = sum(1 for z in zeilen if z[7] == name) print(f"| {name} | {anzahl} |") for name in (f"nur in {args.neu}", f"nur in {args.alt}"): anzahl = sum(1 for z in zeilen if z[7] == name) if anzahl: print(f"| {name} | {anzahl} |") print(f"| **gesamt** | **{len(zeilen)}** |") if args.csv: with open(args.csv, "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["datei", f"eintraege_{args.alt}", f"eintraege_{args.neu}", f"uebersetzt_{args.alt}", f"uebersetzt_{args.neu}", "identisch", "aehnlichkeit_prozent", "klasse"]) for z in zeilen: w.writerow([z[0], z[1], z[2], z[3], z[4], z[5], "" if z[6] is None else f"{z[6]:.1f}", z[7]]) print(f"\nCSV geschrieben: {args.csv}", file=sys.stderr) if __name__ == "__main__": main()