Fusionando una twig de característica que está basada en otra twig de características

Hace unos días tuve una twig master con una historia completamente lineal. Luego creé una twig de características, que llamaremos feat/feature-a . Trabajé en esa twig y luego la envié para que la revisión del código se fusionara en master .

Mientras se revisaba feat/feature-a , quería trabajar en otra function que dependía de algún código introducido por feat/feature-a . Así que creé una twig feat/feature-b desde la twig feat/feature-a .

Mientras trabajaba en feat/feature-b , feat/feature-a se fusionó en master. Entonces ahora el maestro tiene el código introducido por feat/feature-a . Ahora quiero fusionar feat/feature-b en master, pero tengo muchos conflictos de fusión que se ven así:

 <<<<<<< HEAD ======= // Some code that was introduced by feat/feature-b >>>>>>> c948094... My commit message from feat/feature-b 

Mi suposition es que porque tomé cambios de feat/feature-a en mi twig feat/feature-b , ahora estoy tratando de "duplicar" esos cambios que terminan en conflictos de fusión.

Puedo resolverlos de forma manual, pero existen varias veces en decenas de files, por lo que me gustaría saber una mejor solución si la hay.

Resumen: use git rebase --onto <target> <limit>

Como Inútil sugirió en un comentario , si tuvieras una fusión real, esto no debería suceder. Esto es lo que quiero decir con una "fusión real", junto con un diagtwig de cómo se ve la ramificación si dibujas el gráfico de las confirmaciones en cuestión. Comenzamos con algo como esto:

 ...--E---H <-- master \ F--G <-- feat/feature-a \ I--J <-- feat/feature-b 

Aquí hay dos commits (aunque el número exacto no importa) que solo están en feat/feature-b , llamados I y J aquí; hay dos confirmaciones que están en ambas twigs de características, llamadas F y G ; y hay un compromiso que está solo en el master , llamado H (Los compromisos E y anteriores están en las tres twigs).

Supongamos que hacemos una fusión real en el master para traer F y G Eso se ve así, en forma de gráfico:

 ...--E---H--K <-- master \ / F--G <-- feat/feature-a \ I--J <-- feat/feature-b 

Tenga en count que la fusión real K tiene, como sus pointers de historial de confirmación padres, ambos confirman H (en el master ) y G (en feat/feature-a ). Por lo tanto, Git sabe, más tarde, que la fusión de J significa "comenzar con G ". (Más concretamente, commit G será la base de combinación para esta fusión posterior).

Esa fusión solo funcionaría. Pero eso no fue lo que sucedió antes: en cambio, quien hizo la fusión usó la function llamada "squash merge". Mientras squash-merge trae los mismos cambios que una fusión real, no produce una fusión en absoluto. En su lugar, produce una única confirmación que duplica el trabajo de las comstackciones sin embargo-muchos-lo-que se fusionaron. En nuestro caso, duplica el trabajo de F y G , por lo que se ve así:

 ...--E---H--K <-- master \ F--G <-- feat/feature-a \ I--J <-- feat/feature-b 

Tenga en count la falta de un puntero desde K a G

Por lo tanto, cuando vas a fusionar (real o squash-not-really-a- "merge") feat/feature-b , Git piensa que debería comenzar con E (Técnicamente, E es la base de fusión, en lugar de G como en el caso de combinación real anterior). Esto, como viste, termina por darte un conflicto de fusión. (A menudo, "funciona" de todos modos, pero a veces, como en este caso, no sucede).

Eso está bien para el futuro, tal vez, pero ahora la pregunta es cómo solucionarlo.

Lo que quiere hacer aquí es copyr los commits exclusivamente- feat/feature-b a nuevos commits, que vienen después de K Es decir, queremos que la image se vea así:

  I'-J' <-- feat/feature-b / ...--E---H--K <-- master \ F--G <-- feat/feature-a \ I--J [no longer needed] 

La forma más sencilla de hacerlo es volver a establecer la base de estas confirmaciones, ya que rebase significa copyr. El problema es que un simple process de git checkout feat/feature-b; git rebase master git checkout feat/feature-b; git rebase master copyrá demasiados commits.

La solución es decirle a git rebase que se compromete a copyr . Para ello, cambie el argumento de master a feat/feature-a (o el hash cruda ID de commit G , básicamente, cualquier cosa que identifique el primer 1 commit no para copyr). Pero eso le dice a git rebase que los copie donde ya están; así que eso no está bien. Entonces, la solución para el nuevo problema es agregar --onto , que le permite dividir la parte "donde van las copys" de la parte "qué copyr":

 git checkout feat/feature-b git rebase --onto master feat/feature-a 

(Esto supone que todavía tiene el nombre de feat/feature-a apuntando a cometer G ; si no, tendrá que encontrar otra manera de nombrar commit G -usted puede querer dibujar su propio gráfico y / o mirar de cerca git log output, para encontrar el hash de confirmación).


1 "Primero" en estilo retro al estilo Git, eso es. Comenzamos en las confirmaciones más recientes y seguimos las conexiones hacia atrás para las confirmaciones más antiguas. Git hace todo al revés, por lo que ayuda a pensar hacia atrás aquí. 🙂

El model simple se ve así:

 X -> MA <master \ / A <feature-a 

aquí, feature-a puede haber sido aplastado a través de rebase en una única confirmación, pero la fusión sigue siendo una combinación real. Entonces tiene

 X -> MA -> MB <master \ / / A ---> B <feature-b 

donde feature-b se basa en feature-a después de cualquier aplastamiento, y también se fusionó normalmente. Este caso debería funcionar , porque git puede ver que A es un ancestro de B y que ya lo fusionaste.

Para la comparación, esto no funcionará limpiamente:

 X -> MA -> ... <master |\ / | As <feature-a | | | ^--squash------<-- \ \ A0 -> A1 -> ... -> An -> B <feature-b 

aplastó A0..n en As antes de fusionar la característica-a, pero la function-b se ramificó desde An.

Ahora git no tiene idea de cómo se relacionan As y A0..n, por lo que ni la fusión ni el rebasamiento (simple) funcionarán automáticamente. Consulte la excelente respuesta de torek si desea usar rebase --onto para solucionar esta situación.