Establecer el puntero padre git a un padre diferente

220

Si tengo un compromiso en el pasado que apunta a uno de los padres, pero quiero cambiar el padre al que apunta, ¿cómo lo haría?

dougvk
fuente

Respuestas:

404

Utilizando git rebase . Es el comando genérico "tomar commit (s) y colocarlo en un comando padre (base) diferente en Git.

Algunas cosas que debes saber, sin embargo:

  1. Dado que los SHA de compromiso involucran a sus padres, cuando cambia el padre de un commit dado, su SHA cambiará, al igual que los SHA de todos los commits que vienen después (más reciente que él) en la línea de desarrollo.

  2. Si está trabajando con otras personas y ya ha llevado el compromiso público en cuestión a donde lo han llevado, modificar el compromiso es probablemente una mala idea ™. Esto se debe al n. ° 1 y, por lo tanto, a la confusión resultante que los repositorios de otros usuarios encontrarán al tratar de descubrir qué sucedió debido a que sus SHA ya no coinciden con los de los "mismos" compromisos. (Consulte la sección "RECUPERACIÓN DE LA REEMBOLSO DE UPSTREAM" de la página de manual vinculada para obtener más detalles).

Dicho esto, si actualmente se encuentra en una rama con algunas confirmaciones que desea mover a un nuevo padre, se vería así:

git rebase --onto <new-parent> <old-parent>

Eso moverá todo después <old-parent> de la rama actual para sentarse en su <new-parent>lugar.

Ámbar
fuente
Estoy usando git rebase --onto para cambiar de padres, pero parece que termino con un solo commit. Le hice una pregunta: stackoverflow.com/questions/19181665/…
Eduardo Mello
77
¡Ajá, entonces para eso --ontose usa! La documentación siempre ha sido completamente oscura para mí, ¡ incluso después de leer esta respuesta!
Daniel C. Sobral
66
Esta línea: git rebase --onto <new-parent> <old-parent>Me contó todo sobre cómo usar rebase --ontoque cualquier otra pregunta y documento que he leído hasta ahora no pudo.
jpmc26
3
Para mí, este comando debería leer:, rebase this commit on that oneo git rebase --on <new-parent> <commit>. Usar el viejo padre aquí no tiene sentido para mí.
Samir Aguiar
1
@kopranb El viejo padre es importante cuando quieres descartar parte del camino desde tu cabeza hasta la cabeza remota. El caso que está describiendo es manejado por git rebase <new-parent>.
clacke
35

Si resulta que necesita evitar volver a modificar las confirmaciones posteriores (por ejemplo, porque una reescritura del historial sería insostenible), puede usar el reemplazo de git (disponible en Git 1.6.5 y posterior).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

Una vez establecido el reemplazo anterior, cualquier solicitud del objeto B en realidad devolverá el objeto C. El contenido de C es exactamente el mismo que el contenido de B, excepto el primer padre (los mismos padres (excepto el primero), el mismo árbol, mismo mensaje de confirmación).

