From 90c3c7fdc73d2f1eb6a12e11b77577c361892584 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Mon, 19 Oct 2026 18:41:19 +0000 Subject: [PATCH] =?UTF-8?q?Selbst-Update:=20lose/ge=C3=A4nderte=20Dateien?= =?UTF-8?q?=20werden=20automatisch=20beiseitegelegt=20(t=5Ff73a1cc5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EditorUpdater.pull: vor dem pull --ff-only git stash push -u (erfasst GEÄNDERT + UNTRACKED — genau die losen .import-Dateien, die am 19.10. den Update-Knopf blockierten). Nach dem Pull zurückholen (apply+drop); kollidiert das Zurückholen: reset --hard gegen halb gepoppte Zustände, Stash bleibt als Sicherung, ehrliche Klartext-Meldung in der Statuszeile statt roher Git-Zeile. Alte Auto-Stashes gleicher Kennung werden beim Anlegen verworfen (keine Anhäufung). main.gd zeigt die Klartext-Info. tests/test_updater.gd: Negativprobe (nackter pull bricht an untracked vs. getrackt ab), Fix-Probe (läuft durch), geänderte Datei bleibt erhalten, Konflikt-Fall mit Sicherungs-Stash, Anti-Anhäufung. 69 checks. --- src/editor/updater.gd | 104 +++++++++++++++++++++++++++++++++++++-- src/ui/main.gd | 8 ++- tests/test_updater.gd | 111 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/editor/updater.gd b/src/editor/updater.gd index 322e5bf..6d6b126 100644 --- a/src/editor/updater.gd +++ b/src/editor/updater.gd @@ -168,14 +168,40 @@ static func banner_text(local_sha: String, remote_sha: String) -> String: return "🔄 Update verfügbar: %s → %s" % [short_sha(local_sha), short_sha(remote_sha)] +## Klartext-Status für die Statuszeile (Karte t_f73a1cc5): Wenn lokale +## (meist von Godot erzeugte) Dateien das Update blockieren WÜRDEN, legt der +## Pull sie selbst beiseite — Sly liest diesen Satz statt einer rohen Git- +## Zeile („Bitte verschieben oder entfernen Sie diese…“). +const AUTO_STASH_INFO_TEXT := "Update blockiert: eigene erzeugte Dateien — werden automatisch beiseitegelegt und das Update läuft weiter." +## Ehrliche Meldung, wenn das Zurückholen kollidiert ist (Stash bleibt als +## Sicherung liegen; Godot baut die erzeugten Dateien beim Import neu). +const STASH_KEPT_TEXT := "Erzeugte Dateien bleiben beiseitegelegt — beim nächsten Start neu aufgebaut (Sicherungs-Stash bleibt liegen, nichts verloren)." +## Kennung der eigenen Auto-Stashes (Verwerfen alter Einträge, damit sie +## sich nicht anhäufen). +const AUTO_STASH_MESSAGE := "auto: Selbst-Update" + + ## Update ausführen: git pull --ff-only (Klon zieht nur nach, dreht nichts -## um). Rückgabe {ok, output, errors}. Auth-Fehler bekommen einen klaren -## Zusatz-Hinweis (kein Login-Prompt — Karte t_1457e935). +## um). Vorher werden lokal Erzeugtes/Geändertes AUTOMATISCH beiseitegelegt +## (Karte t_f73a1cc5): `git stash push -u` erfasst GEÄNDERT **und** UNTRACKED +## Dateien (`--autostash` allein reicht nicht — es lässt untracked liegen; +## genau die haben am 19.10. den Pull blockiert). Nach dem Pull wird das +## Beiseitegelegte zurückgeholt; kollidiert das (Typfall: lose `.import`- +## Datei trifft auf dieselbe Datei neu getrackt), bleibt der Stash als +## Sicherung liegen und die Statuszeile bekommt den Klartext — NIE ein halb +## gepoppter Zustand. Rückgabe {ok, output, errors, info} (info = Klartext +## fürs Statuszeile-Fenster, leer wenn nichts besonderes passiert ist). static func pull(git_exe: String, project_dir: String) -> Dictionary: + var stash_info := _stash_local_changes(git_exe, project_dir) var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"])) var errors: Array = [] var text := str(r["output"]).strip_edges() + var info := "" if not bool(r["ok"]): + # Pull fehlgeschlagen (z. B. offline): Beiseitegelegtes zurückholen, + # damit nichts „verschwindet", wenn der Fehler nur am Netz lag. + if not stash_info.is_empty(): + _restore_stash(git_exe, project_dir) errors.append("git pull --ff-only fehlgeschlagen (Exit-Code %s):" % str(r["code"])) if not text.is_empty(): errors.append(text) @@ -183,7 +209,79 @@ static func pull(git_exe: String, project_dir: String) -> Dictionary: errors.append("(keine Ausgabe — git nicht gefunden oder Verbindung sofort weg)") if looks_like_auth_error(text): errors.append("Zugangsdaten fehlen — es wird bewusst KEIN Login-Dialog geöffnet (Credential-Store prüfen).") - return {"ok": bool(r["ok"]), "output": str(r["output"]), "errors": errors} + else: + if not stash_info.is_empty(): + info = AUTO_STASH_INFO_TEXT + var restored := _restore_stash(git_exe, project_dir) + if not bool(restored["ok"]): + info += "\n" + STASH_KEPT_TEXT + return {"ok": bool(r["ok"]), "output": str(r["output"]), "errors": errors, "info": info} + + +## Lokales Beiseitelegen vor dem Pull (Karte t_f73a1cc5). Rückgabe: Referenz +## auf den eigenen Auto-Stash (z. B. "stash@{0}") oder "" wenn es nichts zu +## beiseitelegen gab / der Stash nicht angelegt werden konnte. +## - Zuerst alte EIGENE Auto-Stashes verwerfen (gleiche Kennung) — sie +## enthalten nur erzeugte Dateien und dürfen sich nicht anhäufen. +## - "No local changes to save" endet ebenfalls mit Exit 0 → NICHT am Text +## matchen, sondern prüfen, ob ein Eintrag mit unserer Kennung entstand. +static func _stash_local_changes(git_exe: String, project_dir: String) -> String: + _drop_auto_stashes(git_exe, project_dir) + var r := git_run(git_exe, project_dir, PackedStringArray([ + "stash", "push", "-u", "-m", AUTO_STASH_MESSAGE])) + if not bool(r["ok"]): + return "" + return _auto_stash_ref(git_exe, project_dir) + + +## Referenz des eigenen Auto-Stashes ("stash@{N}") oder "". +static func _auto_stash_ref(git_exe: String, project_dir: String) -> String: + var r := git_run(git_exe, project_dir, PackedStringArray([ + "stash", "list", "--pretty=format:%gd|%s"])) + if not bool(r["ok"]): + return "" + for line in str(r["output"]).split("\n"): + var parts := str(line).strip_edges().split("|", true, 1) + if parts.size() == 2 and str(parts[1]).contains(AUTO_STASH_MESSAGE): + return str(parts[0]) + return "" + + +## Alle EIGENEN Auto-Stashes (Kennung AUTO_STASH_MESSAGE) verwerfen — von +## hinten, weil die Indizes sich beim Drop verschieben. +static func _drop_auto_stashes(git_exe: String, project_dir: String) -> void: + var r := git_run(git_exe, project_dir, PackedStringArray([ + "stash", "list", "--pretty=format:%gd|%s"])) + if not bool(r["ok"]): + return + var refs: Array = [] + for line in str(r["output"]).split("\n"): + var parts := str(line).strip_edges().split("|", true, 1) + if parts.size() == 2 and str(parts[1]).contains(AUTO_STASH_MESSAGE): + refs.append(str(parts[0])) + refs.reverse() + for ref in refs: + git_run(git_exe, project_dir, PackedStringArray(["stash", "drop", ref])) + + +## Beiseitegelegtes zurückholen. apply (nicht pop), damit die Referenz nicht +## am Stack verschiebt; bei Erfolg wird der Eintrag gedroppt. Kollidiert das +## Zurückholen (Typfall: erzeugte Datei ist inzwischen getrackt), wird mit +## `git reset --hard` der halb gepoppte Zustand weggeräumt und der Stash ALS +## SICHERUNG LIEGEN GELASSEN — kein Datenverlust, Godot erzeugt die Dateien +## beim nächsten Import-Lauf neu. Bewusst KEIN `git clean`: untracked +## Nutzdaten (z. B. Maps/) dürfen nie angefasst werden. +## Rückgabe {ok, kept, output}. +static func _restore_stash(git_exe: String, project_dir: String) -> Dictionary: + var ref := _auto_stash_ref(git_exe, project_dir) + if ref.is_empty(): + return {"ok": true, "kept": false, "output": ""} + var r := git_run(git_exe, project_dir, PackedStringArray(["stash", "apply", ref])) + if bool(r["ok"]): + git_run(git_exe, project_dir, PackedStringArray(["stash", "drop", ref])) + return {"ok": true, "kept": false, "output": str(r["output"])} + git_run(git_exe, project_dir, PackedStringArray(["reset", "--hard"])) + return {"ok": false, "kept": true, "output": str(r["output"])} ## Import-Lauf: --headless --path --import. Muss NACH dem diff --git a/src/ui/main.gd b/src/ui/main.gd index 8eb63c8..a3dd234 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -1202,7 +1202,13 @@ func _updater_run_thread() -> void: if not bool(pull["ok"]): call_deferred("_update_finished", false, "Update abgebrochen — git pull fehlgeschlagen:", pull["errors"]) return - call_deferred("_set_status", "Update geladen — Version wird importiert (Godot-Import-Lauf)…") + # Karte t_f73a1cc5: Wenn der Pull eigene Dateien automatisch beiseite- + # gelegt hat, zeigt die Statuszeile den Klartext statt der technischen + # Import-Meldung (KEINE rohe Git-Zeile). + var info := str(pull.get("info", "")) + if info.is_empty(): + info = "Update geladen — Version wird importiert (Godot-Import-Lauf)…" + call_deferred("_set_status", info) var godot := TestRun.find_godot(cfg) if godot.is_empty(): call_deferred("_update_finished", false, "Update abgebrochen — kein Godot-Programm für den Import-Lauf:", [ diff --git a/tests/test_updater.gd b/tests/test_updater.gd index 6e598f1..bac23a3 100644 --- a/tests/test_updater.gd +++ b/tests/test_updater.gd @@ -82,6 +82,39 @@ func _write_file(path: String, text: String) -> void: f.close() +func _file_text(path: String) -> String: + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return "" + var text := f.get_as_text() + f.close() + return text + + +## Nur getrackte Verschmutzungen: false, wenn eine NICHT-untracked Zeile in +## git status --porcelain auftaucht (halb gepoppter Konflikt hinterlässt +## genau solche Zeilen). +func _clean_tracked(dir: String) -> bool: + var r := _git(dir, ["status", "--porcelain"]) + for line in str(r["output"]).split("\n"): + var text := str(line).strip_edges() + if not text.is_empty() and not text.begins_with("??"): + return false + return true + + +## Anzahl der EIGENEN Auto-Stashes (Kennung "auto: Selbst-Update"). +func _auto_stash_count(git: String, dir: String) -> int: + var r := EditorUpdater.git_run(git, dir, PackedStringArray([ + "stash", "list", "--pretty=format:%gd|%s"])) + var count := 0 + for line in str(r["output"]).split("\n"): + var parts := str(line).strip_edges().split("|", true, 1) + if parts.size() == 2 and str(parts[1]).contains("auto: Selbst-Update"): + count += 1 + return count + + func _run_tests() -> void: var git := "git" @@ -182,6 +215,84 @@ func _run_tests() -> void: _check((dead_pull["errors"] as Array).size() > 0, "Pull-Fehlerliste fürs Dialog-Fenster") _git(work, ["remote", "set-url", "origin", origin]) + print("== Selbst-Update: lose Dateien blockieren den Pull nicht (Karte t_f73a1cc5) ==") + # Origin bekommt eine GETRACKTE Import-Datei — lokal existiert sie als + # untracked (Godot erzeugt sie beim Import). Genau die Kollision, die am + # 19.10. den Update-Knopf blockiert hat. + DirAccess.make_dir_recursive_absolute(other.path_join("library/thumbs")) + _write_file(other.path_join("library/thumbs/zz_probe.png.import"), "origin-version\n") + _git(other, ["add", "library/thumbs/zz_probe.png.import"]) + _git(other, ["commit", "-m", "v3: Import-Datei getrackt"]) + _check(bool(_git(other, ["push", "origin", "main"])["ok"]), + "Setup: v3 gepusht (Import-Datei jetzt getrackt)") + + # NEGATIVPROBE: der NACKTE Pull (ohne Fix) bricht an der losen Datei ab. + DirAccess.make_dir_recursive_absolute(work.path_join("library/thumbs")) + _write_file(work.path_join("library/thumbs/zz_probe.png.import"), "lokal-erzeugt\n") + var raw := EditorUpdater.git_run(git, work, PackedStringArray(["pull", "--ff-only"])) + _check(not bool(raw["ok"]), "NEGATIVPROBE: nackter pull --ff-only bricht an der Kollision ab") + _check(not str(raw["output"]).strip_edges().is_empty(), + "NEGATIVPROBE: git liefert die rohe (verwirrende) Fehlerzeile") + + # MIT Fix: EditorUpdater.pull legt selbst beiseite und zieht durch. + var fixed := EditorUpdater.pull(git, work) + _check(bool(fixed["ok"]), "MIT Fix: pull läuft ohne Handeingriff durch") + _check(str(fixed["info"]).contains("beiseitegelegt"), + "Klartext-Info fürs Statuszeilen-Fenster gesetzt") + _check(_file_text(work.path_join("library/thumbs/zz_probe.png.import")) == "origin-version\n", + "kein halb gepoppter Zustand: getrackte Version liegt an") + _check(_clean_tracked(work), "Arbeitsbaum (getrackt) sauber — Godot baut die Datei beim Import neu") + _check(_auto_stash_count(git, work) == 1, "Sicherungs-Stash bleibt bei Konflikt liegen") + + print("== Selbst-Update: geänderte getrackte Datei bleibt erhalten ==") + # v4 fasst a.txt NICHT an → das Zurückholen der lokalen b.txt-Änderung + # MUSS klappen: apply + drop, kein Stash-Rest. + _write_file(work.path_join("b.txt"), "lokal geändert\n") + _write_file(other.path_join("c.txt"), "neu\n") + _git(other, ["add", "c.txt"]) + _git(other, ["commit", "-m", "v4: nur c.txt"]) + _check(bool(_git(other, ["push", "origin", "main"])["ok"]), "Setup: v4 gepusht") + var fixed2 := EditorUpdater.pull(git, work) + _check(bool(fixed2["ok"]), "pull mit lokaler Änderung läuft durch") + _check(_file_text(work.path_join("b.txt")) == "lokal geändert\n", + "lokal geänderte Datei ist zurückgeholt (kein Datenverlust)") + _check(_auto_stash_count(git, work) == 0, + "Auto-Stash nach erfolgreichem Rückholen aufgeräumt (keine Anhäufung)") + + print("== Selbst-Update: Kollision mit Origin → Stash bleibt als Sicherung ==") + # v5 ändert dieselbe Datei wie lokal → Zurückholen kollidiert; der Stash + # bleibt als Sicherung liegen, der Arbeitsbaum ist sauber auf v5. + _write_file(work.path_join("a.txt"), "lokal-geändert\n") + _write_file(other.path_join("a.txt"), "origin-geändert\n") + _git(other, ["add", "a.txt"]) + _git(other, ["commit", "-m", "v5: a.txt geändert"]) + _check(bool(_git(other, ["push", "origin", "main"])["ok"]), "Setup: v5 gepusht") + var fixed3 := EditorUpdater.pull(git, work) + _check(bool(fixed3["ok"]), "Update läuft trotzdem durch (Änderung liegt sicher im Stash)") + _check(_file_text(work.path_join("a.txt")) == "origin-geändert\n", + "Arbeitsbaum = Origin-Stand (kein Konflikt-Brei, keine halbe Datei)") + _check(str(fixed3["info"]).contains("neu aufgebaut"), + "ehrliche Meldung: beiseitegelegt, beim nächsten Start neu aufgebaut") + var show := EditorUpdater.git_run(git, work, PackedStringArray([ + "stash", "show", "-p", "stash@{0}"])) + _check(str(show["output"]).contains("lokal-geändert"), + "geänderte Datei liegt unverlustfrei im Sicherungs-Stash") + + print("== Auto-Stash: alte Kennung wird verworfen, nichts häuft sich an ==") + _write_file(work.path_join("library/thumbs/zwei.png.import"), "alt\n") + var s1 := EditorUpdater._stash_local_changes(git, work) + _write_file(work.path_join("library/thumbs/drei.png.import"), "neu\n") + var s2 := EditorUpdater._stash_local_changes(git, work) + _check(not s1.is_empty() and not s2.is_empty(), "beide Läufe legen einen Auto-Stash an") + _check(_auto_stash_count(git, work) == 1, "genau EIN Auto-Stash übrig (alter verworfen)") + # Für die Folge-Abschnitte aufräumen: Stash zurückholen (klappert, weil + # die zwei untracked Dateien an keine getrackte Datei grenzen) und die + # Probendateien wegwerfen. + _check(bool(EditorUpdater._restore_stash(git, work)["ok"]), "Aufräumen: Stash zurückgeholt") + DirAccess.remove_absolute(work.path_join("library/thumbs/zwei.png.import")) + DirAccess.remove_absolute(work.path_join("library/thumbs/drei.png.import")) + _check(_auto_stash_count(git, work) == 0, "Aufräumen: kein Auto-Stash übrig") + print("== Import-Abbruch-Pfad ==") _check(EditorUpdater.import_errors([]).is_empty(), "leere Import-Ausgabe = keine Fehler") _check(EditorUpdater.import_errors(["Eine normale Zeile"]).is_empty(), "normale Ausgabe = keine Fehler")