Wer war's? Den Schuldigen mit git blame finden
git blame erklärt: Ausgabe lesen, Zeilen mit -L, -w, -M, -C eingrenzen, Bulk-Commits ignorieren und die echte Änderung einer Zeile finden.
git blame versieht jede Zeile einer Datei mit einer Anmerkung zu dem Commit, der sie zuletzt geändert hat, samt Autor und Datum dieses Commits.
Der genannte Commit ist häufig der falsche. Man schlägt eine merkwürdige Zeile in fremdem Code nach, und blame präsentiert einen „apply prettier”-Commit über 4.000 Dateien von vor achtzehn Monaten.
Diese Sackgasse ist das normale Ergebnis eines nackten git blame, und daran vorbeizukommen ist die eigentliche Fertigkeit. Dieser Artikel behandelt, wie man die Standardausgabe liest, wie man sie mit -L, -w, -M und -C eingrenzt und von Rauschen befreit, wie man sich rückwärts durch die Eltern-Commits arbeitet, bis man bei der relevanten Änderung ankommt, sowie zwei neuere Ergänzungen: --diff-algorithm (Git 2.53 oder neuer) und git last-modified (Git 2.52 oder neuer).
Die wichtigsten Erkenntnisse
- Der Commit, den git blame für eine Zeile anzeigt, ist der letzte, der sie angefasst hat – und das ist häufig eine Neuformatierung, eine Umbenennung oder eine Verschiebung und nicht die Änderung, die der Zeile ihre Bedeutung gegeben hat.
- blame erneut beim Eltern-Commit des gemeldeten Commits auszuführen (
git blame <hash>^ -- file) und dies zu wiederholen, ist der verlässliche Weg zur ursprünglichen Änderung;--ignore-revund--ignore-revs-fileüberspringen bekannte Rausch-Commits automatisch. -wignoriert Whitespace,-Mverfolgt Zeilen, die innerhalb einer Datei verschoben wurden (Standardschwelle 20 alphanumerische Zeichen), und-Cverfolgt Zeilen, die aus anderen Dateien kopiert wurden (Standard 40), wobei bis zu drei-C-Flags die Suche ausweiten.- Git 2.53 hat
--diff-algorithmzu git blame hinzugefügt; akzeptiert werdenpatience,minimal,histogramodermyers, wobeimyersder Standard ist. - Git 2.52 hat das experimentelle
git last-modifiedeingeführt, das in einem einzigen Durchlauf den letzten Commit meldet, der jeden Pfad in einem Verzeichnis angefasst hat.
Wie liest man die Standardausgabe von git blame?
Jede Zeile der Standardausgabe von git blame enthält vier Felder in dieser Reihenfolge: den abgekürzten Commit-Hash, den Autorennamen, das Autorendatum und die Zeilennummer, gefolgt vom Inhalt der Zeile. Der Abschnitt zum Standardformat der Man-Page listet diese Felder auf; Git kürzt den Hash standardmäßig auf sieben Hexadezimalstellen und lässt eine weitere Spalte frei für das Caret, das einen Boundary-Commit kennzeichnet (die ältesten Commits, die blame erreichen konnte). Datumsangaben werden im ISO-Format ausgegeben, sofern --date oder blame.date nichts anderes vorgibt.
git blame src/router.js
a1b2c3d4 (Jane Doe 2024-03-08 14:22:31 +0100 42) return cache.get(key) ?? fetchRoute(key);
Von links nach rechts gelesen: a1b2c3d4 ist der Commit, Jane Doe und der Zeitstempel sind die Autoren-Identität dieses Commits, 42 ist die Zeilennummer in der aktuellen Datei, und alles nach der schließenden Klammer ist die Zeile selbst.
Das Entscheidende an diesem Hash ist, was er nicht ist. Er ist nicht der Commit, der die Logik eingeführt hat. Er ist der jüngste Commit, dessen Diff die Zeile angefasst hat – und in einer Codebasis mit Formatierern, Lintern und Refactorings ist das häufig eine mechanische Änderung. Behandeln Sie das erste blame-Ergebnis als Spur, nicht als Urteil.
Wie beschränkt man git blame mit -L auf einen Zeilenbereich?
git blame -L 40,60 -- src/router.js beschränkt die Annotation auf die Zeilen 40 bis 60, und git blame -L :handleRoute -- src/router.js beschränkt sie auf den Körper der Funktion, deren Name zu diesem regulären Ausdruck passt. Beide Formen sind unter der -L-Option dokumentiert, die mehr als einmal angegeben werden darf.
git blame -L 40,60 -- src/router.js
git blame -L :handleRoute -- src/router.js
Die :funcname-Form parst Ihre Sprache nicht. Sie erkennt Funktionsnamen auf dieselbe Weise, wie git diff ermittelt, was in einer Hunk-Kopfzeile ausgegeben wird, und das lässt sich pro Dateityp über das diff-Attribut in gitattributes anpassen. Beide Bereichsgrenzen akzeptieren außerdem /regex/-Muster, und die Endgrenze akzeptiert +N-Offsets, sodass auch -L '/^function handleRoute/,+15' gültig ist.
Wie ignoriert man Whitespace und verschobenen Code in git blame?
Mit -w ignoriert git blame Whitespace beim Vergleich der Versionen, sodass eine rein einrückungsbedingte Neuformatierung die von ihr berührten Zeilen nicht mehr für sich beansprucht. -M erfasst Zeilen, die innerhalb einer Datei verschoben wurden, und -C erweitert die Suche auf Zeilen, die aus anderen im selben Commit geänderten Dateien stammen; die Man-Page nennt als Standard-Übereinstimmungsschwellen 20 bzw. 40 alphanumerische Zeichen.
| Symptom | Flag |
|---|---|
| Zeile einer Neueinrückung oder einer Bereinigung von Leerzeichen am Zeilenende zugeschrieben | -w |
| Zeile dem Commit zugeschrieben, der Code innerhalb der Datei umsortiert hat | -M |
| Zeile durch Kopieren oder Verschieben aus einer anderen Datei entstanden | -C (mehrfach kombinierbar) |
| Zeile einem bekannten Massen-Commit zugeschrieben | --ignore-rev <hash> |
git blame -w -- src/router.js
git blame -M -- src/router.js
git blame -C -C -C -- src/router.js
Jedes zusätzliche -C erweitert die Menge der Dateien, in denen git blame nach den kopierten Zeilen sucht:
-Cdurchsucht die anderen Dateien, die derselbe Commit geändert hat.-C -Cdurchsucht zusätzlich die Dateien, die der Commit angefasst hat, der diese Datei erstmals hinzugefügt hat.-C -C -Cerweitert die Suche noch einmal, auf Dateien in beliebigen Commits.
Tragen mehrere -C-Flags einen numerischen Schwellenwert, gewinnt der letzte. Eine vollständige Dateiumbenennung benötigt überhaupt kein Flag: blame verfolgt die Zeilen von sich aus darüber hinweg, und Git bietet derzeit keine Möglichkeit, dieses Verhalten abzuschalten.
Wie findet man den Commit vor einer Massen-Neuformatierung?
Um an einem mechanischen Commit vorbeizukommen, führen Sie blame beim Eltern-Commit dieses Commits erneut aus, git blame <hash>^ -- src/router.js, und wiederholen das, bis der angezeigte Commit einer ist, der das Verhalten der Zeile tatsächlich geändert hat. Das Suffix ^ ist Standard-gitrevisions-Syntax für das erste Elternteil, sodass blame vom Zustand der Datei unmittelbar vor dem Rausch-Commit ausgeht.
- Führen Sie
git blame -L 40,60 -- src/router.jsaus und notieren Sie den Hash in der interessierenden Zeile. - Prüfen Sie den Commit mit
git show --stat <hash>. Handelt es sich um eine Neuformatierung, Umbenennung oder Verschiebung, fahren Sie fort. - Führen Sie
git blame -n <hash>^ -L 40,60 -- src/router.jsaus. Das Flag-ngibt die Nummer jeder Zeile im ursprünglichen Commit aus, was wichtig ist, weil Zeilennummern zwischen Revisionen verrutschen und Sie-Lim nächsten Durchlauf möglicherweise neu ausrichten müssen. - Wiederholen Sie ab Schritt 2, bis der angezeigte Commit das Verhalten der Zeile verändert.
git blame -n a1b2c3d4^ -L 40,60 -- src/router.js
Wenn ein Repository bekannte Rausch-Commits enthält, sparen Sie sich den manuellen Durchgang. --ignore-rev <hash> weist git blame an, Zeilen jenseits eines angegebenen Commits zuzuordnen, und --ignore-revs-file tut dasselbe für eine ganze Datei mit Hashes, vollständig ausgeschrieben, einer pro Zeile. Setzen Sie blame.markIgnoredLines, um neu zugeordnete Zeilen mit ? zu kennzeichnen, und blame.markUnblamableLines, um Zeilen, die nicht neu zugeordnet werden konnten, mit * zu markieren.
git blame --ignore-rev a1b2c3d4 -- src/router.js
git blame --ignore-revs-file .git-blame-ignore-revs -- src/router.js
git config blame.markIgnoredLines true
Wie man diese Liste als .git-blame-ignore-revs committet und blame.ignoreRevsFile darauf zeigen lässt, wird in 5 Git Dotfiles Every Developer Should Know behandelt.
Einen anderen Diff-Algorithmus ausprobieren (Git 2.53 oder neuer)
Git 2.53 hat --diff-algorithm zu git blame hinzugefügt; akzeptiert werden patience, minimal, histogram oder myers (mit default als Alias für myers), und myers ist der Standard. Die Ergänzung findet sich in den Release Notes zu Git 2.53, und die zulässigen Werte sind unter der —diff-algorithm-Option in der Man-Page aufgeführt.
blame entscheidet durch ein Diff der beiden Versionen, welche Elternzeilen welchen Kindzeilen entsprechen, und verschiedene Algorithmen ordnen Zeilen unterschiedlich einander zu. Wenn ein Commit geänderte und unveränderte Zeilen verschachtelt – wie es bei Neuformatierungen oft der Fall ist – kann ein Algorithmus eine Zeile der Neuformatierung zuschreiben, während ein anderer sie dem Commit zuschreibt, der sie ursprünglich geschrieben hat.
git blame -L 40,60 -- src/router.js
git blame -L 40,60 --diff-algorithm=patience -- src/router.js
Kein Algorithmus ist dokumentiert als korrekter als ein anderer. Wenn die Standardzuordnung unplausibel wirkt, kostet dasselbe Kommando mit patience oder histogram einen zusätzlichen Aufruf und liefert eine zweite Meinung zum Vergleich.
Ein Verzeichnis mit git last-modified abfragen
Git 2.52 hat git last-modified hinzugefügt, das in einem einzigen History-Durchlauf den Commit meldet, der jeden Pfad in einem Verzeichnis zuletzt geändert hat, anstatt ein git log -1 pro Datei auszuführen; das Kommando ist als experimentell gekennzeichnet und sein Verhalten kann sich ändern. Die Man-Page zu git-last-modified nennt den experimentellen Status in ihrer NAME-Zeile und zeigt die Ausgabeform als <oid> TAB <path>, eine Zeile pro Pfad, mit vollständiger Objekt-ID und ohne Autor, Datum oder Betreff.
git last-modified -r -- src/
Ohne -r (oder eine --max-depth ungleich null) erhalten Sie nur die Einträge, die dem Pathspec selbst entsprechen, ohne Abstieg in die darunterliegenden Unterverzeichnisse. Umbenennungen und Modusänderungen zählen als Modifikationen. Die Schleife pro Datei, die es ersetzt, durchläuft dieselben Commits für jede Datei erneut; last-modified durchläuft sie einmal. Es beantwortet die Frage „Was hat sich in diesem Modul kürzlich geändert?” – eine andere Frage als „Warum existiert diese Zeile?” – und es lohnt sich, danach zu greifen, bevor Sie damit beginnen, einzelne Dateien zu blamen.
blame ist eine Frage, kein Urteil
Die Ausgabe von git blame nennt die Person, die eine Zeile zuletzt angefasst hat, und dieser Name ist fast nie die Antwort, die Sie brauchen. Führen Sie blame mit -L zum Fokussieren aus, mit -w und -M/-C zum Entfernen mechanischen Rauschens, und arbeiten Sie sich dann über die Eltern-Commits zurück (oder pflegen Sie eine Ignore-Datei), bis der angezeigte Commit eine Nachricht trägt, die die Zeile erklärt. Sobald Sie diesen Commit haben, liefert git show <hash> das Diff und die Begründung – und genau darum geht es bei der Übung: zu verstehen, warum der Code dort steht, damit Sie ihn ändern können, ohne den Vorfall zu wiederholen, der ihn ursprünglich dorthin gebracht hat.
FAQs
Wie finde ich heraus, wer eine Zeile gelöscht hat, wenn git blame doch nur noch existierende Zeilen anzeigt?
git blame verrät nichts über Zeilen, die entfernt oder überschrieben wurden, wie seine Man-Page anmerkt. Verwenden Sie stattdessen die Pickaxe: git log -S'some text' -- src/router.js listet jeden Commit auf, der diese Zeichenfolge hinzugefügt oder entfernt hat, und mit -p wird die Entfernung selbst angezeigt. Alternativ läuft git blame --reverse a1b2c3d..HEAD -- src/router.js die History von diesem Commit an vorwärts durch und nennt die jüngste Revision, in der jede Zeile noch vorhanden war.
Warum zeigt git blame bei manchen Zeilen 00000000 und 'Not Committed Yet' an?
Diese Zeilen enthalten nicht committete Änderungen. Ohne Revisionsargument annotiert git blame die Arbeitskopie der Datei, sodass jede Zeile, die von HEAD abweicht, einen Hash aus Nullen und anstelle des Autorennamens 'Not Committed Yet' erhält. Committen oder stashen Sie die Änderung, oder führen Sie git blame HEAD -- src/router.js aus, um die committete Version zu annotieren und lokale Bearbeitungen vollständig zu ignorieren.
Berücksichtigt die blame-Ansicht von GitHub eine .git-blame-ignore-revs-Datei?
Ja. GitHub wendet eine Datei namens .git-blame-ignore-revs im Repository-Root automatisch auf seine blame-Ansicht an und nutzt dabei denselben --ignore-revs-file-Mechanismus wie die Kommandozeile; dabei wird ein 'Ignoring revisions'-Banner angezeigt. Zeilen, die keinem früheren Commit neu zugeordnet werden können, zeigen weiterhin den ignorierten Commit. Die Datei konfiguriert das lokale Git nicht; jeder Entwickler muss weiterhin git config blame.ignoreRevsFile .git-blame-ignore-revs ausführen.
Was ist der Unterschied zwischen git blame und git log -L?
git blame meldet einen Commit pro Zeile: den jüngsten Commit, der sie in einer einzelnen Version der Datei angefasst hat. git log -L 40,60:src/router.js verfolgt stattdessen die Zeilen 40 bis 60 durch die History und gibt jeden Commit aus, der sie geändert hat, jeweils mit dem Diff für diesen Bereich, neueste zuerst. Verwenden Sie blame, um schnell einen Verdächtigen zu identifizieren, und log -L, um die Entwicklung der Zeilen zu beobachten. Beide akzeptieren die :funcname-Form, etwa git log -L :handleRoute:src/router.js.