Los reemplazos están activos de manera predeterminada, pero se pueden desactivar utilizando la --no-replace-objectsopción git (antes del nombre del comando) o configurando la GIT_NO_REPLACE_OBJECTSvariable de entorno. Los reemplazos se pueden compartir presionando refs/replace/*(además de lo normalrefs/heads/* ).

Si no te gusta el commit-munging (hecho con sed arriba), entonces puedes crear tu commit de reemplazo usando comandos de nivel superior:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

La gran diferencia es que esta secuencia no propaga los padres adicionales si B es una confirmación de fusión.

Chris Johnsen
fuente
Y luego, ¿cómo empujaría los cambios a un repositorio existente? Parece que funciona localmente, pero git push no empuja nada después de eso
Baptiste Wicht
2
@BaptisteWicht: los reemplazos se almacenan en un conjunto separado de referencias. Tendrá que organizar para empujar (y buscar) la refs/replace/jerarquía de referencia. Si solo necesita hacerlo una vez, puede hacerlo git push yourremote 'refs/replace/*'en el repositorio de origen y git fetch yourremote 'refs/replace/*:refs/replace/*'en los repositorios de destino. Si necesita hacerlo varias veces, podría agregar esas especificaciones a una remote.yourremote.pushvariable de configuración (en el repositorio de origen) y una remote.yourremote.fetchvariable de configuración (en los repositorios de destino).
Chris Johnsen
3
Una forma más sencilla de hacer esto es usar git replace --grafts. No estoy seguro de cuándo se agregó esto, pero su propósito es crear un reemplazo que sea igual que commit B pero con los padres especificados.
Paul Wagland
32

Tenga en cuenta que cambiar un commit en Git requiere que todos los commits que le siguen también tengan que cambiarse. Esto se desaconseja si ha publicado esta parte de la historia, y alguien podría haber construido su trabajo sobre la historia que era antes del cambio.

Una solución alternativa a la git rebasemencionada en la respuesta de Amber es usar un mecanismo de injertos (consulte la definición de injertos Git en el Glosario de Git y la documentación del .git/info/graftsarchivo en la documentación de Diseño del repositorio de Git ) para cambiar el elemento primario de una confirmación, verifique que funcionó correctamente con algún visor de historial ( gitk, git log --graph, etc.) y luego usar git filter-branch(como se describe en la sección "Ejemplos" de su página de manual) para hacerlo permanente (y luego eliminar el injerto, y opcionalmente eliminar las referencias originales respaldadas por git filter-branch, o repositorio reclinado):

echo "$ commit-id $ graft-id" >> .git / info / grafts
git filter-branch $ graft-id..HEAD

NOTA !!! ¡Esta solución es diferente de la solución de rebase en que git rebasecambiaría los cambios de rebase / trasplante , mientras que la solución basada en injertos simplemente reparent commits como es , sin tener en cuenta las diferencias entre el padre antiguo y el padre nuevo!

Jakub Narębski
fuente
3
Por lo que entiendo (de git.wiki.kernel.org/index.php/GraftPoint ), git replaceha reemplazado a git grafts (suponiendo que tenga git 1.6.5 o posterior).
Alexander Bird
44
@ Thr4wn: En gran medida cierto, pero si desea reescribir el historial, la forma de hacerlo es injertos + git-filter-branch (o rebase interactivo, o exportación rápida + eg reposurgeon). Si desea / necesita preservar el historial , entonces git-replace es muy superior a los injertos.
Jakub Narębski
@ JakubNarębski, ¿podría explicar qué quiere decir reescribir vs preservar el historial? ¿Por qué no puedo usar git-replace+ git-filter-branch? En mi prueba, filter-branchparecía respetar el reemplazo, rebatir todo el árbol.
cdunn2001
1
@ cdunn2001: git filter-branchreescribe el historial, respetando los injertos y los reemplazos (pero en el historial reescrito los reemplazos serán discutibles); empujar resultará en un cambio no hacia adelante sin fasf. git replacecrea reemplazos transferibles en el lugar; push será un avance rápido, pero necesitaría presionar refs/replacepara transferir reemplazos para tener un historial corregido. HTH
Jakub Narębski
23

Para aclarar las respuestas anteriores y descaradamente enchufar mi propio script:

Depende de si desea "rebase" o "reparent". Un rebase , como lo sugiere Amber , se mueve por los diffs . Un reparent , como lo sugirieron Jakub y Chris , se mueve alrededor de las instantáneas de todo el árbol. Si desea realizar reparent, sugiero usar en git reparentlugar de hacer el trabajo manualmente.

Comparación

Suponga que tiene la imagen a la izquierda y desea que se vea como la imagen a la derecha:

                   C'
                  /
A---B---C        A---B---C

Tanto el rebase como el reparenting producirán la misma imagen, pero la definición de C'difiere. Con git rebase --onto A B, C'no contendrá ningún cambio introducido por B. Con git reparent -p A, C'será idéntico a C(excepto que Bno estará en el historial).

Mark Lodato
fuente
1
+1 He experimentado con el reparentguión; funciona de maravilla; Muy recomendable . También recomendaría crear un nuevobranch etiqueta, y para checkoutesa rama antes de llamar (ya que la etiqueta de la rama se moverá).
Ruibarbo
También vale la pena señalar que: (1) el nodo original permanece intacto, y (2) el nuevo nodo no hereda los hijos del nodo original.
Ruibarbo
0

Definitivamente, la respuesta de @ Jakub me ayudó hace unas horas cuando intentaba exactamente lo mismo que el OP.
Sin embargo, git replace --graftahora es la solución más fácil con respecto a los injertos. Además, un problema importante con esa solución fue que la rama de filtro me hizo perder cada rama que no se fusionó con la rama de HEAD. Entonces, git filter-repohizo el trabajo perfectamente y sin problemas.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Para más información: consulte la sección "Re-injerto de historia" en los documentos

Teodoro
fuente