extends SceneTree ## Editor-Tests (headless): Selbst-Update — Karte t_d95f087a. ## Ausführen: godot --headless --path . --script res://tests/test_updater.gd ## Exit 0 = alles grün. Läuft OHNE UI (EditorUpdater + echte git-Repos in ## user://test_updater): ## 1. Check-Funktion mit ECHTEN Repos: gleich (up_to_date), divergiert ## (update_available), nicht erreichbar (unreachable) — kein Mock, echte ## git-Prozesse gegen ein Bare-Repo / gegen einen abgelehnten Port. ## 2. Banner-Logik (Text, kurze SHAs). ## 3. Import-Abbruch-Pfad: faked Import-Fehler (Exit-Code != 0 UND Parse- ## -Error-Schreiber mit Exit 0) → kein ok, kein Restart (Restart wird nur ## NACH ok angestoßen — hier wird bewusst NUR der ok-Pfad geprüft). ## 4. Restart-Auflösung: cfg-Kommando gewinnt, Default ist self/command. var failures := 0 var checks := 0 const TEST_DIR_PREFIX := "user://test_updater" var base := "" var work := "" func _init() -> void: _cleanup_old() # Karte t_1e82f747: Time.get_ticks_msec() ist nur die Zeit seit Godot-Start # (kleine Zahl, z.B. 115) — bei jedem CLI-Run derselbe Wert! Kollidierte # Namen mit nicht löschbaren Alt-Läufen → git init lief in einer ALTEN # Sitzung (remote existiert schon, alte Dateien im Baum → rote Setup-Zeilen). # Deswegen: Unix-Zeit + ticks_usec = garantiert einzigartig pro Prozess/Run. base = ProjectSettings.globalize_path(TEST_DIR_PREFIX + "_" + str(int(Time.get_unix_time_from_system())) + "_" + str(Time.get_ticks_usec())) DirAccess.make_dir_recursive_absolute(base) _run_tests() _test_ls_remote_warnzeile() _test_submodul_kommt_mit() _force_writable(base) _cleanup_dir(base) # best effort — git-Pack-Dateien können read-only sein if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) func _check(cond: bool, label: String) -> void: checks += 1 if cond: print(" ok: %s" % label) else: failures += 1 print(" FAIL: %s" % label) ## Best effort: alte Läufe aufräumen (git macht Pack-Dateien read-only — ## dann bleibt der Ordner liegen; der LAUF nutzt deshalb IMMER ein frisches, ## Zeit-stempel-basiertes Verzeichnis und hängt nie an Überbleibseln). func _cleanup_old() -> void: var root := DirAccess.open(ProjectSettings.globalize_path("user://")) if root == null: return for name in root.get_directories(): if name.begins_with("test_updater"): var alt := ProjectSettings.globalize_path("user://").path_join(name) _force_writable(alt) _cleanup_dir(alt) ## Karte t_1e82f747: git macht .git-Packs read-only, dann schlägt ## DirAccess.remove_absolute fehlt → der Ordner bleibt liegen und KOLLIDIERT ## mit künftigen Läufen (derselbe ticks_msec-Name!). chmod -R u+w macht die ## Bäume löschbar. Linux/OSX-Fallback per /usr/bin/chmod (Tests sind CLI, ## Windows-spezifische ACLs sind hier nicht der Fall). func _force_writable(path: String) -> void: if not DirAccess.dir_exists_absolute(path): return var chmod := "/usr/bin/chmod" if not FileAccess.file_exists(chmod): chmod = "chmod" # WICHTIG: das 2-Argument-OS.execute ist in Godot 4 ASYNCHRON (gibt nur # eine PID zurück) — ohne output-Array wartet der Lauf nicht, und die # Löschung passiert vor dem chmod. Mit output-Array blockiert er. var out: PackedStringArray = [] OS.execute(chmod, PackedStringArray(["-R", "u+w", path]), out) func _cleanup_dir(base_dir: String) -> void: if not DirAccess.dir_exists_absolute(base_dir): return var d := DirAccess.open(base_dir) if d == null: return # Karte t_1e82f747: get_files()/get_directories() überspringen defaultmäßig # VERSTECKTE Ordner — und .git ist versteckt. Ohne dieses Flag blieb der # Repo-Kern liegen, der Basisordner war nie leer und löschbar. d.include_hidden = true for file in d.get_files(): DirAccess.remove_absolute(base_dir.path_join(file)) for sub in d.get_directories(): _cleanup_dir(base_dir.path_join(sub)) DirAccess.remove_absolute(base_dir) # Rückfallebene: git-Pack-Bäume haben teils noch andere Sonderfälle — wenn # der Ordner trotzdem liegt, räumt rm -rf (CLI-Umgebung) garantiert ab. if DirAccess.dir_exists_absolute(base_dir): var out: PackedStringArray = [] OS.execute("/bin/rm", PackedStringArray(["-rf", base_dir]), out) func _git(dir: String, args: Array) -> Dictionary: var packed := PackedStringArray() for a in args: packed.append(str(a)) var r := EditorUpdater.git_run("git", dir, packed) if not bool(r["ok"]): # Karte t_1e82f747: rote Setup-Nummern ohne Grund sind wertlos — # der echte git-Fehlertext gehört in die Ausgabe. print(" git [%s] %s → code=%s: %s" % [dir, " ".join(args), r["code"], str(r["output"]).strip_edges()]) return r func _write_file(path: String, text: String) -> void: var f := FileAccess.open(path, FileAccess.WRITE) f.store_string(text) 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" # --- echte Repos aufbauen: origin.git (bare) + work (Klon mit main) --- var origin := base.path_join("origin.git") _check(bool(_git(base, ["init", "--bare", "--initial-branch=main", origin])["ok"]), "Setup: Bare-Repo origin.git angelegt") work = base.path_join("work") _check(bool(_git(base, ["init", "--initial-branch=main", work])["ok"]), "Setup: Arbeits-Klon angelegt") _check(bool(_git(work, ["remote", "add", "origin", origin])["ok"]), "Setup: remote origin gesetzt") _git(work, ["config", "user.email", "test@example.local"]) _git(work, ["config", "user.name", "Update-Test"]) _write_file(work.path_join("a.txt"), "Version 1\n") _check(bool(_git(work, ["add", "a.txt"])["ok"]), "Setup: Datei gestagt") _check(bool(_git(work, ["commit", "-m", "v1", "a.txt"])["ok"]), "Setup: Commit v1") _check(bool(_git(work, ["push", "-u", "origin", "main"])["ok"]), "Setup: main nach origin gepusht") var local_sha := EditorUpdater.head_sha(git, work) _check(local_sha.length() == 40, "head_sha: 40-Zeichen-SHA gelesen") print("== Check: auf dem neusten Stand ==") var up := EditorUpdater.check_update(git, work) _check(str(up["status"]) == "up_to_date", "gleiches SHA → up_to_date") _check(str(up["local"]) == local_sha and str(up["remote"]) == local_sha, "beide SHAs befüllt und identisch") print("== Check: Update verfügbar ==") # Fremder Klon pusht einen neuen Commit nach origin — work bleibt hinten. var other := base.path_join("other") _check(bool(_git(base, ["clone", "-q", origin, other])["ok"]), "Setup: zweiter Klon") _git(other, ["config", "user.email", "test@example.local"]) _git(other, ["config", "user.name", "Update-Test"]) _write_file(other.path_join("b.txt"), "neue Version\n") _git(other, ["add", "b.txt"]) _git(other, ["commit", "-m", "v2", "b.txt"]) _check(bool(_git(other, ["push", "origin", "main"])["ok"]), "Setup: v2 gepusht") var avail := EditorUpdater.check_update(git, work) _check(str(avail["status"]) == "update_available", "origin voraus → update_available") _check(str(avail["local"]) == local_sha, "lokaler SHA bleibt (Pull ist NICHT Teil des Checks)") _check(str(avail["remote"]) != local_sha, "remote SHA ist der neue") print("== Banner-Logik ==") var text := EditorUpdater.banner_text(local_sha, str(avail["remote"])) _check(text.contains("Update verfügbar"), "Banner-Text enthält „Update verfügbar“") _check(text.contains(local_sha.substr(0, 7)), "Banner-Text enthält kurzen ALT-SHA") _check(text.contains(EditorUpdater.short_sha(str(avail["remote"]))), "Banner-Text enthält kurzen NEU-SHA") _check(EditorUpdater.short_sha(local_sha).length() == 7, "short_sha = 7 Zeichen") print("== Check: nicht erreichbar ==") # Port 1 (bewusst zu) → Verbindung sofort abgelehnt = der Offline-Pfad. _git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"]) var dead := EditorUpdater.check_update(git, work) _check(str(dead["status"]) == "unreachable", "Origin tot → unreachable (still, kein Fehler)") _check(str(dead["local"]) == local_sha, "lokaler SHA trotzdem bekannt") # Falsches Binary → error (kein Repo-Check möglich) var bad := EditorUpdater.check_update("gibt-es-nicht-xyz", work) _check(str(bad["status"]) == "error", "git nicht gefunden → error (nicht unreachable)") print("== Credential-Guard (Karte t_1457e935): git wird NIE interaktiv ==") _check(EditorUpdater.looks_like_auth_error( "fatal: could not read Username for 'http://127.0.0.1:13020': No such device or address"), "auth-Erkennung: could-not-read-Username") _check(EditorUpdater.looks_like_auth_error( "remote: Unauthorized\nfatal: Authentication failed for 'http://x/'"), "auth-Erkennung: Authentication failed / Unauthorized") _check(EditorUpdater.looks_like_auth_error("fatal: could not read Username; terminal prompts disabled"), "auth-Erkennung: terminal prompts disabled") _check(not EditorUpdater.looks_like_auth_error( "fatal: unable to access 'http://127.0.0.1:1/': Connection refused"), "auth-Erkennung: Connection refused ist KEIN Auth-Fehler") var spawn := EditorUpdater.spawn_argv("git", "/projekt", PackedStringArray(["ls-remote", "origin"])) if OS.has_feature("linux"): _check(str(spawn[0]) == "/usr/bin/env", "argv: POSIX-Spawn über /usr/bin/env") _check(spawn.has("GIT_TERMINAL_PROMPT=0"), "argv: GIT_TERMINAL_PROMPT=0 (kein Terminal-Prompt)") _check(spawn.has("GIT_ASKPASS=/bin/true"), "argv: GIT_ASKPASS=/bin/true (überstimmt geerbtes SSH_ASKPASS)") _check(spawn.has("-c") and spawn.has("credential.helper=") and spawn.has("credential.helper=store"), "argv: Helper-Reset + ausschließlich store") _check(spawn.has("core.askPass=/bin/true"), "argv: core.askPass-Fallback gesetzt") var hat_home := false for eintrag in spawn: if str(eintrag).begins_with("HOME="): hat_home = true _check(hat_home, "argv: HOME aus der Umgebung mitgegeben (kein Hardcode)") _check(str(spawn[spawn.size() - 2]) == "ls-remote" and str(spawn[spawn.size() - 1]) == "origin", "argv: originale git-Argumente bleiben hinten") print("== Pull ==") _git(work, ["remote", "set-url", "origin", origin]) var pulled := EditorUpdater.pull(git, work) _check(bool(pulled["ok"]), "pull --ff-only erfolgreich (v2 wird geladen)") _check(EditorUpdater.head_sha(git, work) == str(avail["remote"]), "nach Pull ist work auf v2") # Befund 25.10.: Klon VOR origin/main (z. B. Feature-Branch) ist KEIN Update. _git(work, ["config", "user.email", "t@example.invalid"]) _git(work, ["config", "user.name", "Update-Test"]) _write_file(work.path_join("lokal.txt"), "nur lokal\n") _git(work, ["add", "lokal.txt"]) _git(work, ["commit", "-m", "lokal voraus", "lokal.txt"]) var ahead := EditorUpdater.check_update(git, work) _check(str(ahead["status"]) == "up_to_date", "Klon VORAUS (Feature-Branch) → up_to_date, kein Dauer-Banner") _git(work, ["reset", "--hard", "-q", str(avail["remote"])]) _git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"]) var dead_pull := EditorUpdater.pull(git, work) _check(not bool(dead_pull["ok"]), "pull gegen toten Origin schlägt fehl") _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("core/thumbs")) _write_file(other.path_join("core/thumbs/zz_probe.png.import"), "origin-version\n") _git(other, ["add", "core/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("core/thumbs")) _write_file(work.path_join("core/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("core/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("core/thumbs/zwei.png.import"), "alt\n") var s1 := EditorUpdater._stash_local_changes(git, work) _write_file(work.path_join("core/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("core/thumbs/zwei.png.import")) DirAccess.remove_absolute(work.path_join("core/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") var errs := EditorUpdater.import_errors(["Parse Error: Identifier not declared in the current scope."]) _check(errs.size() == 1, "Parse Error wird erkannt (Exit-Code-0-Falle, Lektion 17.10.)") errs = EditorUpdater.import_errors(["SCRIPT ERROR: blub", "und weiter"]) _check(errs.size() == 1, "SCRIPT ERROR wird erkannt") # Echter Run mit Exit-Code != 0 (Benutzung von "false": endet mit 1). if FileAccess.file_exists("/bin/false"): var failed := EditorUpdater.run_import("/bin/false", work) _check(not bool(failed["ok"]), "Import mit Exit-Code 1 = NICHT ok (kein Restart)") _check((failed["errors"] as Array).size() > 0, "Import-Fehlerliste befüllt") # Echter Run, Exit-Code 0, ABER Parse-Error in der Ausgabe (die 17.10.-Falle): if FileAccess.file_exists("/bin/sh"): var fake := base.path_join("fake_godot.sh") _write_file(fake, "#!/bin/sh\necho \"Parse Error: Identifier not declared.\"\nexit 0\n") var code := OS.execute("chmod", PackedStringArray(["+x", fake])) if code == 0: var trap := EditorUpdater.run_import(fake, work) _check(not bool(trap["ok"]), "Import „grün“ MIT Parse-Error = NICHT ok (kein Restart!)") print("== Restart-Auflösung ==") var cfg := ConfigFile.new() cfg.set_value("update", "restart_command", "echo hallo welt") var resolved := EditorUpdater.resolve_restart(cfg) _check(str(resolved["mode"]) == "command", "cfg-Kommando gewinnt (mode command)") var argv: PackedStringArray = resolved["argv"] var want := ["echo", "hallo", "welt"] var ok := argv.size() == want.size() if ok: for i in want.size(): if str(argv[i]) != want[i]: ok = false _check(ok, "Kommando sauber an Leerzeichen gesplittet") var resolved2 := EditorUpdater.resolve_restart(ConfigFile.new()) _check(str(resolved2["mode"]) == "self" or str(resolved2["mode"]) == "command", "ohne cfg: plausibler Default (self ODER systemd-Unit erkannt)") if str(resolved2["mode"]) == "self": _check((resolved2["argv"] as PackedStringArray).is_empty(), "mode self: UI baut die Selbst-Restart-Argumente selbst") var self_args := EditorUpdater.self_restart_args("/pfad/zum/klon") _check(str(self_args[0]) == "--path" and str(self_args[1]) == "/pfad/zum/klon", "Selbst-Restart: --path (startet die Map-Editor-APP, nicht die Godot-Editor-Shell)") ## Regression (20.10., Live-Bug im Map-Builder): git schreibt bei einer ## HTTP-Umleitung zuerst "warning: Umleitung nach ..." in die GEMISCHTE ## Ausgabe (git_run mischt stdout+stderr). Die alte Fassung nahm die ERSTE ## Zeile als Ziel-Commit → Banner "Update verfügbar: → warning" hing ## endlos, obwohl der Klon aktuell war. Erwartung: echte "\t"-Zeile ## gewinnt, Warnzeilen werden nie als SHA gelesen. func _test_ls_remote_warnzeile() -> void: var sha := "d0d4fbd2213e596d117b779c6d931629bf9b20ed" var mit_warnung := "warning: Umleitung nach http://127.0.0.1:13020/x.git/\n%s\trefs/heads/main\n" % sha _check(EditorUpdater.sha_aus_ls_remote(mit_warnung, "main") == sha, "Warnzeile ignoriert, echter Commit gelesen") _check(not EditorUpdater.ist_sha("warning"), "'warning' gilt nicht als SHA") _check(EditorUpdater.sha_aus_ls_remote("warning: Umleitung nach x\n", "main") == "", "nur Warnzeilen → leerer Commit (kein Update-Banner)") _check(EditorUpdater.sha_aus_ls_remote("fatal: could not read Username\n", "main") == "", "fatal-Zeile wird nicht als SHA gelesen") _check(EditorUpdater.sha_aus_ls_remote("%s\trefs/heads/main" % sha, "main") == sha, "normale Zeile funktioniert weiterhin") _check(EditorUpdater.sha_aus_ls_remote("%s\trefs/heads/anderer" % sha, "main") == sha, "anderer Branch: SHA bleibt als Notnagel nutzbar") ## Map-Kern als Submodul: Pull allein reicht nicht, das Update muss ihn mitholen. func _test_submodul_kommt_mit() -> void: print("== Submodul ==") OS.set_environment("GIT_ALLOW_PROTOCOL", "file") var core_bare := base.path_join("core.git") var app_bare := base.path_join("app.git") var core_work := base.path_join("core_work") var app_work := base.path_join("app_work") var alt := base.path_join("app_alt") for bare in [core_bare, app_bare]: _git(base, ["init", "--bare", "--initial-branch=main", bare]) for pair in [[core_work, core_bare, "kern.txt"], [app_work, app_bare, "app.txt"]]: _git(base, ["init", "--initial-branch=main", pair[0]]) _git(pair[0], ["remote", "add", "origin", pair[1]]) _git(pair[0], ["config", "user.email", "t@example.invalid"]) _git(pair[0], ["config", "user.name", "Update-Test"]) _write_file(str(pair[0]).path_join(pair[2]), "v1\n") _git(pair[0], ["add", "-A"]) _git(pair[0], ["commit", "-m", "v1"]) _git(pair[0], ["push", "-u", "origin", "main"]) _git(base, ["clone", app_bare, alt]) _check(bool(_git(app_work, ["-c", "protocol.file.allow=always", "submodule", "add", core_bare, "core"])["ok"]), "Submodul: im App-Repo eingetragen") _git(app_work, ["commit", "-m", "Kern als Submodul"]) _git(app_work, ["push", "origin", "main"]) var r := EditorUpdater.pull("git", alt) _check(bool(r["ok"]), "Submodul: Update läuft durch") _check(FileAccess.file_exists(alt.path_join("core/kern.txt")), "Submodul: Kern ist nach dem Update da") _write_file(core_work.path_join("kern.txt"), "v2\n") _git(core_work, ["commit", "-am", "v2"]) _git(core_work, ["push", "origin", "main"]) _git(app_work, ["-c", "protocol.file.allow=always", "submodule", "update", "--remote", "core"]) _git(app_work, ["add", "core"]) _git(app_work, ["commit", "-m", "Kern v2"]) _git(app_work, ["push", "origin", "main"]) r = EditorUpdater.pull("git", alt) _check(bool(r["ok"]) and _file_text(alt.path_join("core/kern.txt")) == "v2\n", "Submodul: neuer Kern-Stand kommt beim nächsten Update mit")