Las confirmaciones de Git se duplican en la misma rama después de hacer un rebase


130

Entiendo el escenario presentado en Pro Git sobre The Perils of Rebasing . El autor básicamente te dice cómo evitar confirmaciones duplicadas:

No reescriba las confirmaciones que ha enviado a un repositorio público.

Voy a contarte mi situación particular porque creo que no se ajusta exactamente al escenario Pro Git y todavía termino con confirmaciones duplicadas.

Digamos que tengo dos sucursales remotas con sus contrapartes locales:

origin/master    origin/dev
|                |
master           dev

Las cuatro ramas contienen los mismos commits y voy a comenzar el desarrollo en dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Después de un par de confirmaciones, empujo los cambios a origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Tengo que volver mastera hacer una solución rápida:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

Y volviendo a devredactar los cambios para incluir la solución rápida en mi desarrollo real:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Si visualizo el historial de confirmaciones con GitX / gitk, noto que origin/devahora contiene dos confirmaciones idénticas C5'y C6'que son diferentes a Git. Ahora si presiono los cambios a origin/deveste es el resultado:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Tal vez no entiendo completamente la explicación en Pro Git, por lo que me gustaría saber dos cosas:

  1. ¿Por qué Git duplica estos commits mientras rebase? ¿Hay alguna razón particular para hacer eso en lugar de simplemente aplicar C5y C6después C7?
  2. ¿Cómo puedo evitar eso? ¿Sería prudente hacerlo?

Respuestas:


86

No debería usar rebase aquí, una simple fusión será suficiente. El libro Pro Git que vinculó básicamente explica esta situación exacta. El funcionamiento interno puede ser ligeramente diferente, pero así es como lo visualizo:

  • C5y C6son retirados temporalmente dedev
  • C7 es aplicado a dev
  • C5y C6se reproducen encima de C7, creando nuevos diffs y por lo tanto nuevos commits

Entonces, en su devrama, C5y C6efectivamente ya no existen: son ahora C5'y C6'. Cuando presionas origin/dev, git ve C5'y C6'como nuevos commits y los agrega al final de la historia. De hecho, si observa las diferencias entre C5y C5'dentro origin/dev, notará que aunque el contenido es el mismo, los números de línea son probablemente diferentes, lo que hace que el hash del commit sea diferente.

Replantearé la regla Pro Git: nunca renuncies los commits que hayan existido en otro lugar que no sea tu repositorio local . Use merge en su lugar.


Tengo el mismo problema, cómo puedo arreglar mi historial de sucursal remota ahora, ¿hay alguna otra opción que no sea eliminar la sucursal y recrearla con la selección de cereza?
Wazery

1
@xdsy: Mira esto y esto .
Justin ᚅᚔᚈᚄᚒᚔ

2
Usted dice "C5 y C6 se retiran temporalmente de dev ... C7 se aplica a dev". Si este es el caso, ¿por qué C5 y C6 aparecen antes que C7 en el pedido de commits en origin / dev?
KJ50

@ KJ50: Porque C5 y C6 ya fueron presionados origin/dev. Cuando devse vuelve a modificar, su historial se modifica (C5 / C6 se elimina temporalmente y se vuelve a aplicar después de C7). La modificación del historial de repositorios empujados es generalmente una Really Bad Idea ™ a menos que sepa lo que está haciendo. En este caso sencillo, el problema podría resolverse haciendo un empuje de fuerza devpara origin/devdespués del rebase y notificar a cualquier otra persona que trabaja fuera del origin/devque probablemente están a punto de tener un mal día. La mejor respuesta, nuevamente, es "no hagas eso ... usa la combinación en su lugar"
Justin ᚅᚔᚈᚄᚒᚔ

3
Una cosa a tener en cuenta: el hash de C5 y C5 'es ciertamente diferente, pero no porque los números de línea sean diferentes, sino por los siguientes dos hechos, de los cuales cualquiera es suficiente para la diferencia: 1) el hash del que estamos hablando es el hash de todo el árbol de origen después de la confirmación, no el hash de la diferencia delta, y por lo tanto C5 'contiene lo que viene del C7, mientras que C5 no lo hace, y 2) El padre de C5' es diferente de C5, y esta información también se incluye en el nodo raíz de un árbol de confirmación que afecta el resultado del hash.
Ozgur Murat

113

Respuesta corta

Omitió el hecho de que ejecutó git push, obtuvo el siguiente error y luego procedió a ejecutar git pull:

To git@bitbucket.org:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

A pesar de que Git intenta ser útil, su consejo de 'git pull' probablemente no sea lo que quieres hacer .

