¿Por qué git-rebase me da conflictos de fusión cuando todo lo que estoy haciendo es aplastar commits?

146

Tenemos un repositorio Git con más de 400 confirmaciones, las primeras dos docenas de las cuales fueron muchas pruebas y errores. Queremos limpiar estos commits aplastando muchos en un solo commit. Naturalmente, git-rebase parece el camino a seguir. Mi problema es que termina con conflictos de fusión, y estos conflictos no son fáciles de resolver. No entiendo por qué debería haber ningún conflicto, ya que solo estoy aplastando las confirmaciones (no eliminando ni reorganizando). Muy probablemente, esto demuestra que no estoy entendiendo completamente cómo git-rebase hace sus aplastamientos.

Aquí hay una versión modificada de los scripts que estoy usando:


repo_squash.sh (este es el script que realmente se ejecuta):


rm -rf repo_squash
git clone repo repo_squash
cd repo_squash/
GIT_EDITOR=../repo_squash_helper.sh git rebase --strategy theirs -i bd6a09a484b8230d0810e6689cf08a24f26f287a

repo_squash_helper.sh (este script solo lo usa repo_squash.sh):


if grep -q "pick " $1
then
#  cp $1 ../repo_squash_history.txt
#  emacs -nw $1
  sed -f ../repo_squash_list.txt < $1 > $1.tmp
  mv $1.tmp $1
else
  if grep -q "initial import" $1
  then
    cp ../repo_squash_new_message1.txt $1
  elif grep -q "fixing bad import" $1
  then
    cp ../repo_squash_new_message2.txt $1
  else
    emacs -nw $1
  fi
fi

repo_squash_list.txt: (este archivo solo lo usa repo_squash_helper.sh)


# Initial import
s/pick \(251a190\)/squash \1/g
# Leaving "Needed subdir" for now
# Fixing bad import
s/pick \(46c41d1\)/squash \1/g
s/pick \(5d7agf2\)/squash \1/g
s/pick \(3da63ed\)/squash \1/g

Dejaré el contenido del "nuevo mensaje" a tu imaginación. Inicialmente, hice esto sin la opción "--estrategia suya" (es decir, usando la estrategia predeterminada, que si entiendo la documentación correctamente es recursiva, pero no estoy seguro de qué estrategia recursiva se usa), y tampoco lo hizo. t trabajo. Además, debo señalar que, utilizando el código comentado en repo_squash_helper.sh, guardé el archivo original en el que funciona el script sed y ejecuté el script sed contra él para asegurarme de que estaba haciendo lo que quería que hiciera ( era). De nuevo, ni siquiera sé por qué habría un conflicto, por lo que no parece importar mucho qué estrategia se utilice. Cualquier consejo o idea sería útil, pero sobre todo solo quiero que este aplastamiento funcione.

Actualizado con información adicional de la discusión con Jefromi:

Antes de trabajar en nuestro repositorio masivo "real", utilicé scripts similares en un repositorio de prueba. Era un repositorio muy simple y la prueba funcionó limpiamente.

El mensaje que recibo cuando falla es:

Finished one cherry-pick.
# Not currently on any branch.
nothing to commit (working directory clean)
Could not apply 66c45e2... Needed subdir

Esta es la primera elección después de la primera confirmación de squash. La ejecución git statusproduce un directorio de trabajo limpio. Si luego hago un git rebase --continue, recibo un mensaje muy similar después de algunas confirmaciones más. Si luego lo hago nuevamente, recibo otro mensaje muy similar después de un par de docenas de confirmaciones. Si lo hago una vez más, esta vez pasa por un centenar de confirmaciones y muestra este mensaje:

Automatic cherry-pick failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>', and
run 'git rebase --continue'
Could not apply f1de3bc... Incremental

Si luego corro git status, obtengo:

# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   repo/file_A.cpp
# modified:   repo/file_B.cpp
#
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified:      repo/file_X.cpp
#
# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# deleted:    repo/file_Z.imp

El bit "ambos modificados" me parece extraño, ya que esto fue solo el resultado de una elección. También vale la pena señalar que si miro el "conflicto", se reduce a una sola línea con una versión que comienza con un carácter [tabulador] y la otra con cuatro espacios. Esto parecía ser un problema con la forma en que configuré mi archivo de configuración, pero no hay nada de eso. (Noté que core.ignorecase está configurado como verdadero, pero evidentemente git-clone lo hizo automáticamente. No estoy completamente sorprendido por eso teniendo en cuenta que la fuente original estaba en una máquina con Windows).

Si reparo manualmente file_X.cpp, falla poco después con otro conflicto, esta vez entre un archivo (CMakeLists.txt) que una versión piensa que debería existir y una versión piensa que no debería. Si soluciono este conflicto diciendo que sí quiero este archivo (que es lo que hago), algunas confirmaciones más tarde obtengo otro conflicto (en este mismo archivo) donde ahora hay algunos cambios no triviales. Todavía es solo alrededor del 25% del camino a través de los conflictos.

También debo señalar, ya que esto podría ser muy importante, que este proyecto comenzó en un repositorio svn. Ese historial inicial muy probablemente fue importado de ese repositorio svn.

Actualización n. ° 2:

En una alondra (influenciado por los comentarios de Jefromi), decidí hacer el cambio de mi repo_squash.sh para ser:

rm -rf repo_squash
git clone repo repo_squash
cd repo_squash/
git rebase --strategy theirs -i bd6a09a484b8230d0810e6689cf08a24f26f287a

Y luego, acabo de aceptar las entradas originales, tal como están. Es decir, el "rebase" no debería haber cambiado nada. Terminó con los mismos resultados descritos anteriormente.

Actualización n. ° 3:

Alternativamente, si omito la estrategia y reemplazo el último comando con:

git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a

Ya no recibo los problemas de rebase "nada que cometer", pero todavía me quedan los otros conflictos.

Actualice con el repositorio de juguetes que recrea el problema:

test_squash.sh (este es el archivo que realmente ejecuta):

#========================================================
# Initialize directories
#========================================================
rm -rf test_squash/ test_squash_clone/
mkdir -p test_squash
mkdir -p test_squash_clone
#========================================================

#========================================================
# Create repository with history
#========================================================
cd test_squash/
git init
echo "README">README
git add README
git commit -m"Initial commit: can't easily access for rebasing"
echo "Line 1">test_file.txt
git add test_file.txt
git commit -m"Created single line file"
echo "Line 2">>test_file.txt 
git add test_file.txt 
git commit -m"Meant for it to be two lines"
git checkout -b dev
echo Meaningful code>new_file.txt
git add new_file.txt 
git commit -m"Meaningful commit"
git checkout master
echo Conflicting meaningful code>new_file.txt
git add new_file.txt 
git commit -m"Conflicting meaningful commit"
# This will conflict
git merge dev
# Fixes conflict
echo Merged meaningful code>new_file.txt
git add new_file.txt
git commit -m"Merged dev with master"
cd ..

#========================================================
# Save off a clone of the repository prior to squashing
#========================================================
git clone test_squash test_squash_clone
#========================================================

#========================================================
# Do the squash
#========================================================
cd test_squash
GIT_EDITOR=../test_squash_helper.sh git rebase -i HEAD@{7}
#========================================================

#========================================================
# Show the results
#========================================================
git log
git gc
git reflog
#========================================================

test_squash_helper.sh (utilizado por test_sqash.sh):

# If the file has the phrase "pick " in it, assume it's the log file
if grep -q "pick " $1
then
  sed -e "s/pick \(.*\) \(Meant for it to be two lines\)/squash \1 \2/g" < $1 > $1.tmp
  mv $1.tmp $1
# Else, assume it's the commit message file
else
# Use our pre-canned message
  echo "Created two line file" > $1
fi

PD: Sí, sé que algunos de ustedes se encogen cuando me ven usando emacs como editor alternativo.

PPS: Sabemos que tendremos que eliminar todos nuestros clones del repositorio existente después del rebase. (En la línea de "no rebasarás un repositorio después de que se haya publicado").

PPPS: ¿Alguien puede decirme cómo agregar una recompensa a esto? No veo la opción en ninguna parte de esta pantalla si estoy en modo de edición o modo de vista.

Ben Hocking
fuente
Más relevante que los guiones utilizados es el intento final de acción: parece bastante seguro que será una lista de selección y squash entremezclados, ¿verdad? ¿Y hay algún compromiso de fusión en la rama rebaseada? (Aunque no lo estás usando de rebase -ptodos modos)
Cascabel
No estoy seguro de lo que quieres decir con "intento final de acción", pero es solo una lista de selección y squash entremezcladas, con las últimas 400 más o menos todas seleccionadas. No hay fusiones en esa lista, aunque el rebase en sí está realizando sus propias fusiones. Según lo que he leído, "rebase -p" no se recomienda con el modo interactivo (que en mi caso no es tan interactivo, por supuesto). De kernel.org/pub/software/scm/git/docs/git-rebase.html : "Esto usa la maquinaria interactiva internamente, pero combinarla con la opción interactiva explícitamente generalmente no es una buena idea"
Ben Hocking
Por "intento final de acción" me refería a la lista de selección / squash que se pasó de nuevo rebase --interactive, esas son una especie de lista de acciones para que git intente. Esperaba que pudieras reducir esto a una sola calabaza que causaba conflictos y evitar toda la complejidad adicional de tus scripts de ayuda. La otra información que falta es cuándo ocurren los conflictos: ¿cuándo git aplica los parches para formar la calabaza o cuando intenta pasar de la calabaza y aplicar el siguiente parche? (Y ¿está seguro de que nada malo sucede con su kludge GIT_EDITOR Otro voto para el caso simple prueba?.)
Cascabel
Gracias por los útiles comentarios. He actualizado la pregunta para reflejar mis respuestas.
Ben Hocking
2
El caso del repositorio de juguetes es mucho, mucho más fácil de entender que sus guiones, y muestra que los guiones no tienen nada que ver con eso, solo el hecho de que está tratando de volver a crear una rama que contiene una fusión con resolución de conflictos (una fusión levemente malvada )
Cascabel

Respuestas:

69

Muy bien, tengo la confianza suficiente para lanzar una respuesta. Quizás tenga que editarlo, pero creo que sé cuál es su problema.

Su caso de prueba de repositorio de juguete tiene una fusión, peor aún, tiene una fusión con conflictos. Y estás rebatiendo a través de la fusión. Sin -p(que no funciona totalmente con -i), las fusiones se ignoran. Esto significa que todo lo que hiciste en tu resolución de conflictos no está allí cuando el rebase intenta elegir el próximo commit, por lo que su parche puede no aplicarse. (Creo que esto se muestra como un conflicto de fusión porque git cherry-pickpuede aplicar el parche haciendo una fusión de tres vías entre la confirmación original, la confirmación actual y el antepasado común).

Desafortunadamente, como señalamos en los comentarios, -iy -p(preservar fusiones) no se llevan muy bien. Sé que la edición / reescritura funciona, y que la reordenación no. Sin embargo, creo que funciona bien con las calabazas. Esto no está documentado, pero funcionó para los casos de prueba que describo a continuación. Si su caso es mucho más complejo, es posible que tenga muchos problemas para hacer lo que quiere, aunque aún será posible. (Moraleja de la historia: limpiar rebase -i antes de fusionar).

Entonces, supongamos que tenemos un caso muy simple, donde queremos juntar A, B y C:

- o - A - B - C - X - D - E - F (master)
   \             /
    Z -----------

Ahora, como dije, si no hubiera conflictos en X, git rebase -i -pfunciona como es de esperar.

Si hay conflictos, las cosas se ponen un poco más complicadas. Hará un buen aplastamiento, pero luego, cuando intente recrear la fusión, los conflictos volverán a ocurrir. Tendrá que resolverlos nuevamente, agregarlos al índice y luego usar git rebase --continuepara continuar . (Por supuesto, puede resolverlos nuevamente revisando la versión del commit de fusión original).

Si le sucede que tiene rererehabilitadas en su repo ( rerere.enabledconjunto en true), esta será manera más fácil - Git será capaz de volver a utilizar el nuevo con cable re solución a partir de cuando se tenía originalmente los conflictos, y todo lo que tiene que hacer es inspeccionarlo para asegurarse de que funcionó correctamente, agregue los archivos al índice y continúe. (Incluso puede ir un paso más allá, encendiéndose rerere.autoupdate, y los agregará por usted, para que la fusión ni siquiera falle). Supongo, sin embargo, que nunca habilitaste rerere, por lo que tendrás que resolver el conflicto tú mismo. *

* O bien, puede probar el rerere-train.shscript de git-contrib, que intenta "cebar [la] recuperación de la base de datos de los compromisos de fusión existentes", básicamente, comprueba todos los compromisos de fusión, intenta fusionarlos y, si la fusión falla, toma los resultados y se los muestra git-rerere. Esto podría llevar mucho tiempo, y en realidad nunca lo he usado, pero podría ser muy útil.

Cascabel
fuente
PD: Mirando hacia atrás a mis comentarios, veo que debería haber captado esto antes. Le pregunté si estaba reorganizando una rama que contenía fusiones, y dijo que no había fusiones en la lista interactiva de rebase, lo cual no es lo mismo, no hubo fusiones allí porque no proporcionó la -popción.
Cascabel
Definitivamente voy a intentar esto. Desde que publiqué esto, también he notado que en algunos casos, simplemente puedo escribir git commit -a -m"Some message"y git rebase --continue, y continuará. Esto funciona incluso sin la -popción, pero funciona aún mejor con la -popción (ya que no estoy haciendo ningún reordenamiento, parece que -pestá bien). De todos modos, los mantendré informados.
Ben Hocking
La configuración git config --global rerere.enabled truey git config --global rerere.autoupdate trueantes de ejecutar el ejemplo de prueba resuelve el problema principal. Curiosamente, sin embargo, no conserva las fusiones, incluso cuando se especifica --preserve-merges. Sin embargo, si no tengo esos conjuntos, y escribo git commit -a -m"Meaningful commit"y git rebase --continueal especificar --preserve-merges, conserva las fusiones. De todos modos, ¡gracias por ayudarme a superar este problema!
Ben Hocking
84

Si no te importa crear una nueva rama, así es como solucioné el problema:

Estar en main:

# create a new branch
git checkout -b new_clean_branch

# apply all changes
git merge original_messy_branch

# forget the commits but have the changes staged for commit
git reset --soft main        

git commit -m "Squashed changes from original_messy_branch"
hlidka
fuente
3
La solución más rápida y eficiente :)
IslamTaha
2
Esta fue una gran sugerencia! Funcionó muy bien para consolidar rápidamente varios commits.
philidem el
3
Esta es una solución mucho más segura. También agregué --squash a la fusión. Being: git merge --squash original_messy_branch
jezpez
1
Gracias por salvarme el día y la vida :) La mejor solución que cualquiera puede obtener
Cuando lo hagas git diff new_clean_branch..original_messy_branch, ¿ deberían ser lo mismo? Para mí son diferentes.
LeonardChallis
5

Estaba buscando un requisito similar, es decir, descartar los compromisos intermedios de mi rama de desarrollo, he encontrado que este procedimiento funcionó para mí.
en mi rama de trabajo

git reset –hard mybranch-start-commit
git checkout mybranch-end-commit . // files only of the latest commit
git add -a
git commit -m”New Message intermediate commits discarded”

¡viola hemos conectado el último commit al start commit de la sucursal! No hay problemas de conflicto de fusión! En mi práctica de aprendizaje, he llegado a esta conclusión en esta etapa. ¿Existe un mejor enfoque para este propósito?

usuario28186
fuente
FYI: solo estaba intentando esto y descubrí que hacer el pago de mybranch-end-commit no me da las eliminaciones que ocurrieron en los commits intermedios. Por lo tanto, solo comprueba los archivos que existían en mybranch-end-commit.
Ahains
1

Sobre la base de la excelente respuesta anterior de @ hlidka que minimiza la intervención manual, quería agregar una versión que conserve cualquier nueva confirmación en master que no esté en la rama para aplastar.

Como creo, estos podrían perderse fácilmente en el git resetpaso de ese ejemplo.

# create a new branch 
# ...from the commit in master original_messy_branch was originally based on. eg 5654da06
git checkout -b new_clean_branch 5654da06

# apply all changes
git merge original_messy_branch

# forget the commits but have the changes staged for commit
# ...base the reset on the base commit from Master
git reset --soft 5654da06       

git commit -m "Squashed changes from original_messy_branch"

# Rebase onto HEAD of master
git rebase origin/master

# Resolve any new conflicts from the new commits
JonoB
fuente
0

Tenga en cuenta que las -Xopciones de estrategia se ignoraron cuando se usaron en un rebase interactivo.

Ver commit db2b3b820e2b28da268cc88adff076b396392dfe (julio de 2013, git 1.8.4+),

No ignore las opciones de fusión en el rebase interactivo

La estrategia de fusión y sus opciones se pueden especificar en git rebase, pero con -- interactive, se ignoraron por completo.

Firmado por: Arnaud Fontaine

Eso significa que la -Xestrategia ahora funciona con un rebase interactivo, así como un rebase simple, y su script inicial ahora podría funcionar mejor.

VonC
fuente
0

Me encontraba con un problema más simple pero similar, en el que tenía 1) resolvió un conflicto de fusión en una sucursal local, 2) seguí trabajando agregando muchas más pequeñas confirmaciones, 3) quería rebase y golpear conflictos de fusión.

Para mí, git rebase -p -i masterfuncionó. Mantuvo el compromiso original de resolución de conflictos y me permitió aplastar a los demás en la parte superior.

Espero que ayude a alguien!

abaldwinhunter
fuente
Todavía tengo algunos conflictos que resolver manualmente al intentar el rebase con -p Es cierto que hubo muchos menos conflictos y solo se limitaron al archivo del proyecto en lugar de a todos los archivos de código con los que estaba teniendo conflictos antes. Aparentemente, "las resoluciones de conflicto de fusión o las enmiendas manuales para confirmar compromisos no se conservan". - stackoverflow.com/a/35714301/727345
JonoB