NTFS-Dirty-Bit-Reparatur, Release-Skript und Projekthygiene

- GUI: Dirty-Bit/Fast-Startup beim Mounten erkennen, Rueckfrage mit
  ntfsfix + erzwungenem Schreib-Mount, Button "NTFS reparieren"
- release.py: Version hochzaehlen, CHANGELOG pflegen, commit+tag,
  ISO bauen und als Gitea-Release (tea) mit ISO-Anhang veroeffentlichen
- out/ bleibt im Repo (out/.gitignore), ISOs ignoriert
- .gitignore ergaenzt, versehentlich getrackte __pycache__ entfernt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-10-22 12:38:20 +02:00
co-authored by Claude Opus 4.8
parent fd3fe19539
commit cbd7e74b1c
10 changed files with 213 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
# live-build Arbeitsverzeichnis (falls lokal gebaut)
/live/
*.iso
+4
View File
@@ -17,3 +17,7 @@ Versionierung nach [SemVer](https://semver.org/lang/de/).
- NTFS-Dirty-Bit-Erkennung beim Mounten mit Rückfrage und automatischem
`ntfsfix` + erzwungenem Schreib-Mount; zusätzlich eigener Button
„NTFS reparieren (Dirty-Bit)".
- `release.py`: Version automatisch hochzählen (Patch/Minor/Major), CHANGELOG
pflegen, committen + taggen, ISO bauen und als Gitea-Release (via `tea`)
mit ISO-Anhang veröffentlichen.
- `out/` bleibt im Repo (per `out/.gitignore`), die gebauten ISOs werden ignoriert.
+17
View File
@@ -53,6 +53,23 @@ sudo dd if=out/pc-unlocker.iso of=/dev/sdX bs=4M status=progress conv=fsync
> Der Container läuft mit `--privileged`, weil `live-build` Loop-Devices und
> Mounts benötigt.
## Release veröffentlichen
`release.py` zählt die Version hoch, pflegt das CHANGELOG, committet + taggt,
baut die ISO und veröffentlicht sie als **Gitea-Release** mit der ISO als Anhang.
```bash
./release.py # Patch: 0.1.0 -> 0.1.1
./release.py --minor # 0.1.0 -> 0.2.0
./release.py --major # 0.1.0 -> 1.0.0
./release.py --no-build # vorhandene out/pc-unlocker.iso anhängen
./release.py --no-publish # nur lokal (bump, commit, tag, build)
./release.py --dry-run # nur anzeigen
```
Veröffentlichen pusht Release-Commit und Tag nach `origin` und ruft dann
`tea release create`. Einmalig nötig: `tea login add` für die Gitea-Instanz.
## App direkt testen (ohne ISO)
Auf einem Linux-Host mit `python3-tk`, `chntpw`, `ntfs-3g`:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
Executable
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Release-Automatik fuer PC-Unlocker.
Zaehlt die Version hoch, pflegt das CHANGELOG, committet + taggt, baut die ISO
und veroeffentlicht sie als Gitea-Release (via `tea`) mit der ISO als Anhang.
Beispiele:
./release.py # Patch-Release (0.1.0 -> 0.1.1), bauen + publizieren
./release.py --minor # 0.1.0 -> 0.2.0
./release.py --major # 0.1.0 -> 1.0.0
./release.py --no-build # vorhandene out/pc-unlocker.iso verwenden
./release.py --no-publish # nur lokal: bump, commit, tag, (build)
./release.py --dry-run # nur anzeigen, nichts aendern
Veroeffentlichen pusht den Release-Commit und das Tag nach origin (zwingend,
damit Gitea das Release anlegen kann) und ruft dann `tea release create`.
Einmalig noetig: `tea login add` fuer git.hacker-net.de.
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
INIT = ROOT / "app" / "puk" / "__init__.py"
CHANGELOG = ROOT / "CHANGELOG.md"
ISO = ROOT / "out" / "pc-unlocker.iso"
VERSION_RE = re.compile(r'^__version__\s*=\s*"(\d+)\.(\d+)\.(\d+)"', re.M)
def run(cmd: list[str], *, check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
print(f" $ {' '.join(cmd)}")
return subprocess.run(cmd, check=check, text=True,
capture_output=capture, cwd=ROOT)
def fail(msg: str) -> None:
print(f"\nFEHLER: {msg}", file=sys.stderr)
sys.exit(1)
def read_version() -> tuple[int, int, int]:
m = VERSION_RE.search(INIT.read_text(encoding="utf-8"))
if not m:
fail(f"__version__ nicht in {INIT} gefunden")
return tuple(int(x) for x in m.groups()) # type: ignore[return-value]
def bump(ver: tuple[int, int, int], part: str) -> tuple[int, int, int]:
major, minor, patch = ver
if part == "major":
return major + 1, 0, 0
if part == "minor":
return major, minor + 1, 0
return major, minor, patch + 1
def write_version(ver: tuple[int, int, int]) -> None:
new = f'__version__ = "{ver[0]}.{ver[1]}.{ver[2]}"'
text = VERSION_RE.sub(new, INIT.read_text(encoding="utf-8"), count=1)
INIT.write_text(text, encoding="utf-8")
def update_changelog(ver_str: str) -> str:
"""[Unreleased]-Inhalt in einen datierten Versionsabschnitt ueberfuehren.
Gibt die Notizen (den verschobenen Inhalt) fuer die Release-Beschreibung zurueck.
"""
text = CHANGELOG.read_text(encoding="utf-8")
marker = "## [Unreleased]"
idx = text.find(marker)
if idx == -1:
fail("'## [Unreleased]' nicht im CHANGELOG gefunden")
after = idx + len(marker)
# Inhalt bis zur naechsten Versionsueberschrift (## [..]) oder Dateiende
nxt = text.find("\n## [", after)
end = nxt if nxt != -1 else len(text)
body = text[after:end].strip("\n")
notes = body if body else "Wartungs-Release."
today = dt.date.today().isoformat()
new_block = (
f"{marker}\n\n"
f"## [{ver_str}] - {today}\n\n"
f"{body}\n"
)
new_text = text[:idx] + new_block + text[end:]
CHANGELOG.write_text(new_text, encoding="utf-8")
return notes
def git_clean_or_die() -> None:
res = run(["git", "status", "--porcelain"], capture=True)
dirty = [l for l in res.stdout.splitlines()
if l and not l.endswith(("CHANGELOG.md", "__init__.py"))]
if dirty:
fail("Arbeitsverzeichnis nicht sauber. Bitte erst committen/stashen:\n"
+ "\n".join(dirty))
def main() -> int:
ap = argparse.ArgumentParser(description="PC-Unlocker Release")
g = ap.add_mutually_exclusive_group()
g.add_argument("--major", action="store_const", const="major", dest="part")
g.add_argument("--minor", action="store_const", const="minor", dest="part")
g.add_argument("--patch", action="store_const", const="patch", dest="part")
ap.add_argument("--no-build", action="store_true", help="ISO nicht neu bauen")
ap.add_argument("--no-publish", action="store_true",
help="nur lokal: kein push, kein Gitea-Release")
ap.add_argument("--dry-run", action="store_true", help="nur anzeigen")
args = ap.parse_args()
part = args.part or "patch"
old = read_version()
new = bump(old, part)
old_s = ".".join(map(str, old))
new_s = ".".join(map(str, new))
tag = f"v{new_s}"
print(f"Release: {old_s} -> {new_s} (tag {tag})")
if args.dry_run:
print("--dry-run: es wird nichts geaendert.")
return 0
if not args.no_publish:
git_clean_or_die()
if shutil.which("tea") is None:
fail("`tea` (Gitea CLI) nicht gefunden. Mit --no-publish nur lokal "
"arbeiten oder tea installieren.")
# 1) Version + CHANGELOG
print("\n==> Version und CHANGELOG aktualisieren")
write_version(new)
notes = update_changelog(new_s)
# 2) ISO bauen
if not args.no_build:
print("\n==> ISO bauen")
run(["bash", str(ROOT / "build.sh")])
if not ISO.exists():
fail(f"ISO fehlt: {ISO} (ohne --no-build bauen lassen)")
# 3) Commit + Tag
print("\n==> Commit und Tag")
run(["git", "add", str(INIT), str(CHANGELOG)])
run(["git", "commit", "-m", f"release: {tag}"])
run(["git", "tag", "-a", tag, "-m", f"PC-Unlocker {tag}"])
if args.no_publish:
print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n"
f" git push origin HEAD && git push origin {tag}\n"
f" tea release create --tag {tag} --title {tag} "
f"--asset {ISO}")
return 0
# 4) Push (noetig, damit Gitea das Release kennt)
print("\n==> Push nach origin")
run(["git", "push", "origin", "HEAD"])
run(["git", "push", "origin", tag])
# 5) Gitea-Release mit ISO-Anhang
print("\n==> Gitea-Release anlegen")
run(["tea", "release", "create",
"--tag", tag,
"--title", tag,
"--note", notes,
"--asset", str(ISO)])
print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.")
return 0
if __name__ == "__main__":
raise SystemExit(main())