Pregunta Git workflow y rebase vs fusionar preguntas


He estado usando Git ahora 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 ramificarse y fusionarse, y hasta ahora, simplemente no lo veo. Claro, la bifurcación es muy simple, pero cuando intento fusionarme, todo se va al infierno. Ahora, estoy acostumbrado a eso desde SVN, pero me parece que acabo de cambiar un sistema de control de versiones por debajo de otro por otro.

Mi compañero me dice que mis problemas provienen de mi deseo de fusionarme de cualquier manera, y que debería usar rebase en lugar de combinar 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 desde el maestro a la sucursal, y fusione desde la rama de regreso a la maestra. 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): utilizo fusionar siempre en lugar de rebase, y presiono mi rama de características (y mis ramificaciones de entidades de función) al repositorio remoto.

Mi razonamiento para la sucursal remota es que quiero hacer una copia de seguridad de mi trabajo mientras trabajo. Nuestro repositorio se respalda automáticamente y se puede restaurar si algo sale mal. Mi computadora portátil no es, o no tan completamente. Por lo tanto, odio tener un código en mi computadora portátil que no esté reflejado en otro lado.

Mi razonamiento para la fusión en lugar de rebase es que la fusión parece ser estándar y la rebase parece ser una función 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 detenidamente el nuevo libro de Programación Pragmática en Git, y cubren la fusión extensamente y apenas mencionan la rebase.

De todos modos, estaba siguiendo mi flujo de trabajo en una sucursal reciente, y cuando traté de fusionarlo de nuevo con el maestro, todo se fue al carajo. Hubo toneladas de conflictos con cosas que no deberían haber importado. Los conflictos simplemente no tenían sentido para mí. Me llevó un día resolver todo, y finalmente culminé en un empuje forzoso hacia el maestro remoto, ya que mi maestro local resolvió 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 súper fáciles, y simplemente 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 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 Squash se fusiona en lugar de fusiones crudas. (Nota: Esto es controvertido, ver abajo.) Esto nos permite convertir toda nuestra rama de características en una única confirmación en el maestro. 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 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

Squash Merge Controversy - Como han señalado varios comentaristas, la fusión de squash eliminará todo el historial en su rama de características. Como su nombre lo indica, destruye todos los commits en uno solo. Para las características pequeñas, esto tiene sentido ya que lo 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.

Github y Bitbucket (¿otros?) Solicitudes de extracción - En caso de que te preguntes cómo se relaciona merge / rebase con Pull Demand, te recomiendo que sigas todos los pasos anteriores hasta que estés listo para volver a combinar con Master. En lugar de fusionarse manualmente con git, simplemente acepta el PR. 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 a Git y nunca quiero volver a SVN. Si estás luchando, simplemente quédate con él y eventualmente verás la luz al final del túnel.


895
2018-01-19 15:16


origen


Respuestas:


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

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

  • reescribe el historial de compromiso local con el del maestro (y luego vuelve a aplicar tu trabajo, resolviendo cualquier conflicto)
  • la fusión final será ciertamente una de "avance rápido", ya que tendrá todo el historial de compromiso del maestro, más solo los 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 fusiona.

Sin embargo, eso significa que, si empuja su rama local (por razón de la copia de seguridad), esa rama no debe ser extraída (o al menos utilizada) por otra persona (ya que el historial de confirmación será reescrito por la rebase sucesiva).


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

Con esta técnica, su trabajo siempre pasa a la parte superior de la rama pública como un parche actualizado con la corriente HEAD.

(una técnica similar existe para el bazar)


342
2018-01-19 15:32



TL; DR

Un flujo de trabajo de git rebase no lo protege de personas que son malas para la resolución de conflictos o personas que están acostumbradas a un flujo de trabajo SVN, como se sugiere en Evitando los desastres de Git: Una historia de Gory. 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 cambio, 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 el historial. Sin embargo, si ¡Alguna vez golpeé un conflicto, de inmediato aborto la 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 va "al infierno" durante una fusión, irá "al infierno" durante una rebase, ¡y potencialmente mucho más infierno! Este es el por qué:

