Preguntas de flujo de trabajo de Git y rebase vs merge


He estado usando Git durante un par de meses en un proyecto con otro desarrollador. Tengo varios años de experiencia con SVN, así que supongo que traigo mucho equipaje a la relación.

He oído que Git es excelente para ramificar y fusionar, y hasta ahora, simplemente no lo veo. Claro, ramificar es muy simple, pero cuando intento fusionarme, todo se va al infierno. Ahora, estoy acostumbrado a eso de SVN, pero me parece que acabo de cambiar un sistema de versiones sub-par por otro.

Mi pareja me dice que mis problemas se derivan de mi deseo de fusionar willy-nilly, y que debería usar rebase en lugar de fusionar en muchas situaciones. Por ejemplo, aquí está el flujo de trabajo que ha establecido:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

Esencialmente, cree una rama de característica, SIEMPRE rebase de master a la rama, y combine de la rama de nuevo a master. Es importante tener en cuenta que la sucursal siempre se mantiene local.

Aquí está el flujo de trabajo que comencé con

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

Hay dos diferencias esenciales (creo): uso merge siempre en lugar de rebasing, y envio mi rama de característica (y mis commits de rama de característica) al repositorio remoto.

Mi razonamiento para la rama remota es que quiero hacer una copia de seguridad de mi trabajo mientras estoy trabajando. Nuestro repositorio se respalda automáticamente y se puede restaurar si algo sale mal. Mi portátil no es, o no tan a fondo. Por lo tanto, odio tener código en mi computadora portátil que no se refleja en algún lugar else.

Mi razonamiento para la fusión en lugar de rebase es que la fusión parece ser estándar y rebase parece ser una característica avanzada. Mi intuición es que lo que estoy tratando de hacer no es una configuración avanzada, por lo que rebase debería ser innecesario. Incluso he leído detenidamente el nuevo libro de programación Pragmática sobre Git, y cubren merge extensamente y apenas mencionan rebase.

De todos modos, estaba siguiendo mi flujo de trabajo en una rama reciente, y cuando traté de fusionarlo de nuevo a master, todo se fue al infierno. Había toneladas de conflictos con cosas que no deberían haber importado. Los conflictos no tenían sentido para mí. Me tomó un día ordenar todo, y finalmente culminó en un empujón forzado al maestro remoto, ya que mi maestro local tiene todos los conflictos resueltos, pero el remoto todavía no estaba feliz.

¿Cuál es el flujo de trabajo "correcto" para algo como esto? Se supone que Git hace que la ramificación y la fusión sean súper fáciles, y simplemente no lo veo.

Actualizar 2011-04-15

Esta parece ser una pregunta muy popular, así que pensé en actualizar con mis dos años de experiencia desde que pregunté por primera vez.

Resulta que el flujo de trabajo original es correcto, al menos en nuestro caso. En otras palabras, esto es lo que hacemos y funciona:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

De hecho, nuestro flujo de trabajo es un poco diferente, ya que tendemos a hacer fusiones squash en lugar de fusiones raw. (Nota: Esto es controvertido, ver abajo.) Esto nos permite convertir toda nuestra rama de características en un solo commit en master. Luego eliminamos nuestra rama de características. Esto nos permite estructurar lógicamente nuestros commits en master, incluso si son un poco desordenados en nuestras ramas. Entonces, esto es lo que hacemos:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

Controversia de fusión de squash - Como varios comentaristas han señalado, la fusión de squash desechará todo el historial de tu rama de característica. Como su nombre lo indica, aplasta todas las confirmaciones en una sola. Para características pequeñas, esto tiene sentido ya que lo condensa en un solo paquete. Para características más grandes, probablemente no sea una gran idea, especialmente si tus commits individuales ya son atómicos. Realmente se reduce a la preferencia personal.

