¿Git rebasing no funciona correctamente?

Parece que Git rebase no funciona de la manera que yo esperaba, de acuerdo con mi comprensión del rebase, y de cómo he visto el trabajo de rebase en Mercurial. He generado un ejemplo para ilustrar el comportamiento extraño, y espero que alguien pueda explicar por qué Git se comporta de la manera que es. Considere este estado del DAG:

Estado inicial

En este escenario, hice los commits f7 y f8 en master, pero quiero mover estos nuevos commits a la twig de características en su lugar. es decir, hice los commits en la twig incorrecta y quiero corregir el error. Puedo realizar la rebase desde SourceTree:

Elige Rebase

SourceTree confirma mi intención:

Rebase Confirmar

Pero el resultado no es en absoluto lo que esperaría:

Resultado de Rebase

¡Aunque los nodos están en la position correcta en el DAG, los cabezales de bifurcación son incorrectos! Al volver a basar f7 y f8 en f6, espero ver el restablecimiento maestro en su position en el origen, y espero que la característica / AAA-1 avance a f8. Me gusta esto:

Rebase corregido

Este es el comportamiento que esperaría, basado en el rebasamiento en Mercurial, y generalmente basado en lo que está haciendo el rebasamiento. ¿Por qué Git hizo esto y cómo hago que se comporte correctamente?

Viniendo de Mercurial, esperas que una confirmación esté permanentemente unida a una twig específica. Es decir, si logras extraer algún compromiso de forma aislada, tienes algo que dice:

I am a commit on branch foo. I change file bar. 

Git no funciona de esta manera: una confirmación es independiente de cualquier twig (nombre), y de hecho, los nombres de las twigs -tags, si se quiere- pueden pelarse y atascarse en cualquier otro lugar. No tienen ningún uso 1, excepto para los humanos que intentan interpretar el desastre.

En Mercurial, cuando "rebase" algunos sets de cambios, usted (en efecto al less) los descarta como diffs contra sus bases, luego cambia a la otra twig en la que los quiere y realiza nuevos commits en esa otra twig . Mercurial solía (tal vez todavía lo hace) llamar a este primer paso, "injertar". Estas nuevas confirmaciones ahora están adjuntas permanentemente a (y solo a) esta otra twig diferente:

 master: f1 - f2 - f3 - f4 - f7 - f8 \ feature/AAA-1: f5 - f6 

se convierte en:

 master: f1 - f2 - f3 - f4 - f7 - f8 \ feature/AAA-1: f5 - f6 - 9 - 10 

En este punto, puede "deshacer de manera segura" f7 y f8, quitándolos de la línea master , y su rebase finaliza con las copys solo en la otra twig.

Tenga en count que dibujo las tags de twig a la izquierda aquí. Esto es seguro porque todas las confirmaciones están permanentemente pegadas a sus twigs, por lo que una vez que un set de cambios está en la línea de su twig, siempre está en la línea de su twig. La única vez que hay una violación de la regla "changeset goes on the (single) line of its branch" es para una combinación, cuando un set de cambios se conecta a (exactamente) dos twigs: se sienta en su twig principal, pero dibuja una connection a la otra twig.

En git, por otro lado, se puede considerar que un commit está "en" cero o más twigs (no hay una restricción "exactamente 1 o 2"), y el set de twigs que un commit está "activado" es dynamic como nombres de twig se puede agregar o eliminar en cualquier momento. (Tenga en count también que la palabra "twig" tiene al less dos significados en git ).

La database de Git funciona de forma muy similar a la de Mercurial: en realidad copy los compromisos. Pero hay una diferencia importante para comenzar: las copys no están específicamente "encendidas" en ninguna twig (y de hecho el process de rebase no opera en ninguna twig, usando lo que git llama "HEAD desconectada"). Entonces, hay una diferencia aún más importante al final.

Como antes, podemos comenzar con un dibujo de gráfico, pero esta vez lo dibujaré un poco diferente:

  f7 <- f8 <-- master / f1 <- f2 <- f3 <- f4 \ f5 <- f6 <-- feature/AAA-1 

Esta vez, las tags están a la derecha, con flechas. El nombre master realmente apunta directamente a cometer f8 , y es f8 que apunta a f7 y f7 apunta a f4 y así sucesivamente.

Lo que esto significa es que, en este momento, los commits f1 a f4 están "on" en ambas twigs. Con git, es mejor decir que estas confirmaciones están "contenidas en" (la historia de) ambas twigs. No hay nada en ninguno de ellos que se comprometa a decir en qué twig estaban originalmente "hechos": llevan sus pointers principales, identificadores de tree de origen y nombres de autor y comstackdor (y marcas de time, etc.), pero no "nombre de twig de origen". (Los recién llegados a git from hg suelen encontrar esto bastante frustrante, creo).

Si ahora le pides a git que rebase f7 y f8 en feature/AAA-1 , git hará copys de los dos commits:

  f7 <- f8 / f1 <- f2 <- f3 <- f4 \ f5 <- f6 <- f7' <- f8' 

(las ' marcas, o f7prime y f8prime, significan que estas son copys de los originales- git cherry-pick , análogos a los injertos de hg). Pero ahora llegamos a la diferencia key, la que te hace tropezar: ahora Git "se desprende" de la label master original y lo hace apuntar a la nueva sugerencia de sugerencia en su lugar. Esto significa que el gráfico final se ve así:

  f7 <- f8 [abandoned -- was master] / f1 <- f2 <- f3 <- f4 \ f5 <- f6 <-- feature/AAA-1 \ f7' <- f8' <-- master 

Mercurial no puede hacer esto: las tags de las twigs no se pueden pelar, mezclar y volver a pegar en otro lugar. Entonces no es así, y es por eso que su rebase funciona de manera diferente.

En git, lo que quieres hacer aquí es simplemente seleccionar las dos confirmaciones en la feature/AAA-1 twig feature/AAA-1 , y luego eliminarlas de la twig master :

 $ git checkout feature/AAA-1 $ git cherry-pick master~2..master # copy the commits $ git checkout master $ git reset --hard master~2 # back up over the originals 

La idea aquí es que no estás reajustando el master en absoluto, y ni siquiera estás realmente modificando tu twig de características: en su lugar, solo estás copyndo dos confirmaciones en tu twig de características, y luego eliminándolas del maestro.


1 Esto es un poco exagerado, ya que las transferencias entre repositorys, git fetch y git push use, también utilizan tags de twig y label. Además, necesita algunas references a commits para mantenerlos vivos, de lo contrario el recolector de basura de git eventualmente los cosechará como "inalcanzable".

Parece que no estás realmente interesado en el rebase. Solo quiere mover las twigs para señalar nuevas cabezas. Eso es bastante fácil, en este caso. No uso SourceTree, pero es bastante simple de hacer en la CLI:

 git checkout feature/AAA-1 git reset --hard master git checkout master git reset --hard origin/master 

Su expectativa es correcta, pero creo que la confusión está en la terminología. De git-scm :

En este ejemplo, ejecutarías lo siguiente:

 $ git checkout experiment $ git rebase master First, rewinding head to replay your work on top of it... Applying: added staged command 

Funciona yendo al ancestro común de las dos twigs (en la que estás y en la que estás refinanciando) …

Tenga en count el uso de "on" y "on" en este caso. En git, la twig sobre la que estás reescalando es la twig que será un subtree después de la rebase. Sospecho que tu uso de SourceTree es al revés para git.

El propósito por el cual git rebase existe es: Usted cometió algo para mostrar feature/AAA , mientras tanto, alguien más presionó para realizar cambios en el origin/feature/AAA . Ahora la feature/AAA (actual) y origin/feature/AAA son divergentes. Podría fusionarlos, pero las reglas del proyecto establecen que la historia debe ser lineal. Entonces, en su lugar, ejecuta git rebase y su feature/AAA twig actual feature/AAA se basa en el origin/feature/AAA más reciente.

Tu caso es diferente. Probablemente, podría haber un command para mover las confirmaciones de una twig a otra, nombrando explícitamente ambas, pero no es una rebase.