Si usted es:

  • Trabajando en una "rama de la característica" o "rama desarrollador" solos , entonces se puede ejecutar git push --forcepara actualizar el mando a distancia con sus confirmaciones post-rebase ( según respuesta de user4405677 ).
  • Trabajando en una rama con múltiples desarrolladores al mismo tiempo, entonces probablemente no deberías estar usandogit rebase en primer lugar. Para actualizar devcon los cambios desde master, debe, en lugar de ejecutar git rebase master dev, ejecutar git merge mastermientras está encendido dev( según la respuesta de Justin ).

Una explicación un poco más larga.

Cada hash de commit en Git se basa en una serie de factores, uno de los cuales es el hash del commit que viene antes.

Si reordena los commits, cambiará los hash de commit; rebasar (cuando hace algo) cambiará los hash de confirmación. Con eso, el resultado de la ejecución git rebase master dev, donde no devestá sincronizado master, creará nuevos commits (y, por lo tanto, hashes) con el mismo contenido que los activados devpero con los commits masterinsertados antes que ellos.

Puede terminar en una situación como esta de múltiples maneras. Dos maneras en las que puedo pensar:

  • Podría tener compromisos en los masterque quiera basar su devtrabajo
  • Podría haber confirmaciones devque ya han sido enviadas a un control remoto, que luego procede a cambiar (reformular mensajes de confirmación, reordenar confirmaciones, confirmaciones de squash, etc.)

Comprendamos mejor lo que sucedió. Aquí hay un ejemplo:

Tienes un repositorio:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Conjunto inicial de confirmaciones lineales en un repositorio

Luego procedes a cambiar los commits.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Aquí es donde tendrá que aceptar mi palabra: hay varias maneras de cambiar los commits en Git. En este ejemplo, cambié la hora de C3, pero estará insertando nuevos commits, cambiando mensajes de commit, reordenando commits, aplastar cometas juntos, etc.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

Lo mismo se comete con nuevos hashes

Aquí es donde es importante notar que los hashes de confirmación son diferentes. Este es el comportamiento esperado ya que ha cambiado algo (cualquier cosa) sobre ellos. Esto está bien, PERO:

Un registro gráfico que muestra que el maestro no está sincronizado con el control remoto

Intentar presionar le mostrará un error (y le indicará que debe ejecutar git pull).

$ git push origin master
To git@bitbucket.org:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Si corremos git pull, vemos este registro:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

O, mostrado de otra manera:

Un registro gráfico que muestra una confirmación de fusión

Y ahora tenemos confirmaciones duplicadas localmente. Si tuviéramos que ejecutar git push, los enviaríamos al servidor.

Para evitar llegar a esta etapa, podríamos haber corrido git push --force(donde corrimos git pull). Esto habría enviado nuestras confirmaciones con los nuevos hashes al servidor sin problemas. Para solucionar el problema en esta etapa, podemos restablecerlo antes de ejecutarlo git pull:

Mire el reflog ( git reflog) para ver cuál era el hash de confirmación antes de ejecutar git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Arriba vemos que ba7688afue el commit en el que estábamos antes de correr git pull. Con ese hash de confirmación en la mano, podemos restablecer eso ( git reset --hard ba7688a) y luego ejecutarlo git push --force.

Y ya hemos terminado.

Pero espera, seguí basando el trabajo en las confirmaciones duplicadas

Si de alguna manera no te diste cuenta de que los commits estaban duplicados y procediste a continuar trabajando encima de los commits duplicados, realmente has hecho un desastre por ti mismo. El tamaño del desorden es proporcional al número de confirmaciones que tiene encima de los duplicados.

Cómo se ve esto:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Registro de Git que muestra confirmaciones lineales sobre confirmaciones duplicadas

O, mostrado de otra manera:

Un gráfico de registro que muestra confirmaciones lineales sobre confirmaciones duplicadas

En este escenario, queremos eliminar las confirmaciones duplicadas, pero mantener las confirmaciones que tenemos basadas en ellas; queremos mantener C6 a C10. Como con la mayoría de las cosas, hay varias maneras de hacerlo:

Ya sea:

  • Cree una nueva rama en el último commit 1 duplicado , cherry-pickcada commit (C6 a C10 inclusive) en esa nueva rama, y ​​trate esa nueva rama como canónica.
  • Ejecutar git rebase --interactive $commit, donde $commitestá la confirmación antes de las dos confirmaciones duplicadas 2 . Aquí podemos eliminar directamente las líneas de los duplicados.

1 No importa cuál de los dos elija, ya sea ba7688ao 2a2e220funciona bien.

2 En el ejemplo sería 85f59ab.

TL; DR

Establecer advice.pushNonFastForwarden false:

git config --global advice.pushNonFastForward false

1
Está bien seguir el consejo de "git pull ..." siempre y cuando uno se dé cuenta de los puntos suspensivos que oculta la opción "--rebase" (también conocida como "-r"). ;-)
G. Sylvie Davies