Github y Bitbucket(otros?) Pull Requests - En caso de que te estés preguntando cómo merge/rebase se relaciona con Pull Requests, te recomiendo seguir todos los pasos anteriores hasta que estés listo para fusionar de nuevo a master. En lugar de fusionarse manualmente con git, simplemente acepta el PR. Tenga en cuenta que esto no hará una fusión de squash (al menos no por defecto), pero no-squash, no-fast-forward es la convención de fusión aceptada en la comunidad de Pull Request (que yo sepa). Específicamente, funciona así:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

He llegado a amar Git y nunca quiero volver a SVN. Si estás luchando, solo quédate con él y eventualmente verás la luz al final del túnel.

Author: Micah, 2009-01-19

10 answers

"Conflictos" significa "evoluciones paralelas de un mismo contenido". Así que si se va "todo al infierno" durante una fusión, significa que tienes evoluciones masivas en el mismo conjunto de archivos.

La razón por la que un rebase es mejor que una fusión es que:

  • reescribe su historial de confirmaciones local con el del maestro (y luego vuelve a aplicar su trabajo, resolviendo cualquier conflicto)
  • la fusión final sin duda será una de "avance rápido", porque tendrá todo el historial de confirmaciones de el maestro, además de solo sus cambios para volver a aplicar.

Confirmo que el flujo de trabajo correcto en ese caso (evoluciones en el conjunto común de archivos) es rebase primero, luego merge.

Sin embargo, eso significa que, si envías tu rama local (por motivos de copia de seguridad), esa rama no debería ser extraída (o al menos utilizada) por nadie más (ya que el historial de confirmaciones será reescrito por el rebase sucesivo).


En ese tema (rebase y luego combine el flujo de trabajo), barraponto menciona en los comentarios dos posts interesantes, ambos de randyfay.com :

Usando esta técnica, su trabajo siempre va encima de la rama pública como un parche que está actualizado con HEAD actual.

(existe una técnica similar para el bazar )

 344
Author: VonC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:10

TL;DR{[17]]}

Un flujo de trabajo de rebase de git no te protege de personas que son malas en la resolución de conflictos o personas que están acostumbradas a un flujo de trabajo de SVN, como se sugiere en Evitar desastres de Git: Una historia sangrienta. Solo hace que la resolución de conflictos sea más tediosa para ellos y hace que sea más difícil recuperarse de una mala resolución de conflictos. En su lugar, use diff3 para que no sea tan difícil en primer lugar.


El flujo de trabajo de rebase no es mejor para el conflicto resolución!

Soy muy pro-rebase para limpiar la historia. Sin embargo, si Alguna vez llego a un conflicto, inmediatamente aborto el rebase y hago una fusión en su lugar! Realmente me mata que la gente esté recomendando un flujo de trabajo de rebase como una mejor alternativa a un flujo de trabajo de fusión para la resolución de conflictos (que es exactamente de lo que se trataba esta pregunta).

Si se va "todo al infierno" durante una fusión, se irá" todo al infierno " durante un rebase, ¡y potencialmente mucho más infierno también! Aca por qué:

Razón # 1: Resuelve los conflictos una vez, en lugar de una vez por cada commit

Cuando rebase en lugar de fusionar, tendrá que realizar la resolución de conflictos hasta tantas veces como tenga confirmaciones de rebase, para el mismo conflicto!

Escenario real

Ramifico de master a refactorizar un método complicado en una rama. Mi trabajo de refactorización se compone de 15 commits en total mientras trabajo para refactorizarlo y obtener revisiones de código. Parte de mi refactorización implica arreglar las pestañas y espacios mixtos que estaban presentes en master antes. Esto es necesario, pero desafortunadamente entrará en conflicto con cualquier cambio realizado posteriormente a este método en master. Por supuesto, mientras estoy trabajando en este método, alguien hace un cambio simple y legítimo al mismo método en la rama maestra que debería fusionarse con mis cambios.

Cuando es el momento de fusionar mi rama con master, tengo dos opciones:

