tag Git workflow y rebase vs fusionar preguntas




git tag remove (8)

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

He escuchado que Git es excelente para ramificarse y fusionarse, y hasta ahora, simplemente no lo veo. Claro, la ramificación es muy simple, pero cuando intento fusionarme, todo va al infierno. Ahora, estoy acostumbrado a eso de SVN, pero me parece que acabo de cambiar un sistema de versiones por debajo del par por otro.

Mi compañero me dice que mis problemas se derivan de mi deseo de fusionarme voluntariamente, y que debería usar rebase en lugar de fusionarme 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ísticas, SIEMPRE rebase del maestro a la rama, y ​​fusione de la rama nuevamente al maestro. Es importante tener en cuenta que la sucursal siempre se mantiene local.

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

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): utilizo la combinación siempre en lugar de rebasar, y presiono mi rama de característica (y mis confirmaciones de rama de característica) al repositorio remoto.

Mi razonamiento para la sucursal remota es que quiero que se realice una copia de respaldo de mi trabajo mientras trabajo. Nuestro repositorio se copia automáticamente y se puede restaurar si algo sale mal. Mi portátil no lo es, o no tan a fondo. Por lo tanto, odio tener un código en mi computadora portátil que no esté reflejado en ningún otro lugar.

Mi razonamiento para la fusión en lugar de la rebase es que la fusión parece ser estándar y la 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 la rebase debería ser innecesaria. Incluso he leído el nuevo libro de Programación Pragmática en Git, y abarcan la fusión extensiva y apenas mencionan rebase.

De todos modos, estaba siguiendo mi flujo de trabajo en una rama reciente, y cuando intenté fusionarlo de nuevo para dominarlo, todo se fue al infierno. Hubo muchos conflictos con cosas que no deberían haber importado. Los conflictos simplemente no tenían sentido para mí. Me tomó un día ordenar todo, y finalmente culminó en un impulso forzado al maestro remoto, ya que mi maestro local ha resuelto todos los conflictos, pero el remoto aún no estaba contento.

¿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 muy fáciles, y no lo veo.

Actualización 2011-04-15

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

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 de squash en lugar de fusiones sin procesar. ( Nota: esto es controvertido, consulte a continuación ) . Esto nos permite convertir toda nuestra rama de características en un solo compromiso en el maestro. Luego borramos nuestra rama de características. Esto nos permite estructurar lógicamente nuestros compromisos en el maestro, incluso si están un poco sucios en nuestras sucursales. 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 sobre la fusión de squash : como han señalado varios comentaristas, la fusión de squash eliminará todo el historial de la rama de tu característica. Como su nombre lo indica, aplasta todos los compromisos en uno solo. Para características pequeñas, esto tiene sentido ya que se condensa en un solo paquete. Para funciones más grandes, probablemente no sea una gran idea, especialmente si sus confirmaciones individuales ya son atómicas. todo se reduce a la preferencia personal.

Solicitudes de extracción de Github y Bitbucket (¿otros?) : En caso de que se esté preguntando cómo se relaciona la combinación / rebase con las solicitudes de extracción, recomiendo seguir todos los pasos anteriores hasta que esté listo para volver a combinar con el maestro. 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 de forma predeterminada), pero no aplastar, el no avance rápido es la convención de combinación aceptada en la comunidad 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

Vine a amar a Git y nunca quiero volver a SVN. Si estás luchando, simplemente sigue adelante y, finalmente, verás la luz al final del túnel.


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

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 siempre se cambiaría de una rama maestra local que nunca cambia? Y luego, llegue el momento de la inserción, está presionando en un repositorio que tiene confirmaciones (locales) que nunca vio y por lo tanto, la inserción falla.


En tu situación creo que tu pareja tiene razón. Lo bueno de rebasar es que, para el forastero, parece que todos los cambios ocurrieron en una secuencia limpia por sí mismos. Esto significa

  • Tus cambios son muy fáciles de revisar.
  • puede seguir haciendo compromisos agradables y pequeños y, sin embargo, puede hacer que los conjuntos de esos compromisos se conviertan en públicos (fusionándolos con el maestro) todos a la vez
  • cuando vea la rama maestra pública, verá diferentes series de confirmaciones para diferentes funciones por parte de diferentes desarrolladores, pero no todas se entremezclarán

Aún puede continuar presionando su rama de desarrollo privado en el repositorio remoto por el bien de la copia de seguridad, pero otros no deben tratar eso como una rama "pública" ya que será rebasado. Por cierto, un comando fácil para hacer esto es git push --mirror origin .

El artículo El software de empaquetado que utiliza Git hace un buen trabajo explicando las ventajas y desventajas de la fusión y la rebasación. Es un contexto un poco diferente pero los principios son los mismos: básicamente se trata de si sus sucursales son públicas o privadas y cómo planea integrarlos en la línea principal.


NO use git push origin --mirror CASI CASY EN CUALQUIER CIRCUNSTANCIA.

No pregunta si está seguro de que desea hacer esto, y es mejor que esté seguro, ya que borrará todas las sucursales remotas que no estén en su caja local.

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


TL; DR

Un flujo de trabajo de git rebase no lo protege de las personas que son malas en la resolución de conflictos o de las personas que están acostumbradas a un flujo de trabajo de SVN, como se sugiere en Evitar los 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 la resolución de conflictos!

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

Si va "al infierno" durante una fusión, irá "al infierno" durante una rebase, ¡y potencialmente mucho más infierno también! Este es el por qué:

Razón # 1: Resuelva conflictos una vez, en lugar de una vez para cada confirmación

Cuando rebasa en lugar de fusionar, tendrá que realizar la resolución de conflictos tantas veces como se haya comprometido a rebase, ¡para el mismo conflicto!

Escenario real

Me bifurco del maestro para refactorizar un método complicado en una rama. Mi trabajo de refactorización se compone de 15 confirmaciones totales mientras trabajo para refactorizarlo y obtener revisiones de código. Parte de mi refactorización consiste en arreglar las pestañas mixtas y los espacios que antes estaban presentes en el maestro. Esto es necesario, pero desafortunadamente entrará en conflicto con cualquier cambio realizado posteriormente a este método en maestro. Por supuesto, mientras estoy trabajando en este método, alguien realiza un cambio simple y legítimo al mismo método en la rama maestra que debería combinarse con mis cambios.

Cuando es hora de fusionar mi sucursal con el maestro, tengo dos opciones:

git merge: me sale un conflicto. Veo el cambio que hicieron para dominarlo y fusionarlo con (el producto final de) mi sucursal. Hecho.

git rebase: Tengo un conflicto con mi primer commit. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi segundo commit. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi tercer commit. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi cuarto commit. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi quinto commit. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi sexto compromiso. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi séptimo compromiso. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi octavo compromiso. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi noveno compromiso. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi décima comisión. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi undécimo compromiso. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi duodécima comisión. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi decimotercer cometer. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi decimocuarto compromiso. Resuelvo el conflicto y continúo la rebase. Me sale un conflicto con mi decimoquinto compromiso. Resuelvo el conflicto y continúo la rebase.

Tienes que estar bromeando si este es tu flujo de trabajo preferido. Todo lo que necesita es una solución de espacio en blanco que entra en conflicto con un cambio realizado en el maestro, y cada confirmación entrará en conflicto y debe resolverse. Y este es un escenario simple con solo un conflicto de espacios en blanco. No permita que tenga un conflicto real que implique cambios de código importantes en los archivos y que deba resolverlo varias veces.

Con toda la resolución adicional de conflictos que necesita hacer, solo aumenta la posibilidad de que cometa un error . Pero los errores están bien en Git ya que puedes deshacerlos, ¿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 malas en eso. Puede ser muy propenso a cometer errores, por lo que es tan bueno que git hace que sea fácil de deshacer.

Cuando fusionas una rama, git crea una confirmación de fusión que puede ser descartada o enmendada si la resolución del conflicto falla. Incluso si ya ha introducido el compromiso de combinación incorrecto en el repositorio público / autorizado, puede usar git revert para deshacer los cambios introducidos por la combinación y rehacer la fusión correctamente en un nuevo compromiso de combinación.

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

Después de una nueva fase, es imposible determinar qué fue originalmente parte de los compromisos y qué se introdujo como resultado de una mala resolución de conflictos.

* Es posible deshacer una rebase si puede extraer las referencias antiguas de los registros internos de git, o si crea una tercera rama que apunta a la última confirmación antes de rebasar.

Saque el infierno de la resolución de conflictos: use 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 cual la resolución de conflictos es confusa y difícil.

diff3 al rescate!

git config --global merge.conflictstyle diff3

Cuando use el diff3, cada nuevo conflicto tendrá una tercera sección, el ancestro común combinado.

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

Primero examine el antepasado común fusionado. Luego compara cada lado para determinar la intención de cada rama. Puede ver que HEAD cambió el mensaje de correo electrónico a mensaje de texto. Su intención es cambiar la clase utilizada para TextMessage, pasando los mismos parámetros. También puede ver que la intención de la rama de función es pasar falso en lugar de verdadero para la opción: include_timestamp. Para fusionar estos cambios, combine la intención de ambos:

TextMessage.send(:include_timestamp => false)

En general:

  1. Compara el ancestro común con cada rama y determina qué rama tiene el cambio más simple
  2. Aplique ese cambio simple a la versión del código de la otra rama, de modo que contenga el cambio más simple y complejo.
  3. Elimine todas las secciones del código de conflicto que no sea la que acaba de fusionar los cambios en

Alternativo: Resuelva aplicando manualmente los cambios de la rama.

Finalmente, algunos conflictos son terribles de entender incluso con diff3. Esto sucede especialmente cuando las diferencias encuentran líneas en común que no son semánticamente comunes (por ejemplo, ¡ambas ramas tienen 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 combinación y aplicar manualmente el diff al otro archivo.

Veamos cómo podemos resolver un conflicto en un escenario donde fusionar origin/feature1 donde lib/message.rb entra en conflicto.

  1. Decida si nuestra rama actualmente retirada ( HEAD , o --ours ) o la rama que estamos fusionando ( origin/feature1 , o --theirs ) es un cambio más sencillo de aplicar. El uso de diff con punto triple ( git diff a...b ) muestra los cambios que ocurrieron en b desde su última divergencia de a , o en otras palabras, compara el ancestro común de ayb 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 usará el lado que elijas.

    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, levante la diferencia del cambio más simple (vea el paso 1). Aplique cada cambio de este dif al archivo conflictivo.


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


De todos modos, estaba siguiendo mi flujo de trabajo en una rama reciente, y cuando intenté fusionarlo de nuevo para dominarlo, todo se fue al infierno. Hubo muchos conflictos con cosas que no deberían haber importado. Los conflictos simplemente no tenían sentido para mí. Me tomó un día ordenar todo, y finalmente culminó en un impulso forzado al maestro remoto, ya que mi maestro local ha resuelto todos los conflictos, pero el remoto aún no estaba contento.

En ninguno de sus compañeros ni en los flujos de trabajo sugeridos, si hubiera encontrado conflictos que no tuvieran sentido. Incluso si lo hubiera hecho, si está siguiendo los flujos de trabajo sugeridos, luego de la resolución no debería ser necesario un impulso 'forzado'. Sugiere que en realidad no ha fusionado la rama a la que estaba empujando, sino que tuvo 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 (deliberadamente o no) rebobinar la rama maestra remota entre su creación de la rama local y el punto en el que intentó fusionarla de nuevo con la rama local?

En comparación con muchos otros sistemas de control de versiones, descubrí que el uso de Git implica luchar menos 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 escritura mediante el seguimiento de la parentesco de compromiso.


Con Git no hay un flujo de trabajo "correcto". Usa lo que flota tu barco. Sin embargo, si constantemente tiene conflictos al fusionar sucursales, ¿quizás 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 las palabras clave de espacio en blanco y subversión (es decir, "$ Id $" y otros).


"Conflictos" significa "evoluciones paralelas de un mismo contenido". Por lo tanto, si 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 una rebase es mejor que una fusión es que:

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

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

Sin embargo, eso significa que, si presiona su sucursal local (por razones de respaldo), esa rama no debe ser retirada (o al menos utilizada) por nadie más (ya que el historial de confirmación será reescrito por la rebase sucesiva).

Sobre ese tema (rebase y luego fusionar el flujo de trabajo), barraponto menciona en los comentarios dos publicaciones interesantes, ambas de randyfay.com :

Utilizando esta técnica, su trabajo siempre se coloca en la parte superior de la rama pública como un parche que está actualizado con la HEAD actual.

( Existe una técnica similar para el bazar ).





git-rebase