44
Recomendaría usar git push's --force-with-leasehoy en día, ya que es un mejor valor predeterminado
Whymarrh

44
Es esta respuesta o una máquina del tiempo. ¡Gracias!
ZeMoon

Explicación muy ordenada ... Me topé con un problema similar que duplicó mi código 5-6 veces después de que intenté cambiar la repetición repetidamente ... solo para asegurarme de que el código esté actualizado con el maestro ... pero cada vez que se presionó nuevas confirmaciones a mi sucursal, duplicando mi código también. ¿Puede decirme si el empuje forzado (con opción de arrendamiento) es seguro si soy el único desarrollador que trabaja en mi sucursal? ¿O fusionar el maestro con el mío en lugar de rebasar es la mejor manera?
Dhruv Singhal

12

Creo que omitiste un detalle importante al describir tus pasos. Más específicamente, su último paso, git pushen dev, realmente le habría dado un error, ya que normalmente no puede impulsar cambios no rápidos.

Así lo hiciste git pullantes del último empuje, lo que resultó en un compromiso de fusión con C6 y C6 'como padres, por lo que ambos permanecerán listados en el registro. Un formato de registro más bonito podría haber hecho más obvio que son ramas fusionadas de confirmaciones duplicadas.

O bien, hizo un git pull --rebase(o sin explícito --rebasesi está implícito en su configuración), que retiró los C5 y C6 originales en su dev local (y reescribió los siguientes a nuevos hash, C7 'C5' 'C6' ').

Una forma de salir de esto podría haber sido git push -fforzar el empuje cuando dio el error y borrar C5 C6 del origen, pero si alguien más también los hizo tirar antes de que los borraras, estarías en muchos más problemas ... Básicamente, todos los que tienen C5 C6 necesitarían hacer pasos especiales para deshacerse de ellos. Es exactamente por eso que dicen que nunca debes rebajar nada de lo que ya está publicado. Sin embargo, todavía es factible si dicha "publicación" está dentro de un equipo pequeño.


1
La omisión de git pulles crucial. Su recomendación de git push -f, aunque peligrosa, es probablemente lo que buscan los lectores.
Whymarrh

En efecto. Cuando escribí la pregunta que realmente hice git push --force, solo para ver qué iba a hacer Git. Aprendí mucho sobre Git desde entonces y hoy en día rebasees parte de mi flujo de trabajo normal. Sin embargo, lo hago git push --force-with-leasepara evitar sobrescribir el trabajo de otra persona.
elitalon

Usar --force-with-leasees un buen valor predeterminado, también dejaré un comentario debajo de mi respuesta
Whymarrh

2

Descubrí que en mi caso, este problema es consecuencia de un problema de configuración de Git. (Implicando tirar y fusionar)

Descripción del problema:

Síntomas: Comisiones duplicadas en la rama secundaria después de la nueva versión, lo que implica numerosas fusiones durante y después de la nueva base.

Flujo de trabajo: Estos son los pasos del flujo de trabajo que estaba realizando:

  • Trabajar en la "rama de características" (hijo de "rama de desarrollo")
  • Confirmar y empujar cambios en "Características-rama"
  • Verifique "Develop-branch" (Rama madre de características) y trabaje con ella.
  • Comprometer e impulsar cambios en "Desarrollar-rama"
  • Verifique "Features-branch" y extraiga los cambios del repositorio (en caso de que alguien más haya cometido trabajo)
  • Rebase "Características-rama" en "Desarrollar-rama"
  • Fuerza de empuje de cambios en "Feature-branch"

Como consecuencia de este flujo de trabajo, la duplicación de todos los commits de "Feature-branch" desde el rebase anterior ... :-(

El problema se debió al tirón de los cambios de la rama secundaria antes del rebase. La configuración de extracción predeterminada de Git es "fusionar". Esto está cambiando los índices de confirmaciones realizadas en la rama secundaria.

La solución: en el archivo de configuración de Git, configure pull para trabajar en modo rebase:

...
[pull]
    rebase = preserve
...

Espero que pueda ayudar a JN Grx


1

Es posible que haya extraído de una rama remota diferente de su actual. Por ejemplo, es posible que haya retirado de Master cuando su sucursal está desarrollando seguimiento de desarrollo. Git obtendrá obedientemente confirmaciones duplicadas si se extrae de una rama no rastreada.

Si esto sucede, puede hacer lo siguiente:

git reset --hard HEAD~n

dónde n == <number of duplicate commits that shouldn't be there.>

Luego, asegúrese de estar tirando de la rama correcta y luego ejecute:

git pull upstream <correct remote branch> --rebase

Tirar con él --rebaseasegurará que no está agregando confirmaciones extrañas que podrían enturbiar el historial de confirmaciones.

Aquí hay un poco de mano para git rebase.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.