-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtx_push.py
More file actions
194 lines (159 loc) · 6.69 KB
/
Copy pathtx_push.py
File metadata and controls
194 lines (159 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#!/usr/bin/env python3
"""Interaktiver Transifex-Push: fragt, welche Dateien hochgeladen werden.
Das Skript liest .tx/config des aktuellen Branches, ordnet jeder .po-Datei
ihren Transifex-Resource-Namen zu und stellt sie zur Auswahl. Gepusht wird
mit der forcierten Kombination, die wir beim Abgleich benutzen:
tx push -t -l de -f --skip --replace-edited-strings <resource> ...
-t nur Uebersetzungen (die Quelltexte kommen
aus CPython, nicht von uns)
-l de nur die deutsche Sprache
-f Zeitstempel-Sperre aus ("remote is newer")
--replace-edited-strings ueberschreibt auch in Transifex bearbeitete
Strings -- GitHub ist die Quelle der Wahrheit
--skip bricht nicht ab, wenn eine Ressource zickt
Aufruf:
python tools/tx_push.py # geaenderte Dateien vorschlagen
python tools/tx_push.py --alle # alle Dateien aus .tx/config
python tools/tx_push.py howto # nach Pfad filtern
python tools/tx_push.py library/csv.po
python tools/tx_push.py --dry-run howto # nur zeigen, nicht pushen
"""
import argparse
import configparser
import re
import subprocess
import sys
from pathlib import Path
try:
import polib
except ImportError:
polib = None # Fortschrittsanzeige ist dann eben leer
def git(*args: str) -> str:
e = subprocess.run(["git", *args], capture_output=True, text=True)
return e.stdout if e.returncode == 0 else ""
def repo_wurzel() -> Path:
aus = git("rev-parse", "--show-toplevel").strip()
if not aus:
sys.exit("Kein Git-Repository -- bitte im python-docs-de-Verzeichnis starten.")
return Path(aus)
def ressourcen(wurzel: Path) -> dict:
"""{po-Pfad: (projekt, resource)} aus .tx/config."""
pfad = wurzel / ".tx" / "config"
if not pfad.is_file():
sys.exit(f"{pfad} nicht gefunden.")
cfg = configparser.ConfigParser()
cfg.read(pfad, encoding="utf-8")
aus = {}
for name in cfg.sections():
# [o:python-doc:p:python-314:r:howto--index]
m = re.match(r"o:([^:]+):p:([^:]+):r:(.+)", name)
if not m:
continue
_org, projekt, resource = m.groups()
abschnitt = cfg[name]
datei = abschnitt.get("trans.de") or abschnitt.get("file_filter", "")
datei = datei.replace("<lang>", "de").strip()
if datei:
aus[datei] = (projekt, resource)
if not aus:
sys.exit("In .tx/config wurden keine Ressourcen gefunden.")
return aus
def geaenderte_dateien(wurzel: Path) -> set:
""".po-Dateien, die sich gegenueber origin/<branch> oder im Baum geaendert haben."""
branch = git("branch", "--show-current").strip()
kandidaten = set()
for befehl in (
["diff", "--name-only"], # unversionierte Aenderungen
["diff", "--name-only", "--cached"], # gestaged
["diff", "--name-only", f"origin/{branch}...HEAD"], # noch nicht gepusht
):
for zeile in git(*befehl).splitlines():
if zeile.endswith(".po"):
kandidaten.add(zeile)
return kandidaten
def stand(wurzel: Path, datei: str) -> str:
"""'123/456 (27 %)' -- oder leer, wenn polib fehlt."""
if polib is None:
return ""
p = wurzel / datei
if not p.is_file():
return "fehlt lokal"
try:
po = polib.pofile(str(p))
except Exception:
return "nicht lesbar"
gesamt = len([e for e in po if not e.obsolete])
fertig = len([e for e in po if not e.obsolete and e.translated()])
prozent = 100 * fertig // gesamt if gesamt else 0
return f"{fertig}/{gesamt} ({prozent} %)"
def auswahl_lesen(anzahl: int) -> list:
"""'1,3-5' oder 'a' einlesen und in Indizes uebersetzen."""
while True:
roh = input("\nNummern (z. B. 1,3-5), a = alle, q = Abbruch: ").strip().lower()
if roh in ("q", ""):
sys.exit("Abgebrochen.")
if roh == "a":
return list(range(anzahl))
indizes = set()
try:
for teil in roh.replace(" ", "").split(","):
if "-" in teil:
von, bis = teil.split("-", 1)
indizes.update(range(int(von) - 1, int(bis)))
else:
indizes.add(int(teil) - 1)
except ValueError:
print("Das habe ich nicht verstanden.")
continue
if any(i < 0 or i >= anzahl for i in indizes):
print(f"Nur 1 bis {anzahl} sind gueltig.")
continue
return sorted(indizes)
def main() -> None:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("muster", nargs="*", help="Pfadteile zum Filtern, z. B. howto")
p.add_argument("--alle", action="store_true",
help="alle Dateien aus .tx/config zur Auswahl stellen")
p.add_argument("--dry-run", action="store_true",
help="Befehl nur anzeigen, nicht ausfuehren")
args = p.parse_args()
wurzel = repo_wurzel()
branch = git("branch", "--show-current").strip()
karte = ressourcen(wurzel)
projekt = next(iter(karte.values()))[0]
dateien = sorted(karte)
herkunft = "alle Ressourcen aus .tx/config"
if args.muster:
dateien = [d for d in dateien if any(m in d for m in args.muster)]
herkunft = "Treffer fuer " + ", ".join(args.muster)
elif not args.alle:
geaendert = geaenderte_dateien(wurzel) & set(karte)
if geaendert:
dateien = sorted(geaendert)
herkunft = "geaenderte Dateien"
else:
print("Keine geaenderten .po-Dateien gefunden -- zeige alle.\n")
if not dateien:
sys.exit("Keine passende Datei gefunden.")
print(f"Branch {branch} -> Transifex-Projekt {projekt} ({herkunft})\n")
breite = max(len(d) for d in dateien)
for i, d in enumerate(dateien, 1):
print(f" {i:>3} {d:<{breite}} {karte[d][1]:<28} {stand(wurzel, d)}")
gewaehlt = [dateien[i] for i in auswahl_lesen(len(dateien))]
ziele = [f"{karte[d][0]}.{karte[d][1]}" for d in gewaehlt]
befehl = ["tx", "push", "-t", "-l", "de", "-f", "--skip",
"--replace-edited-strings", *ziele]
print("\nBefehl:\n " + " ".join(befehl) + "\n")
for d in gewaehlt:
print(f" {d} -> {karte[d][1]}")
if args.dry_run:
print("\n--dry-run: nichts gepusht.")
return
if input("\nJetzt pushen? [j/N] ").strip().lower() not in ("j", "ja"):
sys.exit("Abgebrochen.")
print()
sys.exit(subprocess.run(befehl, cwd=wurzel).returncode)
if __name__ == "__main__":
main()