Wie kann man in Git „Tags rebasen“?

Lesezeit: 13 Minuten

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.

  • Haben Sie Ihre Tags mit jemand anderem geteilt, zB von pushsie 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

Toreks Benutzeravatar
Torek

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_HEADwelcher 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-refund beachten Sie die git cat-file geleitet zu sed mit dem Ergebnis übergeben an git mktagwodurch 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.

  • Danke für deine Antwort! Ich denke, es ist ziemlich allgemein, aber mein spezielles Szenario ist wirklich einfach. Ich habe meine Antwort aktualisiert, um weitere Details bereitzustellen. Mein Problem ist nicht so sehr die Zuordnung von alten zu neuen sha1s — ich könnte das sogar manuell machen (obwohl das unpraktisch wäre, wenn die Anzahl der Tags groß wäre). Mein eigentliches Problem besteht darin, Tags auf die neuen Commits verweisen zu lassen, die im Rebase-Prozess erstellt wurden, ohne irgendetwas anderes im Tag zu ändern (Datum, Nachricht usw.). Und natürlich müsste ein automatischer Weg, dies zu tun, wahrscheinlich die oben erwähnte Zuordnung kennen.

    Benutzer4256966

    5. November 2015 um 20:58 Uhr


  • OK, es hört sich so an, als ob Ihre ursprüngliche Struktur vollständig linear war (keine Zusammenführungen, um die Sie sich Sorgen machen müssen). und Sie haben alle ursprünglichen Commits beibehalten, was die “relative Adressierung” trivial macht: Der Abstand vom alten Tipp zum Tag ist derselbe wie der Abstand vom neuen Tipp zum Zielort des Tags. Das verbleibende Hauptproblem ist, ob es sich um annotierte Tags oder um Lightweight-Tags handelt.

    – Torek

    5. November 2015 um 21:27 Uhr


  • Ja, du hast es auf den Punkt gebracht. Sie sind annotierte Tags. Wie wichtig ist das?

    Benutzer4256966

    5. November 2015 um 22:10 Uhr

  • Annotierte Tags sind tatsächliche Objekte, daher müssen sie mit den angepassten Commit-IDs kopiert (oder neu erstellt) werden. Wenn sie signiert sind, ist es wahrscheinlich am einfachsten, sie von Grund auf neu zu erstellen, anstatt sie zu kopieren. (Dann muss in beiden Fällen ein Lightweight-Tag erstellt werden, das irgendwo hinzeigt, in diesem Fall auf das neue annotierte Tag-Objekt; bei einfachen Lightweight-Tags verweisen wir das Lightweight-Tag auf den Rebase-Commit.) Ich komme zurück dazu später, ich habe gerade eine Besorgung…

    – Torek

    5. November 2015 um 22:37 Uhr

  • Ich verstehe. In meinem speziellen Fall sind sie nicht signiert. Ist es nicht möglich, die vorhandenen annotierten Tags einfach auf die neuen Commit-Objekte verweisen zu lassen? (Keine Eile, und danke fürs Nachfassen).

    Benutzer4256966

    5. November 2015 um 22:57 Uhr

Sie können verwenden git rebasetags

Sie verwenden genau so, wie Sie es verwenden würden git rebase


git rebasetags <rebase args>

Falls die Rebase interaktiv ist, wird Ihnen eine Bash-Shell angezeigt, in der Sie die Änderungen vornehmen können. Beim Verlassen dieser Shell werden die Tags wiederhergestellt.

Geben Sie hier die Bildbeschreibung ein

Von dieser Beitrag

Benutzeravatar von Laura A. Rivera
Laura A. Rivera

Dank Toreks ausführlicher Anleitung habe ich eine Implementierung zusammengestellt.

#!/usr/bin/env bash
set -eo pipefail

orig_master="$(git rev-parse ORIG_HEAD)"

sane_grep () {
    GREP_OPTIONS= LC_ALL=C grep "[email protected]"
}

map_sha1() {
    local result line

    # git rev-list $orig_master > /tmp/orig_list
    result="$(git rev-list "${orig_master}" | sane_grep -n "$1" || {
        echo "WARNING: ID $1 is not mapped" 1>&2
        return 1
    })"

    if [[ -n "${result}" ]]
    then
        # annoyingly, grep produces "4:matched-text"
        # on a match.  strip off the part we don't want.
        result=${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~$((result - 1))
    fi
}

adjust_lightweight_tag () {
    local old_sha1=$1 new_sha1 tag=$2

    new_sha1=$(map_sha1 "${old_sha1}")

    if [[ -n "${new_sha1}" ]]
    then
        git update-ref "${tag}" "${new_sha1}"
    fi
}

die () {
    echo "$1"
    exit 1
}

adjust_annotated_tag () {
    local sha1t=$1
    local ref=$2
    local tag="${ref#refs/tags/}"

    local sha1="$(git rev-parse -q "${sha1t}^{commit}")"
    local new_sha1="$(map_sha1 "${sha1}")"

    if [[ -n "${new_sha1}" ]]
    then
        local new_sha1=$(
            (
                printf 'object %s\ntype commit\ntag %s\n' \
                        "$new_sha1" "$tag"
                git cat-file tag "$ref" |
                sed -n \
                        -e '1,/^$/{
                    /^object /d
                    /^type /d
                    /^tag /d
                    }' \
                        -e '/^-----BEGIN PGP SIGNATURE-----/q' \
                        -e 'p'
            ) | git mktag
        ) || die "Could not create new tag object for $ref"

        if git cat-file tag "$ref" | \
                sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1
        then
            echo "gpg signature stripped from tag object $sha1t"
        fi

        echo "$tag ($sha1 -> $new_sha1)"
        git update-ref "$ref" "$new_sha1"
    fi
}

git for-each-ref --format="%(objectname) %(objecttype) %(refname)" refs/tags |
while read sha1 type ref
do
    case $type in
    tag)
        adjust_annotated_tag "${sha1}" "${ref}" || true
        ;;
    commit)
        adjust_lightweight_tag "${sha1}" "${ref}" || true
        echo
        ;;
    *)
        echo "ERROR: unknown object type ${type}"
        ;;
    esac
done

1439790cookie-checkWie kann man in Git „Tags rebasen“?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy