¿Cómo “rebase las etiquetas” en git?

14 minutos de lectura

Supongamos que tengo el siguiente repositorio git simple: una sola rama, algunas confirmaciones una tras otra, un par de ellas han sido etiquetadas (con anotado tags) después de confirmar cada uno de ellos, y luego, un día, decido que quiero cambiar el primer compromiso (que, por cierto, no está etiquetado, si eso cambia algo). así que corro git rebase --interactive --root y simplemente marque ‘editar’ para la confirmación inicial, cambie algo en él y git rebase --continue. Ahora todas las confirmaciones en mi repositorio han sido recreadas, por lo tanto, sus sha1 han cambiado. Sin embargo, las etiquetas que creé no han cambiado por completo y siguen apuntando al sha1 de las confirmaciones anteriores.

¿Existe una forma automática de actualizar las etiquetas a las confirmaciones correspondientes creadas al reorganizar?

Algunas personas sugieren usar git filter-branch --tag-name-filter cat -- --tags pero eso primero me advierte que cada una de mis etiquetas no ha cambiado y luego dice que cada una de mis etiquetas se cambia a sí misma (mismo nombre de etiqueta y mismo hash de confirmación). Y todavía, git show --tags dice que las etiquetas aún apuntan a las confirmaciones anteriores.

  • ¿Ha compartido sus etiquetas con alguien más, por ejemplo, por pushenviarlos a un repositorio compartido?

    – Chris

    5 de noviembre de 2015 a las 1:59

  • @Chris: No, no lo he hecho.

    usuario4256966

    5 de noviembre de 2015 a las 20:58

avatar de usuario de torek
Torek

En cierto sentido, es demasiado tarde (pero espera, hay buenas noticias). los filter-branch El código puede ajustar las etiquetas porque mantiene, durante su filtrado, un mapeo de old-sha1 a new-sha1.

De hecho, ambos filter-branch y rebase usa la misma idea básica, que es que cada compromiso es copiado, expandiendo el contenido original, haciendo los cambios deseados y luego haciendo una nueva confirmación del resultado. Esto significa que durante cada paso de copia es trivial escribir el par en un archivo, y luego, una vez que haya terminado, arregla las referencias buscando el nuevo-sha1 de su antiguo-sha1 . Una vez que haya terminado con todas las referencias, estará comprometido con la nueva numeración y eliminará la asignación.

El mapa ya no está, por lo tanto, “en cierto sentido, es demasiado tarde”.

Por suerte, no es demasiado tarde. 🙂 Su rebase es repetible, o al menos, las partes clave probablemente lo sean. Además, si su rebase fue lo suficientemente simple, es posible que no necesite repetirlo en absoluto.

Veamos el pensamiento “repetir”. Tenemos un gráfico original G de alguna forma arbitraria:

     o--o
    /    \
o--o--o---o--o   <-- branch-tip
 \          /
  o--o--o--o

(¡vaya, un platillo volador!). hemos hecho un git rebase --root en (alguna parte de) él, copiando (algunos o todos) los compromisos (preservando las fusiones o no) para obtener un nuevo gráfico G ‘:

    o--o--o--o   <-- branch-tip
   /
  /  o--o
 /  /    \
o--o--o---o--o
 \          /
  o--o--o--o

Dibujé esto compartiendo solo el nodo raíz original (y ahora es un velero con una grúa en él, en lugar de un platillo volador). Puede haber más compartir, o menos. Es posible que algunos de los nodos antiguos hayan quedado completamente sin referencia y, por lo tanto, hayan sido recolectados como basura (probablemente no: los registros de referencia deberían mantener todos los nodos originales vivos durante al menos 30 días). Pero en cualquier caso, todavía tenemos etiquetas que apuntan a alguna “parte G antigua” de G’, y aquellos Las referencias garantizan que aquellos los nodos, y todos sus padres, todavía están en el nuevo G’.

Por lo tanto, si sabemos cómo se hizo el rebase original, podemos repetirlo en el subgráfico de G’ que es la parte importante de G. Qué tan difícil o fácil es esto y qué comando(s) usar para hacerlo , depende de si toda la G original está en G’, cuál fue el comando de rebase, cuánto G’ se superpone a la G original y más (ya que git rev-list, que es nuestra clave para obtener una lista de nodos, probablemente no tenga forma de distinguir entre nodos “originales, estaba en G” y “nuevos en G'”). Pero probablemente se pueda hacer: es solo una pequeña cuestión de programación, en este punto.

Si lo repite, esta vez querrá mantener el mapeo, especialmente si el gráfico resultante G” no se superpone completamente a G’, porque lo que necesita ahora no es el mapa en sí, sino un proyección de este mapa, de G a G’.

Simplemente le damos a cada nodo en el G original una dirección relativa única (por ejemplo, “desde la punta, encuentre la confirmación principal n.º 2; desde esa confirmación, busque la confirmación principal n.º 1; desde esa confirmación…”) y luego busque la correspondiente dirección relativa en G”. Esto nos permite reconstruir las partes críticas del mapa.

Dependiendo de la simplicidad del rebase original, podríamos saltar directamente a esta fase. Por ejemplo, si sabemos con certeza que todo el gráfico se copió sin aplanarlo (de modo que tengamos dos platillos voladores independientes), entonces la dirección relativa de la etiqueta T en G es la dirección relativa que queremos en G’, y ahora es trivial usar esa dirección relativa para crear una nueva etiqueta que apunte a la confirmación copiada.

Gran actualización basada en nueva información.

Usando la información adicional de que el gráfico original era completamente lineal y que hemos copiado cada confirmación, podemos usar una estrategia muy simple. Todavía necesitamos reconstruir el mapa, pero ahora es fácil, ya que cada confirmación anterior tiene exactamente una confirmación nueva, que tiene cierta distancia lineal (que es fácil de representar como un solo número) desde cualquier extremo del gráfico original (lo haré utilice la distancia desde la punta).

Es decir, el gráfico anterior se ve así, con una sola rama:

A <- B <- C ... <- Z   <-- master

Las etiquetas simplemente apuntan a una de las confirmaciones (a través de un objeto de etiqueta anotado), por ejemplo, quizás la etiqueta foo apunta a un objeto de etiqueta anotada que apunta a confirmar W. Entonces notamos que W son cuatro confirmaciones de Z.

El nuevo gráfico se ve exactamente igual, excepto que cada confirmación se reemplazó con su copia. Llamemos a estos A', B'y así sucesivamente, a través de Z'. La rama (única) apunta a la confirmación más importante, es decir, Z'. Querremos ajustar la etiqueta original. foo para que tengamos un nuevo objeto de etiqueta anotada apuntando a W'.

Necesitaremos el ID SHA-1 de la confirmación tip-most original. Esto debería ser fácil de encontrar en el registro de referencia para la rama (única), y probablemente sea simplemente master@{1} (aunque eso depende de cuántas veces haya modificado la rama desde entonces; y si hay nuevas confirmaciones que agregó desde la reorganización, también debemos tenerlas en cuenta). También puede estar en la referencia especial. ORIG_HEADcual git rebase deja atrás en caso de que decidas que no te gusta el resultado de rebase.

Supongamos que master@{1} es el ID correcto y que no hay nuevas confirmaciones. Después:

orig_master=$(git rev-parse master@{1})

guardaría este ID en $orig_master.

Si quisiéramos construir el mapa completo, esto sería suficiente:

$ git rev-list $orig_master > /tmp/orig_list
$ git rev-list master > /tmp/new_list
$ wc -l /tmp/orig_list /tmp/new_list

(la salida para ambos archivos debería ser la misma; si no, alguna suposición aquí ha salido mal; mientras tanto, dejaré de lado el shell $ prefijo también, a continuación, ya que el resto de esto realmente debería ir en una secuencia de comandos, incluso para un solo uso, en caso de errores tipográficos y necesidad de ajustes)

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

(esto, bastante no probado, está destinado a pegar los dos archivos juntos, una especie de versión shell de Python zip en las dos listas—para obtener el mapeo). Pero en realidad no necesitamos el mapeo, todo lo que necesitamos son esos conteos de “distancia desde la punta”, así que voy a fingir que no nos molestamos aquí.

Ahora necesitamos iterar sobre todas las etiquetas:

# 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

Todavía tenemos que escribir los dos adj_anno_tag y adj_lightweight_tag funciones de caparazón. Primero, sin embargo, escribamos una función de shell que produzca la nueva ID dada la antigua ID, es decir, busque el mapeo. Si usáramos un archivo de mapeo real, haríamos grep o awk para la primera entrada, luego imprimiríamos la segunda. Sin embargo, usando el método sórdido de un solo archivo antiguo, lo que queremos es el número de línea del ID coincidente, que podemos obtener con 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))
}

El caso de ADVERTENCIA nunca debería ocurrir, y el rev-parse nunca debería fallar, pero probablemente deberíamos verificar el estado de retorno de esta función de shell.

El actualizador de etiquetas ligero ahora es bastante 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
}

Actualizar una etiqueta anotada es más difícil, pero podemos robar código de git filter-branch. No voy a citarlo todo aquí; en cambio, solo te doy este bit:

$ vim $(git --exec-path)/git-filter-branch

y estas instrucciones: busque la segunda ocurrencia de git for-each-refy tenga en cuenta la git cat-file canalizado a sed con el resultado pasado a git mktagque establece la variable de shell new_sha1.

Esto es lo que necesitamos para copiar el objeto de la etiqueta. La nueva copia debe apuntar al objeto encontrado usando $(map_sha1) en la confirmación a la que apuntaba la etiqueta anterior. Podemos encontrar ese compromiso de la misma manera. filter-branch hace, usando git rev-parse $old_sha1^{commit}.

(Por cierto, al escribir esta respuesta y mirar el script filter-branch, se me ocurre que hay un error en filter-branch, que importaremos a nuestro código de corrección de etiquetas posterior a la reorganización: si una etiqueta anotada existente apunta a otra etiqueta, no lo arreglamos. Solo arreglamos etiquetas ligeras y etiquetas que apuntan directamente a confirmaciones).

Tenga en cuenta que ninguno de los códigos de ejemplo anteriores se ha probado realmente, y convertirlo en un script de propósito más general (que podría ejecutarse después de cualquier rebase, por ejemplo, o mejor aún, incorporarse en el mismo rebase interactivo) requiere una buena cantidad de trabajo adicional.

  • ¡Gracias por tu respuesta! Creo que es bastante general, pero mi escenario particular es realmente simple. He actualizado mi respuesta para proporcionar más detalles. Mi problema no es tanto obtener el mapeo de los sha1 antiguos a los nuevos; incluso podría hacerlo manualmente (aunque eso no sería práctico si la cantidad de etiquetas fuera grande). Mi problema real es cómo hacer que las etiquetas apunten a las nuevas confirmaciones creadas en el proceso de rebase, sin cambiar nada más en la etiqueta (fecha, mensaje, etc.). Y, obviamente, una forma automática de hacerlo probablemente necesitaría conocer el mapeo antes mencionado.

    usuario4256966

    5 de noviembre de 2015 a las 20:58


  • OK, parece que su estructura original era completamente lineal (no hay fusiones de las que preocuparse) y retuvo todas las confirmaciones originales, lo que hace que el “direccionamiento relativo” sea trivial: la distancia desde la sugerencia anterior hasta la etiqueta es la misma que la distancia desde la sugerencia nueva hasta donde debe ir la etiqueta. El principal problema pendiente es si se trata de etiquetas anotadas o etiquetas ligeras.

    – torek

    5 de noviembre de 2015 a las 21:27


  • Sí, lo clavaste. Son etiquetas anotadas. ¿Cómo importa eso?

    usuario4256966

    5 de noviembre de 2015 a las 22:10

  • Las etiquetas anotadas son objetos reales, por lo que deben copiarse (o volver a crearse) con los ID de confirmación ajustados. Si están firmados, probablemente sea más fácil volver a crearlos desde cero, en lugar de copiarlos. (Entonces, en cualquier caso, hay una etiqueta liviana que se debe hacer para apuntar a algún lugar, en este caso, al nuevo objeto de etiqueta anotado; para las etiquetas livianas simples, apuntamos la etiqueta liviana a la confirmación de rebase). Volveré a esto mas tarde, tengo un mandado ahora mismo…

    – torek

    5 de noviembre de 2015 a las 22:37

  • Ya veo. En mi caso particular, no están firmados. ¿No es posible simplemente hacer que las etiquetas anotadas existentes apunten a los nuevos objetos de confirmación? (Sin prisas, y gracias por el seguimiento).

    usuario4256966

    5 de noviembre de 2015 a las 22:57

Puedes usar git rebasetags

Lo usas como lo usarías git rebase


git rebasetags <rebase args>

En caso de que la reorganización sea interactiva, se le presentará un shell de bash donde podrá realizar los cambios. Al salir de ese shell, se restaurarán las etiquetas.

ingrese la descripción de la imagen aquí

De esta publicación

Avatar de usuario de Laura A. Rivera
Laura A. Rivera

Gracias al recorrido detallado de torek, armé una implementación.

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

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

sane_grep () {
    GREP_OPTIONS= LC_ALL=C grep "$@"
}

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

¿Ha sido útil esta solución?