Motivo n. ° 1: resolver conflictos una vez, en lugar de una vez por cada compromiso

Cuando restaure 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 ramifico de maestro para refactorizar un método complicado en una rama. Mi trabajo de refactorización consta de 15 compromisos totales mientras trabajo para refactorizarlo y obtener revisiones de código. Parte de mi refactorización implica arreglar las pestañas y los espacios mixtos que estaban presentes en el maestro antes. Esto es necesario, pero desafortunadamente entrará en conflicto con cualquier cambio realizado después de este método en el maestro. Efectivamente, mientras estoy trabajando en este método, alguien hace un cambio simple y legítimo al mismo método en la rama principal que debería fusionarse con mis cambios.

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

git merge:  Tengo 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 primero cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi segundo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi tercero cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi cuarto cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi quinto cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi sexto cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi séptimo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi octavo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi noveno cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi décimo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi undécimo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi duodécimo cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi decimotercero cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi decimocuarto cometer. Resuelvo el conflicto y continúo la rebase. Tengo un conflicto con mi decimoquinto cometer. Resuelvo el conflicto y continúo la rebase.

Tienes que estar bromeando si esta es su flujo de trabajo preferido Todo lo que se necesita es una solución en blanco que entra en conflicto con un cambio realizado en el maestro, y cada compromiso entrará en conflicto y se resolverá. Y este es un sencillo escenario con solo un conflicto de espacio en blanco. Que Dios prohíba que tengas un conflicto real que implique grandes cambios de código en los archivos y tengas que resolverlo ese varias veces.

Con toda la resolución extra de conflictos que necesita hacer, solo aumenta la posibilidad de que cometerás 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 malas en eso. Puede ser muy propenso a errores, por lo que es tan bueno que Git hace que sea fácil de deshacer!

Cuando te fusionas una rama, git crea un compromiso de fusión que puede descartarse o modificarse si la resolución del conflicto no es buena. Incluso si ya ha empujado la mala combinación de compromiso al repositorio público / autorizado, puede usar git revertpara deshacer los cambios introducidos por la fusión y rehacer la combinación correctamente en una nueva fusión de confirmación.

Cuando restabaste una sucursal, en el caso probable de que la resolución del conflicto se haga mal, estás jodido. Cada commit ahora contiene la combinación incorrecta, y no puedes simplemente volver a hacer la rebase *. En el mejor de los casos, debe volver y modificar cada una de las confirmaciones afectadas. No es divertido.

Después de una rebase, es imposible determinar qué fue originalmente parte de las confirmaciones y qué se introdujo como resultado de una resolución de conflicto incorrecta.

* Es posible deshacer una rebase si puedes extraer las referencias anteriores de los registros internos de git, o si creas una tercera rama que apunta a la última confirmación antes de volver a basar.

Olvídate de la resolución de conflictos: usa diff3

Tome este conflicto, por ejemplo:

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

Al observar el conflicto, es imposible saber qué cambió cada rama o cuál fue su intención. Esta es la razón más importante en mi opinión por qué 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 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 compara cada lado para determinar la intención de cada rama. Puede ver que HEAD cambió EmailMessage a TextMessage. 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 características 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 tanto el cambio más simple como el más complejo
  3. Elimine todas las secciones del código de conflicto que no sean la que acaba de combinar los cambios en

Alternativo: Resuelva aplicando manualmente los cambios de la sucursal

Finalmente, algunos conflictos son terribles de entender incluso con diff3. Esto ocurre especialmente cuando diff encuentra 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 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 dónde lib/message.rb conflictos.

  1. Decida si nuestra rama actualmente desprotegida (HEAD, o --ours) o la rama que estamos fusionando (origin/feature1, o --theirs) es un cambio más simple de aplicar. Usar 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, compare 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. Mira la versión más complicada del archivo. Esto eliminará todos los marcadores de conflicto y usará 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. Una vez revisado el complicado cambio, extraiga la diferencia del cambio más simple (vea el paso 1). Aplique cada cambio de este diff al archivo conflictivo.


358
2018-06-27 04:25



