publish kann den allerersten Commit nie zu einem leeren Remote pushen - _local_ahead_of_remote verwechselt "Branch nie gepusht" mit "nicht ahead"
#97
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Befund
Nach
publish --no-push(dem Workaround aus #96) existierte ein lokaler Commit aufmain,originwar ein frisch angelegtes, komplett leeres Repository. Der zweite, dokumentiertepublish-Aufruf sollte diesen Commit pushen:Kein Push fand statt - verifiziert per
git ls-remote origin(leer) direkt danach. Beliebig oft wiederholt, solange der Working-Tree sauber war, blieb das Ergebnis dasselbe: der Commit blieb für immer lokal.Betroffene Version: 5.0.0. Behoben in 5.0.1-beta.1, Commit
9ef021b.Ursache - verifiziert
git fetch origin mainscheitert gegen ein leeres Remote-Repo erwartungsgemäß (fatal: couldn't find remote ref main). Das ist infetch_remote()als harmloser Fall dokumentiert, undreconcile()behandelte es korrekt alsno-remote-or-fetch-failed.Das Problem saß in
_local_ahead_of_remote(), die entscheidet, ob trotz leerem Working-Tree noch etwas zu pushen ist:remote_ref_exists()dokumentiert selbst den Fall, der hier eintrat: "false for a remote that was never fetched, or a branch that has never been pushed (the very first publish)". Genau dieser Fall wurde alsFalsegewertet, obwohl lokal ein Commit existierte, den der Remote nicht hatte. Inpublish_command()führte das zu einem frühen Return -changesleer,local_aheadfälschlichFalse,git pushnie aufgerufen.Dieselbe Funktion trug im eigenen Docstring die Selbstbeschreibung "true right after a stranded commit from a previous failed push" - sie sollte genau diesen Fall erkennen, tat es aber nicht, wenn der Remote-Branch noch nie existiert hatte. Ohne Fix gab es keinen Weg, einen bereits committeten Stand über
tools/wikitool publishzu einem leeren Remote zu bringen; Invariante 5 verbietet den naheliegenden Ausweg (rohesgit push).Präzisierung gegenüber der ersten Fassung: "jede neue Instanz" war zu weit gegriffen. Mit #96 behoben läuft der Normalpfad (
publish-> Gate 42 ->publish --confirm) durch, weilchangesdort nicht leer ist und der Push regulär stattfindet. Dieser Defekt griff, sobald der Commit stand und der Working-Tree sauber war: nach--no-push, nach einem an Netz/Auth gescheiterten Push, und bei jedem Wiederholungsversuch danach.Entscheidung und Umsetzung
Die offene Frage aus Akzeptanzkriterium 3 - wie "Branch existiert auf dem Remote nicht" von "Remote unerreichbar" unterschieden wird - ist beantwortet:
git ls-remote --exit-code <remote> <branch>trennt die drei Lagen über den Exit-Code allein, ohne Ausgabe zu parsen. Auf die Meldung zu matchen schied aus, weil git seine Fehlertexte übersetzt.Empirisch gemessen:
ls-remote --exit-code <remote> <branch>Umgesetzt, wie geplant:
remote_lacks_branch(remote, branch)kapselt den Aufruf und istTruenur bei rc 2. Sein Docstring hält fest, dass der Exit-Code und nicht die Meldung gelesen wird, und warum._has_commits(branch)(git rev-list --count -n 1 <branch>, rc 128 auf ungeborenem Branch).-n 1deckelt den Walk, statt die ganze Historie zu zählen, um eine Ja/Nein-Frage zu beantworten._local_ahead_of_remote()behandelt den fehlenden Tracking-Ref zweigeteilt:remote_lacks_branch(...) and _has_commits(...). Ein unerreichbares Remote behält das bisherigeFalse.remote_ref_exists()blieb unverändert - sein zweiter Aufruferreconcile()meint damit weiterhin richtig "nichts zum Abgleichen da". Der Fix sitzt ausschließlich in_local_ahead_of_remote().Warum der unerreichbare Fall bewusst nicht als "ahead" gilt: sonst liefe jeder Publish ohne Netz in einen scheiternden
git pushstatt in das heutige "Nothing to commit" - eine Verhaltensänderung für Offline- und Nur-lokal-Instanzen, die dieser Defekt nicht verlangt. Wo es tatsächlich etwas zu committen gibt, meldet der Push den echten Netz-/Auth-Fehler ohnehin unverändert.Akzeptanzkriterien
publisheinen bereits vorhandenen lokalen Commit nachorigin/main, auch bei sauberem Working-Tree und leeremchanges. Test:test_publish_pushes_a_committed_stand_to_a_remote_that_has_no_branch_yet.git init -b main->publish --no-push->publishpusht. Genau diese Abfolge ist der Testkörper des vorigen Punktes.test_an_unreachable_remote_is_not_mistaken_for_one_that_lacks_the_branch, das beide Ebenen prüft (remote_lacks_branchund_local_ahead_of_remote).test_an_unborn_branch_is_not_ahead_of_an_empty_remote.remote_ref_exists()undreconcile()verhalten sich unverändert; die bestehendensync- und Rebase-Review-Tests liefen ohne jede Anpassung durch. Zusätzlich hälttest_a_reachable_remote_without_the_branch_is_told_apart_from_one_that_has_itdie positive Hälfte der Unterscheidung fest.tools/chemenu/tests/test_git_publish.pyträgt die Tests mit leerem erreichbarem bare-Remote und mit unerreichbarem Remote, über das neuefresh_instance-Fixture. Derrepo-Fixture taugte wie erwartet für keinen von beiden.publish-Zeile intools/CONTRACT.mdsagt jetzt die Wahrheit: die Zusage zum Strandungsfall gilt ausdrücklich auch für einen Branch, den der Remote nie gesehen hat, und der unerreichbare Fall ist als bewusste Ausnahme benannt.instructions/setup-instance.mdSchritt 14 beschreibt den tatsächlichen Ablauf - bestätigt, nicht angenommen: der Schritt war bereits richtig und blieb unverändert.Verifikation
pytestintools/: 1204 Tests grün, davon 96 intest_git_publish.py.test_publish_pushes_a_committed_stand_to_a_remote_that_has_no_branch_yet; die drei Schutztests (unerreichbares Remote, ungeborener Branch nicht ahead, positive Unterscheidung) bestehen erwartungsgemäß in beide Richtungen - sie halten fest, was sich nicht ändern durfte.instructions/dev/testing-conventions.mdSchritt 6: identisches Ergebnis, 1204 grün.tools/wikitool docs verifyundtools/wikitool instructions verify: beide OK.Beteiligte Dateien
tools/chemenu/commands/git_publish.py-_local_ahead_of_remote(), neuremote_lacks_branch()und_has_commits();remote_ref_exists()undfetch_remote()unveränderttools/chemenu/tests/test_git_publish.pytools/CONTRACT.md(publish-Zeile)Beziehung zu #96
Verschiedene Funktionen, verschiedene Ursachen, dieselbe Situation deckte beide auf: der allererste Publish einer neuen Instanz. #96 verhinderte den ersten Commit selbst, dieser hier dessen Push. Gemeinsam umgesetzt, ein Commit (
9ef021b), ein--patch-Bump (5.0.0 -> 5.0.1-beta.1).Changelog: Offene Designfrage aus Akzeptanzkriterium 3 beantwortet und als Abschnitt "Entscheidung" in den Body geschrieben:
git ls-remote --exit-code <remote> <branch>trennt die drei Fälle über den Exit-Code (0 / 2 / 128, empirisch gemessen), ohne locale-abhängige Meldungen zu parsen. Fix bleibt auf_local_ahead_of_remote()beschränkt -remote_ref_exists()behält seine heutige Bedeutung fürreconcile(). Nullcommit-Schutz und die Begründung, warum der unerreichbare Fall bewusst beim heutigenFalsebleibt, ergänzt. Reichweite im Abschnitt "Warum das zählt" korrigiert: "jede neue Instanz" war zu weit - mit #96 behoben läuft der Normalpfad durch, dieser Defekt greift bei bereits stehendem Commit und sauberem Working-Tree. Zwei Akzeptanzkriterien ergänzt (ungeborener Branch nicht "ahead";reconcileunverändert), dietools/CONTRACT.md-Zeile als heute falsche Zusage benannt. Schnitt: gemeinsame Umsetzung mit #96, ein--patch-Bump.Changelog: Umgesetzt und veröffentlicht (
9ef021b, 5.0.0 -> 5.0.1-beta.1). Body auf den Endstand geschrieben: alle acht Akzeptanzkriterien abgehakt und je mit dem Test benannt, der sie hält. Gegenüber dem Plan eine Ergänzung: nebenremote_lacks_branch()kam ein zweiter Helper_has_commits()dazu, statt den Nullcommit-Schutz inline zu schreiben -git rev-list --count -n 1, damit die Ja/Nein-Frage nicht die ganze Historie zählt. Abschnitt "Verifikation" ergänzt (1204 Tests grün, Gegenprobe gegen den zurückgedrehten Fix, Leere-Maschine-Lauf,docs verify/instructions verify). Letztes Kriterium bestätigt statt angenommen:instructions/setup-instance.mdSchritt 14 war bereits richtig und blieb unverändert.