Git merge: Tengo un conflicto. Veo la cambio que hicieron para master y fusionarlo con (el producto final de) mi rama. Terminado.

Git rebase: Tengo un conflicto con mi primer commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi segundo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi tercer commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi cuarto commit. Resuelvo la conflicto y continuar la rebase. Tengo un conflicto con mi quinto commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi sexto commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi séptimo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi octavo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi noveno commit. Yo resuelvo el conflicto y continuar la rebase. Tengo un conflicto con mi décimo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi undécimo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi doceavo commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi decimotercera commit. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi decimocuarto cometer. Resuelvo el conflicto y continúo el rebase. Tengo un conflicto con mi decimoquinto commit. Resuelvo el conflicto y continúo el rebase.

Tienes que estar bromeando si este es tu flujo de trabajo preferido. Todo lo que se necesita es una corrección de espacios en blanco que entra en conflicto con un cambio realizado en master, y cada commit entrará en conflicto y debe ser resuelto. Y este es un escenario simple con solo un conflicto de espacios en blanco. Dios no permita que tengas un verdadero conflicto que implica cambios importantes de código entre archivos y tiene que resolver que varias veces.

Con toda la resolución de conflictos adicional que necesita hacer, solo aumenta la posibilidad de que cometa un error. Pero los errores están bien en git ya que puedes deshacer, ¿verdad? Excepto, por supuesto...

Razón # 2: Con rebase,no hay deshacer!

Creo que todos podemos estar de acuerdo en que la resolución de conflictos puede ser difícil, y también que algunas personas son muy malo en eso. Puede ser muy propenso a errores, por lo que es tan grande que git hace que sea fácil de deshacer!

Cuando se fusiona una rama, git crea un commit de fusión que puede ser descartado o modificado si la resolución de conflictos va mal. Incluso si ya ha enviado la confirmación de fusión incorrecta al repositorio público / autoritativo, puede usar git revert para deshacer los cambios introducidos por la fusión y rehacer la fusión correctamente en una nueva confirmación de fusión.

Cuando rebase una rama, en el probable caso de que la resolución de conflictos se haga mal, estás jodido. Cada commit ahora contiene la fusión incorrecta, y no puedes simplemente rehacer la rebase*. En el mejor de los casos, tienes que volver atrás y modificar cada una de las confirmaciones afectadas. No es divertido.

Después de un rebase, es imposible determinar qué era originalmente parte de los commits y qué se introdujo como resultado de una mala resolución de conflictos.

*Puede ser posible deshacer un rebase si puede excavar las referencias antiguas de los registros internos de git, o si creas una tercera rama que apunte a la última confirmación antes de cambiar la base.

Saca el infierno de la resolución de conflictos: usa diff3

Tomemos este conflicto por ejemplo:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Mirando el conflicto, es imposible decir qué cambió cada rama o cuál fue su intención. Esta es la razón más importante en mi opinión por la que la resolución de conflictos es confusa y difícil.

Diff3 al rescate!

git config --global merge.conflictstyle diff3

Cuando se utiliza el diff3, cada nuevo conflicto tendrá una 3a sección, el ancestro común fusionado.

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

Primero examine el ancestro común fusionado. Luego compare cada lado para determinar la intención de cada rama. Puedes ver que HEAD cambió EmailMessage a TextMessage. Su intención es cambiar la clase utilizada a TextMessage, pasando los mismos parámetros. También puede ver que la intención de feature-branch es pasar false en lugar de true para la opción: include_timestamp. Para combinar estos cambios, combine la intent de ambos:

TextMessage.send(:include_timestamp => false)

En general:

  1. Compare el ancestro común con cada rama, y determine qué rama tiene el cambio más simple{[127]]}
  2. Aplique ese cambio simple a la versión del código de la otra rama, de modo que contenga tanto el cambio más simple como el más complejo
  3. Elimine todas las secciones del código de conflicto que no sean las que acaba de fusionar los cambios en

Alternar: Resolver aplicando manualmente el cambios de sucursal

Finalmente, algunos conflictos son terribles de entender incluso con diff3. Esto sucede especialmente cuando diff encuentra líneas en común que no son semánticamente comunes (por ejemplo. ¡ambas ramas tenían una línea en blanco en el mismo lugar!). Por ejemplo, una rama cambia la sangría del cuerpo de una clase o reordena métodos similares. En estos casos, una mejor estrategia de resolución puede ser examinar el cambio desde cualquier lado de la fusión y aplicar manualmente el diff al otro archivo.

Veamos cómo podemos resolver un conflicto en un escenario donde la fusión origin/feature1 donde lib/message.rb conflictos.

  1. Decida si nuestra rama actualmente comprobada (HEAD, o --ours) o la rama que estamos fusionando (origin/feature1, o --theirs) es un cambio más simple de aplicar. El uso de diff con triple punto (git diff a...b) muestra los cambios que ocurrieron en b desde su última divergencia de a, o en otras palabras, comparar el ancestro común de a y b con b.

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. Echa un vistazo a la versión más complicada del archivo. Esto eliminará todos los marcadores de conflicto y utilizará el lado que elija.

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. Con el cambio complicado comprobado, tire hacia arriba de la diferencia del cambio más simple (ver paso 1). Aplique cada cambio de esta diferencia al archivo en conflicto.

 362
Author: Edward Anderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-29 15:13:37

En mi flujo de trabajo, rebase tanto como sea posible (y trato de hacerlo a menudo. No dejar que las discrepancias se acumulen reduce drásticamente la cantidad y la gravedad de las colisiones entre ramas).

Sin embargo, incluso en un flujo de trabajo basado principalmente en rebase, hay un lugar para las fusiones.

Recuerde que merge en realidad crea un nodo que tiene dos padres. Ahora considere la siguiente situación: Tengo dos marcas de características independientes A y B, y ahora quiero desarrollar cosas en función rama C que depende de A y B, mientras que A y B están siendo revisados.

Lo que hago entonces, es lo siguiente:

  1. Crea (y comprueba) la rama C encima de A.
  2. Fusionarlo con B

Ahora la rama C incluye cambios tanto de A como de B, y puedo seguir desarrollándolo. Si hago algún cambio a A, entonces reconstruyo el gráfico de ramas de la siguiente manera:

  1. crear la rama T en la nueva parte superior de A
  2. combinar T con B
  3. rebase C en T
  4. delete branch T

De esta manera puedo mantener gráficos arbitrarios de ramas, pero hacer algo más complejo que la situación descrita anteriormente ya es demasiado complejo, dado que no hay una herramienta automática para hacer el rebase cuando el padre cambia.

 29
Author: Alex Gontmakher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-12 20:54:12

NO utilice git push origin mirror mirror BAJO CASI NINGUNA CIRCUNSTANCIA.

No te pregunta si estás seguro de que quieres hacer esto, y será mejor que estés seguro, porque borrará todas tus ramas remotas que no estén en tu caja local.

Http://twitter.com/dysinger/status/1273652486

 20
Author: Scott Brown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-04-10 01:06:06

Tengo una pregunta después de leer su explicación: ¿Podría ser que nunca hizo un

git checkout master
git pull origin
git checkout my_new_feature

Antes de hacer el 'git rebase/merge master' en tu rama de características?

Porque tu rama maestra no se actualizará automáticamente desde el repositorio de tu amigo. Tienes que hacer eso con el git pull origin. Es decir, tal vez usted siempre rebase de una rama maestra local que nunca cambia? Y luego llega el momento de empujar, está empujando en un repositorio que tiene confirmaciones (locales) que nunca vio y por lo tanto, el empuje falla.

 13
Author: knweiss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-20 13:38:39