En mi flujo de trabajo, realizo la mayor cantidad de trabajo posible (y trato de hacerlo a menudo. No permitir que las discrepancias se acumulen reduce drásticamente la cantidad y la gravedad de las colisiones entre sucursales).

Sin embargo, incluso en un flujo de trabajo principalmente basado en la base, existe un lugar para las fusiones.

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

Lo que hago entonces, es lo siguiente:

  1. Crear (y pagar) la rama C en la parte superior de A.
  2. Combínalo con B

Ahora la rama C incluye cambios de A y B, y puedo continuar desarrollando en ella. Si hago algún cambio en A, entonces reconstruyo el gráfico de ramas de la siguiente manera:

  1. crea la rama T en la nueva parte superior de A
  2. fusionar T con B
  3. rebase C en T
  4. eliminar la rama T

De esta forma 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 existe una herramienta automática para hacer la reposición cuando la matriz cambia.


29
2018-05-12 20:54



NO use el origen de git push --mirror BAJO CASI NINGUNA CIRCUNSTANCIA.

No le pregunta si está seguro de que desea hacer esto, y será mejor que lo haga, ya que borrará todas las sucursales remotas que no estén en su buzón local.

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


20
2018-04-10 01:06



Tengo una pregunta después de leer tu explicación: ¿podría ser que nunca hiciste una

git checkout master
git pull origin
git checkout my_new_feature

antes de hacer el 'git rebase / merge master' en su rama de características?

Porque tu la rama principal 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 volverías a tener una base de una rama maestra local que nunca cambiará? Y luego viene el tiempo de inserción, está presionando en un repositorio que tiene confirmaciones (locales) que nunca ha visto y, por lo tanto, falla el impulso.


13
2018-05-25 20:52



En su situación, creo que su pareja es correcta. Lo que es bueno de rebase es que, para el externo, tus cambios parecen que todos pasaron en una secuencia limpia por sí mismos. Esto significa

  • sus cambios son muy fáciles de revisar
  • puedes continuar haciendo commits pequeños y agradables y aún así puedes hacer que los conjuntos de esos commits sean públicos (al fusionarse en master) todo a la vez
  • cuando miras la rama maestra pública verás diferentes series de confirmaciones para diferentes características por diferentes desarrolladores, pero no todas serán entremezcladas

Todavía puede seguir impulsando su rama de desarrollo privado hacia el repositorio remoto por el bien de la copia de seguridad, pero otros no deberían tratar eso como una rama "pública" ya que se volverá a basar. Por cierto, un comando fácil para hacer esto es git push --mirror origin .

El artículo Software de empaque utilizando Git hace un trabajo bastante bueno explicando las ventajas y desventajas en la fusión versus el rebase. 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 integrarlas en la línea principal.


12
2018-01-19 16:15



De todos modos, estaba siguiendo mi flujo de trabajo en una sucursal reciente, y cuando traté de fusionarlo de nuevo con el maestro, todo se fue al carajo. Hubo toneladas de conflictos con cosas que no deberían haber importado. Los conflictos simplemente no tenían sentido para mí. Me llevó un día resolver todo, y finalmente culminé en un empuje forzoso hacia el maestro remoto, ya que mi maestro local resolvió todos los conflictos, pero el remoto aún no estaba contento.

Ni en los flujos de trabajo de su socio ni en los 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, luego de la resolución, no se debería requerir una inserción 'forzada'. Sugiere que no ha fusionado realmente la rama a la que estaba presionando, pero que ha tenido que presionar una rama que no era descendiente del extremo remoto.

Creo que debes mirar cuidadosamente lo que sucedió. ¿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 en la rama local?

En comparación con muchos otros sistemas de control de versiones, he descubierto que usar Git implica pelear menos con la herramienta y te permite trabajar en los problemas que son fundamentales para tus flujos de origen. Git no realiza magia, por lo que los cambios conflictivos causan conflictos, pero debería facilitar la tarea de escritura mediante el seguimiento del origen de los commits.


11
2018-01-19 19:30



Por lo que he observado, git merge tiende a mantener las ramas separadas incluso después de la fusión, mientras que rebase luego fusionar 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é cometidos pertenecen a qué rama incluso después de la fusión.


6
2017-08-25 05:40