Mir wurde der Beweis gezeigt: die Ausgabe von zwei Befehlen. Der erste ist
git show deadbeef
- zeigte Änderungen an der Datei, nennen wir es Page.php. Die canBeEdited-Methode und ihre Verwendung wurden hinzugefügt.
Und in der Ausgabe des zweiten Befehls -
git log -p Page.php
- Es gab kein Deadbeef-Commit. In der aktuellen Version der Page.php-Datei gab es keine canBeEdited-Methode.
Da wir nicht schnell eine Lösung fanden, machten wir einen weiteren Patch im Master, zerlegten die Änderungen - und ich beschloss, dass ich mit einem neuen Verstand zum Problem zurückkehren würde.
"Offtopic"
, Git. , , .
Wurde es absichtlich gemacht? Die Datei wurde umbenannt?
Ich begann nach dem Problem zu suchen, indem ich im Chat des Teams der Release-Ingenieure um Hilfe bat. Sie sind unter anderem für das Hosting von Repositorys und die Automatisierung von Git-bezogenen Prozessen verantwortlich. Um ehrlich zu sein, hätten sie den Patch wahrscheinlich entfernen können, aber sie hätten es spurlos gemacht.
Einer der Release-Ingenieure schlug vor, git log mit der Option --follow auszuführen. Möglicherweise wurde die Datei umbenannt und daher zeigt Git einige der Änderungen nicht an.
--follow Fahren
Sie fort, den Verlauf einer Datei über das Umbenennen hinaus aufzulisten (funktioniert nur für eine einzelne Datei).
(Dateiversionsverlauf nach dem Umbenennen anzeigen (funktioniert nur für einzelne Dateien))
Es
git log --follow Page.php
gab ein Deadbeef in der Ausgabe , aber keine Datei wurde gelöscht oder umbenannt. Und doch war nicht sichtbar, dass die canBeEdited-Methode irgendwo gelöscht wurde. Die folgende Option schien in dieser Geschichte eine Rolle zu spielen, aber wo die Änderungen stattfanden, war noch unklar.
Leider ist das betreffende Repository eines der größten, das wir haben. Von der Einführung des ersten Patches bis zu seinem Verschwinden gab es 21.000 Commits. Es war auch ein Glück, dass die erforderliche Datei nur in zehn von ihnen bearbeitet wurde. Ich habe sie alle studiert und fand nichts Interessantes.
Wir suchen Zeugen! Wir brauchen einen Lebendbären
Halt! Wir haben nur nach Deadbeef gesucht? Denken wir logisch: Es muss ein Commit geben, nennen wir es Livebear. Danach wird Deadbeef nicht mehr im Dateiverlauf angezeigt. Vielleicht gibt uns das nichts, aber es gibt uns einige Gedanken.
Es gibt einen git bisect-Befehl zum Durchsuchen des Git-Verlaufs. Gemäß der Dokumentation können Sie das Commit finden, in dem der Fehler zum ersten Mal aufgetreten ist. In der Praxis kann es verwendet werden, um jeden Moment in der Geschichte zu finden, wenn Sie wissen, wie Sie feststellen können, ob dieser Moment angekommen ist. Unser Fehler war das Fehlen von Änderungen im Code. Ich könnte dies mit einem anderen Befehl überprüfen - git grep. Immerhin hat es mir gereicht zu wissen, ob es in Page.php eine canBeEdited-Methode gibt. Ein bisschen Debuggen und Lesen der Dokumentation:
livebear [build]: Füge branch origin / XXX in build_web_yyyy.mm.dd.hh zusammen
Es sieht aus wie ein normales Zusammenführungs-Commit eines Task-Zweigs mit einem Release-Zweig. Aber mit diesem Commit konnte ich das Problem reproduzieren:
$ git checkout -b test livebear^1 2>/dev/null $ grep -c canBeEdited Page.php 2 $ git merge —-no-edit -—no-stat livebear^2 Removing … … Removing … Merge made by the ‘recursive’ strategy. $ grep -c canBeEdited Page.php 0 $ git log -p Page.php | grep -c canBeEdited 0
Zwar fand ich bei Livebear nichts Interessantes, und der Zusammenhang mit unserem Problem blieb unklar. Nachdem ich ein wenig nachgedacht hatte, schickte ich die Ergebnisse meiner Suche an den Entwickler: Wir waren uns einig, dass das Reproduktionsschema zu kompliziert sein wird, selbst wenn wir zur Wahrheit kommen, und wir können uns in Zukunft nicht gegen so etwas versichern. Aus diesem Grund haben wir offiziell beschlossen, die Suche einzustellen.
Meine Neugier blieb jedoch unbefriedigt.
Beharrlichkeit ist kein Laster, sondern ein großes Ekel
Mehrmals kehrte ich zu dem Problem zurück, ließ git bisect laufen und fand immer mehr neue Commits. Alle sind misstrauisch, alle sind Fusionen, aber das hat mir nichts gegeben. Es scheint mir, dass ein Commit mir dann öfter begegnet ist als andere, aber ich bin mir nicht sicher, ob er am Ende der Schuldige war.
Natürlich habe ich auch andere Suchmethoden ausprobiert. Zum Beispiel habe ich mehrmals die 21.000 Commits durchlaufen, die zum Zeitpunkt des Problems vorgenommen wurden. Es war nicht sehr aufregend, aber ich stieß auf ein interessantes Muster. Ich habe den gleichen Befehl ausgeführt:
git grep -c canBeEdited {commit} -- Page.php
Es stellte sich heraus, dass sich die "schlechten" Commits, die nicht den erforderlichen Code hatten, in derselben Verzweigung befanden! Und eine Suche in diesem Thread führte mich schnell zu einem Hinweis:
changekiller Zweig 'master' in TICKET-XXX_description zusammenführen
Dies war auch eine Fusion zweier Zweige. Und beim Versuch, es lokal zu wiederholen, gab es einen Konflikt in der erforderlichen Datei - Page.php. Nach dem Status des Repositorys zu urteilen, verließ der Entwickler seine Version der Datei und verwarf die Änderungen vom Master (dh sie gingen verloren). Eine lange Zeit verging und der Entwickler erinnerte sich nicht genau daran, was passiert war, aber in der Praxis wurde die Situation in einer einfachen Reihenfolge reproduziert:
git checkout -b test changekiller^1 git merge -s ours changekiller^2
Es bleibt abzuwarten, wie eine legitime Abfolge von Maßnahmen zu einem solchen Ergebnis hätte führen können. Da ich in der Dokumentation nichts darüber fand, ging ich in den Quellcode.
Ist der Mörder Git?
In der Dokumentation heißt es, dass das Git-Protokoll mehrere Commits als Eingabe empfängt und dem Benutzer die übergeordneten Commits anzeigen sollte, mit Ausnahme der Eltern der Commits, die mit einem ^ vor ihnen übermittelt wurden. Es stellt sich heraus, dass das Git-Protokoll A ^ B Commits anzeigen sollte, die Eltern von A und nicht Eltern von B sind.
Der Befehlscode erwies sich als ziemlich komplex. Es gab viele verschiedene Optimierungen für die Arbeit mit dem Speicher, und im Allgemeinen schien mir das Lesen von C-Code nie eine sehr angenehme Erfahrung zu sein. Die Grundlogik kann mit folgendem Pseudocode dargestellt werden:
// , commit commit; rev_info revs; revs = setup_revisions(revisions_range); while (commit = get_revision(revs)) { log_tree_commit(commit); }
Hier akzeptiert die Funktion get_revision revs, eine Reihe von Steuerflags, als Eingabe. Jeder seiner Aufrufe sollte das nächste Commit für die Verarbeitung in der richtigen Reihenfolge (oder Leere, wenn wir das Ende erreicht haben) geben. Es gibt auch eine setup_revisions-Funktion, die die Drehzahlstruktur ausfüllt, und log_tree_commit, die Informationen auf dem Bildschirm anzeigt.
Ich hatte das Gefühl, herauszufinden, wo ich nach dem Problem suchen sollte. Ich habe eine bestimmte Datei (Page.php) an den Befehl übergeben, da ich nur an den Änderungen interessiert war. Dies bedeutet, dass das Git-Protokoll eine Logik zum Filtern von "zusätzlichen" Commits haben muss. Die Funktionen setup_revisions und get_revision wurden vielerorts verwendet - kaum das Problem damit. Das ließ log_tree_commit übrig.
Zu meiner unbeschreiblichen Freude gab es in dieser Funktion wirklich einen Code, der berechnet, welche Änderungen an einem bestimmten Commit vorgenommen wurden. Ich dachte, die allgemeine Logik sollte ungefähr so aussehen:
void log_tree_commit(commit) { if (tree_has_changed(commit, commit->parents)) { log_tree_commit_1(commit); } }
Aber je länger ich mir den echten Code ansah, desto mehr wurde mir klar, dass ich falsch lag. Diese Funktion druckte nur Nachrichten. Also glauben Sie Ihren Gefühlen danach!
Ich ging zurück zu den Funktionen setup_revisions und get_revision. Die Logik ihrer Arbeit war schwer zu verstehen - der "Nebel" der Hilfsfunktionen störte, von denen einige benötigt wurden, um korrekt mit Zeigern und Speicher zu arbeiten. Alles sah so aus, als wäre die Hauptlogik eine einfache Durchquerung des Festschreibungsbaums, dh ein ziemlich normaler Algorithmus:
rev_info setup_revisions(revisions_range, ...) { rev_info rev; commit commit; // — for (commit = get_commit_from_range(revisions_range)) { revs->commits = commit_list_append(commit, revs->commits) } } commit get_revision(rev_info revs) { commit c; commit l; c = get_revision_1(revs); for (l = c->parents; l; l = l->next) { commit_list_insert(l, &revs->commits); } return c; } commit get_revision_1(rev_info revs) { return pop_commit(revs->commits); }
Eine Liste wird erstellt (revs-> Commits), das erste (oberste) Element des Commit-Baums wird dort platziert. Dann werden die Commits von Anfang an schrittweise aus dieser Liste entfernt und ihre Eltern am Ende hinzugefügt.
Beim Lesen des Codes stellte ich fest, dass es im "Nebel" der Hilfsfunktionen eine komplexe Logik zum Filtern von Commits gibt, nach der ich so lange gesucht habe. Dies geschieht in der Funktion get_revision_1:
commit get_revision_1(rev_info revs) { commit commit; commit = pop_commit(revs->commits); try_to_sipmlify_commit(commit); return commit; } void try_to_simplify_commit(commit commit) { for (parent = commit->parents; parent; parent = parent->next) { if (rev_compare_tree(revs, parent, commit) == REV_TREE_SAME) { parent->next = NULL; commit->parents = parent; } } }
Wenn mehrere Zweige zusammengeführt werden und der Status der Datei derselbe bleibt wie in einem von ihnen, ist es nicht sinnvoll, andere Zweige zu berücksichtigen. Wenn sich der Status der Datei nirgendwo geändert hat, verlassen wir nur den ersten Zweig.
Beispiel. Bezeichnen wir die Commits, bei denen sich die Datei nicht geändert hat, mit Null, mit Eins - denen, bei denen sich die Datei geändert hat, und mit X - dem Zusammenführen von Zweigen.
In dieser Situation berücksichtigt der Code den Feature-Zweig nicht - es gibt keine Änderungen daran. Wenn die Datei dort geändert wurde, wurden die Änderungen in X "verworfen", was bedeutet, dass ihr Verlauf nicht sehr relevant ist: Dieser Code ist nicht mehr vorhanden.
Ähnliches ist bei uns passiert. Zwei Entwickler haben Änderungen in einer Datei vorgenommen - Page.php, eine - im Master-Zweig, im Deadbeef-Commit, die zweite - in ihrem Task-Zweig.
Als der zweite Entwickler Änderungen aus dem Master-Zweig in den Task-Zweig zusammenführte, trat während der Lösung ein Konflikt auf, bei dem er die Änderungen einfach aus dem Master herauswarf. Die Zeit verging, er beendete die Arbeit an der Aufgabe und der Aufgabenzweig wurde auf den Master hochgeladen, wodurch die Änderungen aus dem Deadbeef-Commit entfernt wurden.
Das Commit selbst blieb bestehen. Wenn Sie jedoch git log mit dem Parameter Page.php ausführen, wird das Deadbeef-Commit in der Ausgabe nicht angezeigt.
Optimierung ist ein undankbarer Job
Ich beeilte mich, die Regeln für das Senden von Änderungen und Fehlern an Git selbst sorgfältig zu studieren. Immerhin dachte ich, ich hätte ein wirklich ernstes Problem gefunden: Denken Sie nur, einige der Commits verschwinden einfach aus der Ausgabe - und dies ist das Standardverhalten! Glücklicherweise erwiesen sich die Regeln als umfangreich, die Zeit war spät und am nächsten Morgen war meine Sicherung weg.
Ich habe festgestellt, dass diese Optimierung die Git-Leistung in großen Repositorys wie unserem erheblich beschleunigt. Es gibt auch Dokumentation dafür in man git-rev-list , und dieses Verhalten kann sehr einfach deaktiviert werden.
Übrigens, wie ist --follow in diese Geschichte involviert?
Tatsächlich gibt es viele Möglichkeiten, die Funktionsweise dieser Logik zu beeinflussen. Insbesondere zum Follow-Flag im Git-Code wurde vor 13 Jahren ein Kommentar gefunden:
Commits können nicht mit folgender Umbenennung beschnitten werden: Die Pfade ändern sich.
(Übersetzung: Beim Umbenennen können keine Commits ausgelöst werden: Pfade können sich ändern.)
PS
Ich selbst bin seit einigen Jahren Teil des Release-Engineering-Teams von Badoo, und viele im Unternehmen glauben, dass wir Git verstehen.
(Übersetzung: Original: xkcd.com/1597 )
In dieser Hinsicht müssen wir uns mit den Problemen befassen, die in diesem System auftreten, und einige von ihnen scheinen mir ziemlich neugierig zu sein - wie zum Beispiel in diesem Artikel beschrieben. Sehr oft werden Probleme schnell gelöst: Wir sind bereits auf viel gestoßen, etwas ist in der Dokumentation gut beschrieben. Dieser Fall war eine Ausnahme.
Tatsächlich hatte die Dokumentation zwar einen Abschnitt zur Vereinfachung des Verlaufs, aber nur für den Befehl git rev-list, und ich dachte nicht daran, dort nachzuschauen. Vor sechs Monaten wurde dieser Abschnitt in das Handbuch des Befehls git log aufgenommen, aber unser Fall ist etwas früher aufgetreten - ich hatte einfach keine Zeit, diesen Artikel fertigzustellen. (*)
Und schließlich habe ich einen kleinen Bonus für diejenigen, die bis zum Ende gelesen haben. Ich habe ein sehr kleines Repository, in dem das Problem reproduziert wird:
$ git clone https://github.com/Md-Cake/lost-changes.git Cloning into 'lost-changes'... … $ git log --oneline test.php edfd6a4 master: print 3 between 1 and 2 096d4cf init $ git log --oneline --full-history test.php afea493 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'changekiller' 57041b8 (origin/changekiller) print 4 between 1 and 2 edfd6a4 master: print 3 between 1 and 2 096d4cf init
Vielen Dank für Ihre Aufmerksamkeit!
(*) UPD: Es stellte sich heraus, dass der Abschnitt zur Vereinfachung des Verlaufs viel länger als sechs Monate in der Dokumentation des Befehls git log enthalten war, und ich habe ihn einfach übersprungen. Vielen Dank youROCKdas machte darauf aufmerksam!