Angenommen, ich habe das folgende einfache Git-Repository: einen einzelnen Zweig, einige Commits nacheinander, einige davon wurden getaggt (mit kommentiert Tags), nachdem ich jeden von ihnen festgeschrieben habe, und dann beschließe ich eines Tages, dass ich den ersten Commit ändern möchte (der übrigens nicht getaggt ist, falls das etwas ändert). Also laufe ich git rebase --interactive --root
und markieren Sie einfach ‘Bearbeiten’ für das anfängliche Commit, ändern Sie etwas darin und git rebase --continue
. Jetzt wurden alle Commits in meinem Repository neu erstellt, daher haben sich ihre sha1 geändert. Die von mir erstellten Tags sind jedoch völlig unverändert und verweisen immer noch auf sha1 der vorherigen Commits.
Gibt es eine automatische Möglichkeit, die Tags auf die entsprechenden Commits zu aktualisieren, die beim Rebasing erstellt wurden?
Einige Leute schlagen vor, zu verwenden git filter-branch --tag-name-filter cat -- --tags
aber das warnt mich zuerst, dass alle meine Tags unverändert sind, und sagt dann, dass alle meine Tags in sich selbst geändert wurden (gleicher Tag-Name und gleicher Commit-Hash). Und weiterhin, git show --tags
sagt, dass die Tags immer noch auf die alten Commits zeigen.
In gewisser Weise ist es zu spät (aber warten Sie, es gibt gute Neuigkeiten). Die filter-branch
code ist in der Lage, die Tags anzupassen, da er während seiner Filterung eine Zuordnung von old-sha1 zu new-sha1 beibehält.
Eigentlich beides filter-branch
und rebase
Verwenden Sie die gleiche Grundidee, nämlich dass jeder Commit ist kopiert, indem Sie den ursprünglichen Inhalt erweitern, alle gewünschten Änderungen vornehmen und dann aus dem Ergebnis einen neuen Commit erstellen. Das bedeutet, dass es bei jedem Kopierschritt trivial ist, das Paar in eine Datei zu schreiben, und wenn Sie fertig sind, reparieren Sie Referenzen, indem Sie new-sha1 von ihrem old-sha1 nachschlagen . Sobald alle Verweise fertig sind, sind Sie an die neue Nummerierung gebunden und entfernen die Zuordnung.
Die Karte ist inzwischen verschwunden, daher “in gewisser Weise ist es zu spät”.
Zum Glück ist es noch nicht zu spät. 🙂 Ihr Rebase ist wiederholbar, oder zumindest die wichtigsten Teile davon sind es wahrscheinlich. Wenn Ihr Rebase einfach genug war, müssen Sie es möglicherweise überhaupt nicht wiederholen.
Schauen wir uns den „Wiederholungs“-Gedanken an. Wir haben einen ursprünglichen Graphen G von beliebiger Form:
o--o
/ \
o--o--o---o--o <-- branch-tip
\ /
o--o--o--o
(Wow, eine fliegende Untertasse!). Wir haben eine gemacht git rebase --root
darauf (einen Teil davon), Kopieren (einiger oder aller) Commits (Mergen beibehalten oder nicht), um einen neuen Graphen G’ zu erhalten:
o--o--o--o <-- branch-tip
/
/ o--o
/ / \
o--o--o---o--o
\ /
o--o--o--o
Ich habe diese gemeinsame Nutzung nur des ursprünglichen Wurzelknotens gezeichnet (und jetzt ist es ein Segelboot mit einem Kran darauf anstelle einer fliegenden Untertasse). Es kann mehr oder weniger geteilt werden. Einige der alten Knoten wurden möglicherweise vollständig unreferenziert und wurden daher in den Garbage Collection gesammelt (wahrscheinlich nicht: Die Reflogs sollten alle ursprünglichen Knoten mindestens 30 Tage lang am Leben erhalten). Aber auf jeden Fall haben wir immer noch Tags, die auf einen “alten G-Teil” von G’ und verweisen jene Referenzen garantieren das jene Knoten und alle ihre Eltern befinden sich immer noch im neuen G’.
Wenn wir also wissen, wie die ursprüngliche Rebase durchgeführt wurde, können wir sie auf dem Teilgraphen von G’ wiederholen, der der wichtige Teil von G ist. Wie schwer oder einfach dies ist und welche Befehle dafür verwendet werden müssen , hängen davon ab, ob das gesamte ursprüngliche G in G’ enthalten ist, was der Rebase-Befehl war, wie viel G’ das ursprüngliche G überlagert und mehr (seit git rev-list
, was unser Schlüssel zum Erhalten einer Liste von Knoten ist, hat wahrscheinlich keine Möglichkeit, zwischen “ursprünglichen, war-in-G”- und “neu in G'”-Knoten zu unterscheiden). Aber es kann wahrscheinlich getan werden: Es ist an dieser Stelle nur eine kleine Frage der Programmierung.
Wenn Sie es wiederholen, möchten Sie dieses Mal die Abbildung beibehalten, insbesondere wenn der resultierende Graph G” G’ nicht vollständig überlappt, denn was Sie jetzt brauchen, ist nicht die Abbildung selbst, sondern a Projektion dieser Karte, von G nach G’.
Wir geben einfach jedem Knoten im ursprünglichen G eine eindeutige relative Adresse (z. B. „vom Tipp, finde Eltern-Commit Nr. 2; von diesem Commit, finde Eltern-Commit Nr. 1; von diesem Commit …“) und finden dann das entsprechende relative Adresse in G”. Dadurch können wir die kritischen Teile der Karte neu erstellen.
Abhängig von der Einfachheit der ursprünglichen Rebase können wir möglicherweise direkt zu dieser Phase springen. Wenn wir beispielsweise sicher wissen, dass der gesamte Graph ohne Abflachung kopiert wurde (so dass wir zwei unabhängige fliegende Untertassen haben), dann die relative Adresse für Tag T
in G ist die relative Adresse, die wir in G’ wollen, und jetzt ist es trivial, diese relative Adresse zu verwenden, um ein neues Tag zu erstellen, das auf das kopierte Commit zeigt.
Großes Update basierend auf neuen Informationen
Mit der zusätzlichen Information, dass der ursprüngliche Graph vollständig linear war und dass wir jeden Commit kopiert haben, können wir eine sehr einfache Strategie anwenden. Wir müssen die Karte immer noch rekonstruieren, aber jetzt ist es einfach, da jeder alte Commit genau einen neuen Commit hat, der einen gewissen linearen Abstand (der leicht als einzelne Zahl dargestellt werden kann) von jedem Ende des ursprünglichen Diagramms hat (ich werde Abstand-von-Spitze verwenden).
Das heißt, der alte Graph sieht so aus, mit nur einem Zweig:
A <- B <- C ... <- Z <-- master
Die Tags zeigen einfach auf einen der Commits (über ein annotiertes Tag-Objekt), z. B. vielleicht tag foo
zeigt auf ein annotiertes Tag-Objekt, das auf Commit zeigt W
. Das merken wir dann W
ist vier Commits zurück von Z
.
Das neue Diagramm sieht genauso aus, außer dass jeder Commit durch seine Kopie ersetzt wurde. Nennen wir diese A'
, B'
und so weiter, durch Z'
. Die (einzelne) Verzweigung zeigt auf den am weitesten oben liegenden Commit, d. h. Z'
. Wir möchten das ursprüngliche Tag anpassen foo
sodass wir ein neues annotiertes Tag-Objekt haben, das auf zeigt W'
.
Wir benötigen die SHA-1-ID des ursprünglichen Commits mit der höchsten Spitze. Dies sollte im Reflog für den (einzelnen) Zweig leicht zu finden sein und ist wahrscheinlich einfach [email protected]{1}
(obwohl das davon abhängt, wie oft Sie den Zweig seitdem optimiert haben; und wenn Sie seit dem Rebasing neue Commits hinzugefügt haben, müssen wir diese ebenfalls berücksichtigen). Es kann durchaus auch in der speziellen ref sein ORIG_HEAD
welcher git rebase
hinterlässt, falls Sie entscheiden, dass Ihnen das Rebase-Ergebnis nicht gefällt.
Nehmen wir das an [email protected]{1}
die richtige ID ist und dass es keine solchen neuen Commits gibt. Dann:
orig_master=$(git rev-parse [email protected]{1})
würde diese ID in speichern $orig_master
.
Wenn wir die vollständige Karte erstellen wollten, würde dies tun:
$ git rev-list $orig_master > /tmp/orig_list
$ git rev-list master > /tmp/new_list
$ wc -l /tmp/orig_list /tmp/new_list
(Die Ausgabe für beide Dateien sollte gleich sein; wenn nicht, ist hier eine Annahme falsch gelaufen; in der Zwischenzeit lasse ich Shell weg $
Präfix auch unten, da der Rest wirklich in ein Skript gehen sollte, auch für den einmaligen Gebrauch, im Falle von Tippfehlern und Notwendigkeit für Optimierungen)
exec 3 < /tmp/orig_list 4 < /tmp/new_list
while read orig_id; do
read new_id <& 4; echo $orig_id $new_id;
done <& 3 > /tmp/mapping
(Dies, ziemlich ungetestet, soll die beiden Dateien zusammenfügen – eine Art Shell-Version von Python zip
auf den beiden Listen – um die Zuordnung zu erhalten). Aber wir brauchen die Kartierung nicht wirklich, wir brauchen nur diese “Entfernung von der Spitze”-Zählungen, also werde ich so tun, als hätten wir uns hier nicht darum gekümmert.
Jetzt müssen wir über alle Tags iterieren:
# We don't want a pipe here because it's
# not clear what happens if we update an existing
# tag while `git for-each-ref` is still running.
git for-each-ref refs/tags > /tmp/all-tags
# it's also probably a good idea to copy these
# into a refs/original/refs/tags name space, a la
# git filter-branch.
while read sha1 objtype tagname; do
git update-ref -m backup refs/original/$tagname $sha1
done < /tmp/all-tags
# now replace the old tags with new ones.
# it's easy to handle lightweight tags too.
while read sha1 objtype tagname; do
case $objtype in
tag) adj_anno_tag $sha1 $tagname;;
commit) adj_lightweight_tag $sha1 $tagname;;
*) echo "error: shouldn't have objtype=$objtype";;
esac
done < /tmp/all-tags
Wir müssen die beiden noch schreiben adj_anno_tag
und adj_lightweight_tag
Shell-Funktionen. Lassen Sie uns zunächst eine Shell-Funktion schreiben, die die neue ID aus der alten ID erzeugt, dh die Zuordnung nachschlägt. Wenn wir eine echte Zuordnungsdatei verwenden würden, würden wir nach dem ersten Eintrag grep oder awk suchen und dann den zweiten ausgeben. Was wir jedoch mit der schäbigen Single-Old-File-Methode wollen, ist die Zeilennummer der passenden ID, die wir bekommen können grep -n
:
map_sha1() {
local grep_result line
grep_result=$(grep -n $1 /tmp/orig_list) || {
echo "WARNING: ID $1 is not mapped" 1>&2
echo $1
return 1
}
# annoyingly, grep produces "4:matched-text"
# on a match. strip off the part we don't want.
line=${grep_result%%:*}
# now just get git to spit out the ID of the (line - 1)'th
# commit before the tip of the current master. the "minus
# one" part is because line 1 represents master~0, line 2
# is master~1, and so on.
git rev-parse master~$((line - 1))
}
Der WARNING-Fall sollte niemals auftreten und die rev-parse-Funktion sollte niemals fehlschlagen, aber wir sollten wahrscheinlich den Rückgabestatus dieser Shell-Funktion überprüfen.
Der leichtgewichtige Tag-Updater ist jetzt ziemlich trivial:
adj_lightweight_tag() {
local old_sha1=$1 new_sha1 tag=$2
new_sha1=$(map_sha1 $old_sha1) || return
git update-ref -m remap $tag $new_sha1 $old_sha1
}
Das Aktualisieren eines annotierten Tags ist schwieriger, aber wir können Code stehlen git filter-branch
. Ich werde hier nicht alles zitieren; Stattdessen gebe ich Ihnen nur dieses Bit:
$ vim $(git --exec-path)/git-filter-branch
und diese Anweisungen: Suche nach dem zweiten Vorkommen von git for-each-ref
und beachten Sie die git cat-file
geleitet zu sed
mit dem Ergebnis übergeben an git mktag
wodurch die Shell-Variable festgelegt wird new_sha1
.
Das brauchen wir, um das Tag-Objekt zu kopieren. Die neue Kopie muss auf das Objekt verweisen, das mithilfe von $(map_sha1) auf dem Commit gefunden wurde, auf das das alte Tag verwies. Wir können dieses Commit auf die gleiche Weise finden filter-branch
tut, mit git rev-parse $old_sha1^{commit}
.
(Übrigens, beim Schreiben dieser Antwort und beim Betrachten des filter-branch-Skripts fällt mir ein, dass es einen Fehler im filter-branch gibt, den wir in unseren Post-Rebase-Tag-Fixup-Code importieren werden: wenn ein vorhandenes annotiertes Tag zeigt zu einem anderen Tag, wir reparieren es nicht. Wir reparieren nur leichtgewichtige Tags und Tags, die direkt auf Commits verweisen.)
Beachten Sie, dass keiner der obigen Beispielcodes tatsächlich getestet wurde, und die Umwandlung in ein allgemeineres Skript (das beispielsweise nach jedem Rebase ausgeführt werden könnte oder noch besser in das interaktive Rebase selbst integriert werden könnte) erfordert eine ganze Menge Zusätzliche Arbeit.
Haben Sie Ihre Tags mit jemand anderem geteilt, zB von
push
sie in ein gemeinsam genutztes Repository übertragen?– Chris
5. November 2015 um 1:59 Uhr
@Chris: Nein, habe ich nicht.
– Benutzer4256966
5. November 2015 um 20:58 Uhr