Selbst-Update: lose/geänderte Dateien werden automatisch beiseitegelegt (t_f73a1cc5)
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.
This commit is contained in:
parent
3e07e91f5a
commit
90c3c7fdc7
3 changed files with 219 additions and 4 deletions
|
|
@ -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)]
|
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
|
## Update ausführen: git pull --ff-only (Klon zieht nur nach, dreht nichts
|
||||||
## um). Rückgabe {ok, output, errors}. Auth-Fehler bekommen einen klaren
|
## um). Vorher werden lokal Erzeugtes/Geändertes AUTOMATISCH beiseitegelegt
|
||||||
## Zusatz-Hinweis (kein Login-Prompt — Karte t_1457e935).
|
## (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:
|
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 r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"]))
|
||||||
var errors: Array = []
|
var errors: Array = []
|
||||||
var text := str(r["output"]).strip_edges()
|
var text := str(r["output"]).strip_edges()
|
||||||
|
var info := ""
|
||||||
if not bool(r["ok"]):
|
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"]))
|
errors.append("git pull --ff-only fehlgeschlagen (Exit-Code %s):" % str(r["code"]))
|
||||||
if not text.is_empty():
|
if not text.is_empty():
|
||||||
errors.append(text)
|
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)")
|
errors.append("(keine Ausgabe — git nicht gefunden oder Verbindung sofort weg)")
|
||||||
if looks_like_auth_error(text):
|
if looks_like_auth_error(text):
|
||||||
errors.append("Zugangsdaten fehlen — es wird bewusst KEIN Login-Dialog geöffnet (Credential-Store prüfen).")
|
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: <godot> --headless --path <projekt> --import. Muss NACH dem
|
## Import-Lauf: <godot> --headless --path <projekt> --import. Muss NACH dem
|
||||||
|
|
|
||||||
|
|
@ -1202,7 +1202,13 @@ func _updater_run_thread() -> void:
|
||||||
if not bool(pull["ok"]):
|
if not bool(pull["ok"]):
|
||||||
call_deferred("_update_finished", false, "Update abgebrochen — git pull fehlgeschlagen:", pull["errors"])
|
call_deferred("_update_finished", false, "Update abgebrochen — git pull fehlgeschlagen:", pull["errors"])
|
||||||
return
|
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)
|
var godot := TestRun.find_godot(cfg)
|
||||||
if godot.is_empty():
|
if godot.is_empty():
|
||||||
call_deferred("_update_finished", false, "Update abgebrochen — kein Godot-Programm für den Import-Lauf:", [
|
call_deferred("_update_finished", false, "Update abgebrochen — kein Godot-Programm für den Import-Lauf:", [
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,39 @@ func _write_file(path: String, text: String) -> void:
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
func _file_text(path: String) -> String:
|
||||||
|
var f := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if f == null:
|
||||||
|
return "<fehlt>"
|
||||||
|
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:
|
func _run_tests() -> void:
|
||||||
var git := "git"
|
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")
|
_check((dead_pull["errors"] as Array).size() > 0, "Pull-Fehlerliste fürs Dialog-Fenster")
|
||||||
_git(work, ["remote", "set-url", "origin", origin])
|
_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 ==")
|
print("== Import-Abbruch-Pfad ==")
|
||||||
_check(EditorUpdater.import_errors([]).is_empty(), "leere Import-Ausgabe = keine Fehler")
|
_check(EditorUpdater.import_errors([]).is_empty(), "leere Import-Ausgabe = keine Fehler")
|
||||||
_check(EditorUpdater.import_errors(["Eine normale Zeile"]).is_empty(), "normale Ausgabe = keine Fehler")
|
_check(EditorUpdater.import_errors(["Eine normale Zeile"]).is_empty(), "normale Ausgabe = keine Fehler")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue