¿Qué hace exactamente "rebase --preserve-merges" de git (y por qué?)

355

La documentaciónrebase de Git para el comando es bastante breve:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).

Entonces, ¿qué sucede realmente cuando lo usas --preserve-merges? ¿Cómo difiere del comportamiento predeterminado (sin esa bandera)? ¿Qué significa "recrear" una fusión, etc.

Chris
fuente
20
Advertencia: a partir de Git 2.18 (Q2 2018, 5 años después), en git --rebase-mergesúltima instancia, reemplazará a la anterior git --preserve-merges. Vea mi respuesta a continuación
VonC

Respuestas:

464

Al igual que con un rebase git normal, git with --preserve-mergesprimero identifica una lista de confirmaciones realizadas en una parte del gráfico de confirmaciones y luego reproduce esas confirmaciones encima de otra parte. Las diferencias con --preserve-mergespreocupación sobre qué commits se seleccionan para la reproducción y cómo funciona esa reproducción para los commits de fusión.

Para ser más explícito acerca de las principales diferencias entre rebase normal y preservación de fusión:

  • El rebase de preservación de fusión está dispuesto a reproducir (algunos) commits de fusión, mientras que el rebase normal ignora por completo los commits de fusión.
  • Debido a que está dispuesto a reproducir confirmaciones de fusión, la rebase de preservación de fusión tiene que definir lo que significa reproducir una confirmación de fusión y lidiar con algunas arrugas adicionales
    • La parte más interesante, conceptualmente, es quizás elegir cuáles deberían ser los padres de fusión del nuevo commit.
    • La repetición de las confirmaciones de fusión también requiere verificar explícitamente las confirmaciones particulares ( git checkout <desired first parent>), mientras que el rebase normal no tiene que preocuparse por eso.
  • El rebase de preservación de fusión considera un conjunto de confirmaciones menos profundo para la repetición:
    • En particular, solo considerará volver a ejecutar los commits realizados desde la (s) base (s) de fusión más reciente, es decir, la última vez que las dos ramas divergieron, mientras que el rebase normal podría repetir los commits desde la primera vez que las dos ramas divergieron.
    • Para ser provisional y poco claro, creo que esto es, en última instancia, un medio para descartar la repetición de "viejos compromisos" que ya han sido "incorporados" a un compromiso de fusión.

Primero trataré de describir "suficientemente exactamente" lo que --preserve-mergeshace rebase , y luego habrá algunos ejemplos. Por supuesto, uno puede comenzar con los ejemplos, si eso parece más útil.

El algoritmo en "breve"

Si realmente quiere meterse en la maleza, descargue la fuente git y explore el archivo git-rebase--interactive.sh. (Rebase no es parte del núcleo C de Git, sino que está escrito en bash. Y, detrás de escena, comparte código con "rebase interactivo").

Pero aquí esbozaré lo que creo que es su esencia. Para reducir la cantidad de cosas en las que pensar, me he tomado algunas libertades. (p. ej., no trato de capturar con un 100% de precisión el orden preciso en el que tienen lugar los cálculos e ignorar algunos temas que parecen menos centrales, p. ej., qué hacer con los commits que ya se han seleccionado entre las ramas).

Primero, tenga en cuenta que un rebase que no conserva la fusión es bastante simple. Es más o menos:

Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A") 
Replay all those commits onto B one at a time in order.

Rebase --preserve-mergeses comparativamente complicado. Aquí es tan simple como pude hacerlo sin perder cosas que parecen bastante importantes:

Find the commits to replay:
  First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
    This (these) merge base(s) will serve as a root/boundary for the rebase.
    In particular, we'll take its (their) descendants and replay them on top of new parents
  Now we can define C, the set of commits to replay. In particular, it's those commits:
    1) reachable from B but not A (as in a normal rebase), and ALSO
    2) descendants of the merge base(s)
  If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less:
    git log A..B --not $(git merge-base --all A B)
Replay the commits:
  Create a branch B_new, on which to replay our commits.
  Switch to B_new (i.e. "git checkout B_new")
  Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
    If it's a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
    Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c':
      To create a merge commit, its parents must exist and we must know what they are.
      So first, figure out which parents to use for c', by reference to the parents of c:
        For each parent p_i in parents_of(c):
          If p_i is one of the merge bases mentioned above:
            # p_i is one of the "boundary commits" that we no longer want to use as parents
            For the new commit's ith parent (p_i'), use the HEAD of B_new.
          Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
            # Note: Because we're moving parents-before-children, a rewritten version
            # of p_i must already exist. So reuse it:
            For the new commit's ith parent (p_i'), use the rewritten version of p_i.
          Otherwise:
            # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it
            For the new commit's ith parent (p_i'), use p_i, i.e. the old commit's ith parent.
      Second, actually create the new commit c':
        Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
        Merge in the other parent(s):
          For a typical two-parent merge, it's just "git merge p_2'".
          For an octopus merge, it's "git merge p_2' p_3' p_4' ...".
        Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
  Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")

Rebase con un --onto Cargumento debería ser muy similar. En lugar de comenzar la reproducción de confirmación en la CABEZA de B, comienza la reproducción de confirmación en la CABEZA de C. (Y use C_new en lugar de B_new).

Ejemplo 1

Por ejemplo, tome commit graph

  B---C <-- master
 /                     
A-------D------E----m----H <-- topic
         \         /
          F-------G

m es un compromiso de fusión con los padres E y G.

Supongamos que reajustamos el tema (H) en la parte superior del maestro (C) usando un rebase normal que no conserva la fusión. (Por ejemplo, tema de pago; maestro de rebase ). En ese caso, git seleccionaría las siguientes confirmaciones para la reproducción:

  • elegir D
  • elegir E
  • elegir F
  • elegir G
  • elegir H

y luego actualice el gráfico de confirmación de la siguiente manera:

  B---C <-- master
 /     \                
A       D'---E'---F'---G'---H' <-- topic

(D 'es el equivalente repetido de D, etc.)

Tenga en cuenta que merge commit m no está seleccionado para la reproducción.

Si, en su lugar, --preserve-mergesrealizamos un rebase de H sobre C. (por ejemplo, tema de pago; rebase --preserve-merges master ). En este nuevo caso, git seleccionaría los siguientes commits para la repetición:

  • elegir D
  • elegir E
  • seleccione F (en D 'en la rama' subtema ')
  • seleccione G (en F 'en la rama' subtema ')
  • elija Combinar rama 'subtema' en tema
  • elegir H

Ahora m fue elegido para la repetición. También tenga en cuenta que los padres de fusión E y G fueron seleccionados para su inclusión antes de la fusión de compromiso m.

Aquí está el gráfico de confirmación resultante:

 B---C <-- master
/     \                
A      D'-----E'----m'----H' <-- topic
        \          / 
         F'-------G'

Nuevamente, D 'es una versión escogida (es decir, recreada) de D. Igual para E', etc. Se ha reproducido cada commit que no está en master. Tanto E como G (los padres combinados de m) se han recreado como E 'y G' para servir como los padres de m '(después de la rebase, la historia del árbol sigue siendo la misma).

Ejemplo 2

A diferencia del rebase normal, el rebase que conserva la fusión puede crear varios hijos del encabezado aguas arriba.

Por ejemplo, considere:

  B---C <-- master
 /                     
A-------D------E---m----H <-- topic
 \                 |
  ------- F-----G--/ 

Si volvemos a basar H (tema) encima de C (maestro), entonces los commits elegidos para rebase son:

  • elegir D
  • elegir E
  • elegir F
  • elegir G
  • recoger m
  • elegir H

Y el resultado es así:

  B---C  <-- master
 /    | \                
A     |  D'----E'---m'----H' <-- topic
       \            |
         F'----G'---/

Ejemplo 3

En los ejemplos anteriores, tanto el commit de fusión como sus dos padres son commits repetidos, en lugar de los padres originales que tiene el commit de fusión original. Sin embargo, en otros rebases, un commit de fusión reproducido puede terminar con padres que ya estaban en el gráfico de commit antes de la fusión.

Por ejemplo, considere:

  B--C---D <-- master
 /    \                
A---E--m------F <-- topic

Si volvemos a basar el tema en maestro (preservando las fusiones), entonces los compromisos de repetición serán

  • elegir fusionar cometer m
  • elegir F

El gráfico de confirmación reescrito se verá así:

                     B--C--D <-- master
                    /       \             
                   A-----E---m'--F'; <-- topic

Aquí, el comando fusionar m 'repetido' obtiene los padres que ya existían en el gráfico de compromiso, es decir, D (la CABEZA del maestro) y E (uno de los padres del comité de fusión original m).

Ejemplo 4

La rebase de preservación de fusión puede confundirse en ciertos casos de "confirmación vacía". Al menos esto es cierto solo en algunas versiones anteriores de git (por ejemplo, 1.7.8.)

Tome este gráfico de compromiso:

                   A--------B-----C-----m2---D <-- master
                    \        \         /
                      E--- F--\--G----/
                            \  \
                             ---m1--H <--topic

Tenga en cuenta que ambos commit m1 y m2 deberían haber incorporado todos los cambios de B y F.

Si intentamos hacer git rebase --preserve-mergesde H (tema) en D (maestro), se eligen las siguientes confirmaciones para la reproducción:

  • recoger m1
  • elegir H

Tenga en cuenta que los cambios (B, F) unidos en m1 ya deberían estar incorporados en D. (Esos cambios ya deberían estar incorporados en m2, porque m2 combina los elementos secundarios de B y F.) Por lo tanto, conceptualmente, reproducir m1 encima de D probablemente debería ser un no-op o crear un commit vacío (es decir, uno donde la diferencia entre revisiones sucesivas está vacía).

En cambio, sin embargo, git puede rechazar el intento de reproducir m1 encima de D. Puede obtener un error así:

error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed

Parece que uno olvidó pasar una bandera a git, pero el problema subyacente es que a git no le gusta crear commits vacíos.

Chris
fuente
66
Me di cuenta de que git rebase --preserve-mergeses mucho más lento que un rebasesin --preserve-merges. ¿Es eso un efecto secundario de encontrar los compromisos correctos? ¿Hay algo que uno pueda hacer para acelerarlo? (Por cierto ... ¡gracias por la respuesta muy detallada!)
David Alan Hjelle
77
Parece que siempre debes usar --preserve-merges. De lo contrario, existe la posibilidad de perder el historial, es decir, la fusión se compromete.
DarVar
19
@DarVar Siempre pierdes el historial en un rebase, porque afirmas que los cambios se realizaron en una base de código diferente de la que realmente tenían.
Crónica del
55
¿Sigue siendo una "respuesta provisional"?
Andrew Grimm
55
@Chronial Por supuesto que tienes razón, ese rebase siempre incorpora perder la historia, pero quizás DarVar aludía al hecho de que no solo pierdes la historia, sino que también cambia la base del código. La resolución de conflictos contiene información que se pierde en todas las formas posibles en que se puede realizar un rebase. Siempre tienes que rehacerlo. ¿Realmente no hay forma de dejar que git rehaga su resolución de conflictos? ¿Por qué no puede git cherry-pick the merge commit?
Nils_M
94

Git 2.18 (Q2 2018) mejorará considerablemente la --preserve-mergeopción al agregar una nueva opción.

" git rebase" aprendido " --rebase-merges" para trasplantar toda la topología del gráfico de compromiso en otro lugar .

(Nota: Git 2.22, Q2 2019, en realidad se desvaloriza --preserve-merge , y Git 2.25, Q1 2020, deja de anunciarlo en el " git rebase --help" resultado )

Ver cometer 25cff9f , cometer 7543f6f , cometer 1131ec9 , cometer 7ccdf65 , cometer 537e7d6 , cometer a9be29c , cometer 8f6aed7 , cometer 1644c73 , cometer d1e8b01 , cometer 4c68e7d , cometer 9055e40 , cometer cb5206e , cometer a01c2a5 , cometer 2f6b1d1 , cometer bf5c057 (25 abr 2018) por Johannes Schindelin ( dscho) .
Ver commit f431d73 (25 abr 2018) por Stefan Beller ( stefanbeller) .
Ver commit 2429335 (25 de abril de 2018) por Phillip Wood ( phillipwood) .
(Fusionada por Junio ​​C Hamano - gitster- en commit 2c18e6a , 23 de mayo de 2018)

pull: aceptar --rebase-mergespara recrear la topología de la rama

Similar al preservemodo que simplemente pasa la --preserve-merges opción al rebasecomando, el mergesmodo simplemente pasa la --rebase-mergesopción.

Esto permitirá a los usuarios reabastecer convenientemente las topologías de confirmación no triviales al extraer nuevas confirmaciones, sin aplanarlas.


git rebaseLa página de manual ahora tiene una sección completa dedicada al cambio de la historia con fusiones .

Extraer:

Existen razones legítimas por las que un desarrollador puede querer recrear confirmaciones de fusión: para mantener la estructura de la rama (o "topología de confirmación") cuando se trabaja en múltiples ramas interrelacionadas.

En el siguiente ejemplo, el desarrollador trabaja en una rama de tema que refactoriza la forma en que se definen los botones, y en otra rama de tema que usa esa refactorización para implementar un botón "Informar un error".
La salida de git log --graph --format=%s -5puede verse así:

*   Merge branch 'report-a-bug'
|\
| * Add the feedback button
* | Merge branch 'refactor-button'
|\ \
| |/
| * Use the Button class for all buttons
| * Extract a generic Button class from the DownloadButton one

Es posible que el desarrollador quiera volver a basar esas confirmaciones en una nueva master mientras mantiene la topología de la rama, por ejemplo, cuando se espera que la primera rama del tema se integre mastermucho antes que la segunda, por ejemplo, para resolver conflictos de fusión con cambios en la DownloadButtonclase que hizo en master.

Este rebase se puede realizar usando la --rebase-mergesopción


Ver commit 1644c73 para un pequeño ejemplo:

rebase-helper --make-script: introduce una bandera para volver a combinar las fusiones

El secuenciador acaba de aprender nuevos comandos destinados a recrear la estructura de la rama ( similar en espíritu a --preserve-merges, pero con un diseño sustancialmente menos roto ).

Permitamos rebase--helperque genere listas de tareas haciendo uso de estos comandos, activados por la nueva --rebase-mergesopción.
Para una topología de confirmación como esta (donde HEAD apunta a C):

- A - B - C (HEAD)
    \   /
      D

la lista de tareas generada se vería así:

# branch D
pick 0123 A
label branch-point
pick 1234 D
label D

reset branch-point
pick 2345 B
merge -C 3456 D # C

¿Cuál es la diferencia con --preserve-merge?
Commit 8f6aed7 explica:

Érase una vez, este desarrollador aquí pensó: ¿no sería bueno si, por ejemplo, los parches de Git para Windows en la parte superior del núcleo Git pudieran representarse como un matorral de ramas y volverse a basar en la parte superior del núcleo de Git para ¿Mantener un conjunto de series de parches que se pueden elegir fácilmente?

El original intento de responder a esta era: git rebase --preserve-merges.

Sin embargo, ese experimento nunca tuvo la intención de ser una opción interactiva, y solo fue respaldado git rebase --interactiveporque la implementación de ese comando ya parecía muy, muy familiar: fue diseñada por la misma persona que diseñó --preserve-merges: la suya verdaderamente.

Y por "el suyo verdaderamente", el autor se refiere a sí mismo: Johannes Schindelin ( dscho) , quien es la razón principal (con algunos otros héroes - Hannes, Steffen, Sebastian, ...) de que tenemos Git para Windows (aunque atrás en el día - 2009 - eso no fue fácil ).
Está trabajando en Microsoft desde septiembre de 2015 , lo que tiene sentido teniendo en cuenta que Microsoft ahora usa mucho Git y necesita sus servicios.
Esa tendencia comenzó en 2013 en realidad, con TFS . Desde entonces, Microsoft administra el repositorio de Git más grande del planeta . Y, desde octubre de 2018, Microsoft adquirió GitHub .

Puedes ver a Johannes hablar en este video para Git Merge 2018 en abril de 2018.

Algún tiempo después, otro desarrollador (¡te estoy mirando, Andreas! ;-)) decidió que sería una buena idea permitir --preserve-mergesque se combinara --interactive(¡con advertencias!) Y el mantenedor de Git (bueno, el mantenedor de Git interino durante la ausencia de Junio, eso es) de acuerdo, y fue entonces cuando el glamour del --preserve-mergesdiseño comenzó a desmoronarse bastante rápido y sin glamour.

Aquí Jonathan está hablando de Andreas Schwab de Suse.
Puedes ver algunas de sus discusiones en 2012 .

¿La razón? En el --preserve-mergesmodo, los padres de una confirmación de fusión (o, para el caso, de cualquier confirmación) no se indicaron explícitamente, sino que estaban implícitos en el nombre de confirmación pasado al pickcomando .

Esto hizo imposible, por ejemplo, reordenar los commits .
Por no hablar de mover confirmaciones entre ramas o, Dios no lo quiera, dividir ramas temáticas en dos.

Lamentablemente, estas deficiencias también impidieron que ese modo (cuyo propósito original era servir las necesidades de Git para Windows, con la esperanza adicional de que también podría ser útil para otros) sirviera las necesidades de Git para Windows.

Cinco años más tarde, cuando se volvió realmente insostenible tener una serie de parches hodge-podge grandes y poco manejables de parches en parte relacionados y en parte no relacionados en Git para Windows que de vez en cuando se basaban en las etiquetas centrales de Git (ganando la ira inmerecida del desarrollador de la git-remote-hgserie desafortunada que primero dejó obsoleto el enfoque competitivo de Git para Windows, solo para ser abandonado sin mantenedor más tarde) fue realmente insostenible, nacieron las " tijeras de jardín Git " : un guión, que se apoya en la parte superior del rebase interactivo, eso determinaría primero la topología de ramificación de los parches que se volverán a crear, creará una lista de pseudo tareas para su posterior edición, transformará el resultado en una lista de tareas real (haciendo un uso intensivo deexec comando para "implementar" los comandos de la lista de tareas que faltan) y finalmente recrear la serie de parches en la parte superior del nuevo compromiso base.

(El script de tijeras de jardín Git se menciona en este parche en commit 9055e40 )

Eso fue en 2013.
Y tomó alrededor de tres semanas elaborar el diseño e implementarlo como un script fuera de árbol. Huelga decir que la implementación necesitó bastantes años para estabilizarse, mientras que el diseño en sí demostró ser sólido.

Con este parche, la bondad de las tijeras de jardín Git trata de la git rebase -imisma .
Pasar la --rebase-mergesopción generará una lista de tareas que se puede entender fácilmente y donde es obvio cómo reordenar las confirmaciones .
Se pueden introducir nuevas ramas insertando labelcomandos y llamando merge <label>.
Y una vez que este modo se haya estabilizado y aceptado universalmente, podemos desaprobar el error de diseño que fue--preserve-merges .


Git 2.19 (Q3 2018) mejora la nueva --rebase-mergesopción al hacer que funcione --exec.

La " --exec" opción de " git rebase --rebase-merges" colocó los comandos exec en lugares incorrectos, lo que se ha corregido.

Ver commit 1ace63b (09 de agosto de 2018), y commit f0880f7 (06 de agosto de 2018) por Johannes Schindelin ( dscho) .
(Fusionada por Junio ​​C Hamano - gitster- en commit 750eb11 , 20 de agosto de 2018)

rebase --exec: haz que funcione con --rebase-merges

La idea de --execes agregar una execllamada después de cada uno pick.

Desde la introducción del fixup!/ s quash!commit, esta idea se extendió a aplicarse a "pick, posiblemente seguida de una corrección / cadena calabaza", es decir, un exec no se inserta entre una picky cualquiera de sus correspondientes fixupo squashlíneas.

La implementación actual utiliza un truco sucio para lograrlo: se supone que solo hay comandos pick / fixup / squash, y luego inserta las execlíneas antes que cualquier otra que pickno sea la primera, y agrega una final.

Con las listas de tareas generadas por git rebase --rebase-merges, esta implementación simple muestra sus problemas: produce exactamente lo incorrecto cuando hay label, resety mergecomandos.

Cambiemos la implementación para hacer exactamente lo que queremos: buscar picklíneas, omitir cualquier cadena de arreglo / aplastar, y luego insertar la exec línea . Enjabonar, enjuagar, repetir.

Nota: nos esforzamos por insertar antes de las líneas de comentarios siempre que sea posible, ya que las confirmaciones vacías están representadas por líneas de selección comentadas (y queremos insertar una línea ejecutiva de una selección anterior antes de dicha línea, no después).

Mientras lo hace, también agregue execlíneas después de los mergecomandos, porque son similares en espíritu a los pickcomandos: agregan nuevos commits.


Git 2.22 (Q2 2019) corrige el uso de las referencias / reescritura / jerarquía para almacenar un estado intermedio de rebase, que inherentemente hace que la jerarquía por árbol de trabajo.

Ver commit b9317d5 , commit 90d31ff , commit 09e6564 (07 mar 2019) por Nguyễn Thái Ngọc Duy ( pclouds) .
(Fusionada por Junio ​​C Hamano - gitster- en commit 917f2cd , 09 abr 2019)

Asegúrese de que las referencias / reescritas / sean por árbol de trabajo

a9be29c (secuenciador: crea referencias generadas por el labelcomando worktree-local, 2018-04-25, Git 2.19) agrega refs/rewritten/como espacio de referencia por árbol de trabajo.
Desafortunadamente (lo malo) hay un par de lugares que necesitan actualización para asegurarse de que realmente sea por árbol de trabajo.

- add_per_worktree_entries_to_dir()se actualiza para asegurarse de que el listado de referencia mire por árbol de trabajo en refs/rewritten/lugar de uno por repositorio.

  • common_list[]se actualiza para que git_path()devuelva la ubicación correcta. Esto incluye " rev-parse --git-path".

Este desastre es creado por mí.
Comencé a tratar de arreglarlo con la introducción de refs/worktree,dónde todas las referencias serán por árbol de trabajo sin tratamientos especiales.
Las referencias desafortunadas / reescritas llegaron antes que las referencias / worktree, así que esto es todo lo que podemos hacer.


Con Git 2.24 (cuarto trimestre de 2019), " git rebase --rebase-merges" aprendió a manejar diferentes estrategias de fusión y pasarles opciones específicas de estrategia.

Ver commit 476998d (04 Sep 2019) por Elijah Newren ( newren) .
Ver cometer e1fac53 , comprometerse a63f990 , comprometerse 5dcdd74 , comprometerse e145d99 , comprometerse 4e6023b , comprometerse f67336d , comprometerse a9c7107 , comprometerse b8c6f24 , comprometerse d51b771 , comprometerse c248d32 , comprometerse 8c1e240 , comprometerse 5efed0e , comprometerse 68b54f6 , comprometerse 2e7bbac , comprometerse 6180b20 , comprometerse d5b581f (31 Jul 2019) porJohannes Schindelin ( dscho) .
(Fusionada por Junio ​​C Hamano - gitster- en commit 917a319 , 18 sep 2019)


Con Git 2.25 (Q1 2020), la lógica utilizada para diferenciar las referencias globales del repositorio local y del repositorio es fija, para facilitar la fusión-preservación.

Ver commit f45f88b , commit c72fc40 , commit 8a64881 , commit 7cb8c92 , commit e536b1f (21 oct 2019) por SZEDER Gábor ( szeder) .
(Fusionada por Junio ​​C Hamano - gitster- en commit db806d7 , 10 nov 2019)

path.c: no llame a la matchfunción sin valor entrie_find()

Firmado por: SZEDER Gábor

'logs / refs' no es una ruta de trabajo específica del árbol, pero desde commit b9317d55a3 (Asegúrese de que refs / rewritten / is per-worktree, 2019-03-07, v2.22.0-rc0) ' git rev-parse --git-path' ha estado devolviendo una ruta falsa si hay un final ' /' presente:

$ git -C WT/ rev-parse --git-path logs/refs --git-path logs/refs/
/home/szeder/src/git/.git/logs/refs
/home/szeder/src/git/.git/worktrees/WT/logs/refs/

Usamos una trieestructura de datos para decidir de manera eficiente si una ruta pertenece al directorio común o está trabajando en un árbol específico.

Como sucede, b9317d55a3 desencadenó un error que es tan antiguo como la trieimplementación en sí, agregado en 4e09cf2acf (" path: optimizar la comprobación de directorio común", 2015-08-31, Git v2.7.0-rc0 - fusión incluida en el lote # 2 ).

  • De acuerdo con el comentario que describe trie_find(), solo debería llamar a la función de coincidencia dada 'fn' para un "/ -o- \ 0-terminado prefijo de la clave para la cual el trie contiene un valor".
    Esto no es cierto: hay tres lugares donde trie_find () llama a la función de coincidencia, pero a uno de ellos le falta la verificación de la existencia del valor.

  • b9317d55a3 agregó dos nuevas claves a trie:

    • ' logs/refs/rewritten' y
    • ' logs/refs/worktree', junto al ya existente ' logs/refs/bisect'.
      Esto dio como resultado un trienodo con la ruta ' logs/refs/', que no existía antes y que no tiene un valor asociado.
      Una consulta para ' logs/refs/' encuentra este nodo y luego toca ese sitio de llamada de la matchfunción que no verifica la existencia del valor y, por lo tanto, invoca la matchfunción con un NULLvalor.
  • Cuando la matchfunción check_common()se invoca con un NULLvalor, devuelve 0, lo que indica que la ruta consultada no pertenece al directorio común, lo que finalmente resulta en la ruta falsa que se muestra arriba.

Agregue la condición que falta para trie_find()que nunca invoque la función de coincidencia con un valor no existente.

check_common() entonces ya no tendrá que verificar que obtuvo un valor no NULL, por lo tanto, elimine esa condición.

Creo que no hay otros caminos que puedan causar una salida falsa similar.

AFAICT la única otra tecla que resulta en que la función de coincidencia se llame con un NULLvalor es ' co' (debido a las teclas ' common' y ' config').

Sin embargo, como no están en un directorio que pertenece al directorio común, se espera la ruta de trabajo específica resultante del árbol.

VonC
fuente
3
Creo que esta debería ser la respuesta principal, en --preserve-mergesrealidad no "preserva" las fusiones como quieras, es muy ingenuo. Esto le permite preservar los compromisos de fusión y sus relaciones de compromiso principal al tiempo que le brinda la flexibilidad de un rebase interactivo. Esta nueva característica es increíble y si no fuera por esta respuesta SO bien escrita, ¡no lo habría sabido!
egucciar el
@egucciar Gracias. Y no es la única característica de Git 2.18 ( stackoverflow.com/search?q=user%3A6309+%22git+2.18%22 ), y Git 2.19 ( stackoverflow.com/search?q=user%3A6309+%22git+2.19% 22 )
VonC
1
Extremadamente útil si está intentando mover trozos de commits como en este Q / A, stackoverflow.com/questions/45059039/…
okovko
1
¡Oh, eso es lo que había estado buscando desde hace tiempo! Tuve una solución manual para casos como ese en el que uno debería crear un compromiso ficticio uniendo todas las fusiones.
carnicer
Típico Git. Te atreves a hacer una pregunta simple y lo más probable es que tengas que aprender la historia de Git, los algoritmos internos, todos los detalles de implementación desordenados, además de que también necesitas una especialización en teoría de gráficos para comprender lo que está sucediendo.
Dimitris