En su situación creo que su pareja tiene razón. Lo bueno de rebasing es que para el forastero tus cambios parecen que todos ocurrieron en una secuencia limpia por sí mismos. Esto significa

  • sus cambios son muy fáciles de revisar
  • puedes continuar haciendo commits pequeños y sin embargo puedes hacer públicos conjuntos de esos commits (fusionándolos en master) a la vez
  • cuando mires la rama master pública verás diferentes series de commits para diferentes características de diferentes desarrolladores, pero no todos se entremezclan

Todavía puede continuar empujando su rama de desarrollo privado al repositorio remoto por el bien de la copia de seguridad, pero otros no deben tratar eso como una rama "pública", ya que estará rebasando. POR cierto, un comando fácil para hacer esto es git push --mirror origin.

El artículo Empaquetando software usando Git hace un trabajo bastante bueno explicando las compensaciones en la fusión versus el rebasamiento. Es un poco diferente contexto pero los principios son los mismos basically básicamente se reduce a si sus ramas son públicas o privadas y cómo planea integrarlas en la línea principal.

 12
Author: Pat Notz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-19 16:15:02

De todos modos, estaba siguiendo mi flujo de trabajo en una rama reciente, y cuando traté de fusionarlo de nuevo a master, todo se fue al infierno. Había toneladas de conflictos con cosas que no deberían haber importado. Los conflictos no tenían sentido para mí. Me tomó un día ordenar todo, y finalmente culminó en un empujón forzado al maestro remoto, ya que mi maestro local tiene todos los conflictos resueltos, pero el remoto todavía no estaba feliz.

Ni en la de tu pareja ni en tu flujos de trabajo sugeridos en caso de que haya encontrado conflictos que no tenían sentido. Incluso si lo hubiera hecho, si está siguiendo los flujos de trabajo sugeridos, después de la resolución, no debería ser necesario un empuje 'forzado'. Sugiere que en realidad no ha fusionado la rama a la que estaba empujando, sino que ha tenido que empujar una rama que no era un descendiente de la punta remota.

Creo que necesitas mirar cuidadosamente lo que pasó. ¿Podría alguien más haber (deliberadamente o no) rebobinado el maestro remoto rama entre la creación de la rama local y el punto en el que se intentó fusionar de nuevo en la rama local?

En comparación con muchos otros sistemas de control de versiones, he encontrado que usar Git implica menos lucha contra la herramienta y le permite trabajar en los problemas que son fundamentales para sus flujos de origen. Git no realiza magia, por lo que los cambios conflictivos causan conflictos, pero debería facilitar la tarea de escribir mediante su seguimiento del parentesco de las confirmaciones.

 11
Author: CB Bailey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-04 19:07:25

Por lo que he observado, git merge tiende a mantener las ramas separadas incluso después de la fusión, mientras que rebase luego merge lo combina en una sola rama. Este último resulta mucho más limpio, mientras que en el primero, sería más fácil averiguar qué confirmaciones pertenecen a qué rama incluso después de la fusión.

 6
Author: Pepe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-25 05:40:35

"Incluso si eres un solo desarrollador con solo unas pocas ramas, vale la pena acostumbrarse a usar rebase y merge correctamente. El patrón de trabajo básico se verá como:

  • Crear una nueva rama B a partir de la rama existente A

  • Añadir / confirmar cambios en la rama B

  • Rebase actualizaciones de la rama A

  • Combinar cambios de la rama B en la rama A "

Https://www.atlassian.com/git/tutorials/merging-vs-rebasing /

 6
Author: Rakka Rage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-26 22:14:10

Con Git no hay un flujo de trabajo "correcto". Usa lo que sea que flota tu barco. Sin embargo, si constantemente tiene conflictos al fusionar ramas, tal vez debería coordinar mejor sus esfuerzos con sus compañeros desarrolladores. Parece que ustedes dos siguen editando los mismos archivos. Además, tenga cuidado con los espacios en blanco y las palabras clave de subversion (por ejemplo, "Id Id Id" y otras).

 2
Author: Bombe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-19 15:25:30