226 votes

Pourquoi git-rebase me donne-t-il des conflits de fusion alors que je ne fais qu'écraser des commits ?

Nous avons un dépôt Git avec plus de 400 commits, dont les deux premières douzaines ont été beaucoup d'essais et d'erreurs. Nous voulons nettoyer ces commits en en réduisant un grand nombre en un seul. Naturellement, git-rebase semble être la voie à suivre. Mon problème est que cela aboutit à des conflits de fusion, et que ces conflits ne sont pas faciles à résoudre. Je ne comprends pas pourquoi il devrait y avoir des conflits du tout, puisque je ne fais qu'écraser des commits (sans les supprimer ou les réarranger). Très probablement, cela démontre que je ne comprends pas complètement comment git-rebase fait ses écrasements.

Voici une version modifiée des scripts que j'utilise :


repo_squash.sh (c'est le script qui est réellement exécuté) :


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 (ce script est utilisé uniquement par 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 : (ce fichier est utilisé seulement par 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

Je laisse le contenu du "nouveau message" à votre imagination. Initialement, j'ai fait cela sans l'option "--strategy theirs" (c'est-à-dire en utilisant la stratégie par défaut, qui si je comprends bien la documentation est récursive, mais je ne suis pas sûr de la stratégie récursive utilisée), et cela n'a pas non plus fonctionné. De plus, je dois signaler que, en utilisant le code commenté dans repo_squash_helper.sh, j'ai sauvegardé le fichier original sur lequel le sed script fonctionne et j'ai exécuté le sed script contre lui pour m'assurer qu'il faisait ce que je voulais qu'il fasse (il le faisait). Encore une fois, je ne sais même pas pourquoi il y a serait Il n'y a pas de conflit, donc la stratégie utilisée ne semble pas avoir beaucoup d'importance. Tout conseil ou avis serait utile, mais je veux surtout que l'écrasement fonctionne.

Mis à jour avec des informations supplémentaires provenant d'une discussion avec Jefromi :

Avant de travailler sur notre "vrai" référentiel massif, j'ai utilisé des scripts similaires sur un référentiel de test. C'était un référentiel très simple et le test a fonctionné proprement.

Le message que j'obtiens lorsqu'il échoue est le suivant :

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

C'est le premier choix après le premier engagement du squash. Exécution de git status donne un répertoire de travail propre. Si je fais ensuite un git rebase --continue J'obtiens un message très similaire après quelques commandes supplémentaires. Si je le refais, j'obtiens un autre message très similaire après quelques dizaines de commits. Si je le fais encore, cette fois-ci, il passe par une centaine de commits, et donne ce message :

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 je lance ensuite git status j'obtiens :

# 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

La partie "les deux modifiés" me semble bizarre, puisque c'était juste le résultat d'un choix. Il est également intéressant de noter que si je regarde le "conflit", il se résume à une seule ligne avec une version commençant par un caractère [tab], et l'autre par quatre espaces. Cela semblait être un problème avec la façon dont j'ai configuré mon fichier de configuration, mais il n'y a rien de tel. (J'ai bien noté que core.ignorecase est réglé sur true, mais il est évident que git-clone l'a fait automatiquement. Je ne suis pas complètement surpris par cela étant donné que la source originale était sur une machine Windows).

Si je corrige manuellement file_X.cpp, il échoue peu de temps après avec un autre conflit, cette fois entre un fichier (CMakeLists.txt) qu'une version pense devoir exister et une autre version pense ne pas devoir exister. Si je corrige ce conflit en disant que je veux ce fichier (ce que je fais), quelques commits plus tard j'obtiens un autre conflit (dans ce même fichier) où maintenant il y a des changements plutôt non triviaux. Il ne s'agit encore que d'environ 25% du chemin à travers les conflits.

Je dois également souligner, puisque cela peut être très important, que ce projet a commencé dans un dépôt svn. Cet historique initial a très probablement été importé de ce dépôt svn.

Mise à jour n°2 :

Sur un coup de tête (influencé par les commentaires de Jefromi), j'ai décidé de faire le changement de mon repo_squash.sh pour être :

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

Et puis, j'ai juste accepté les entrées originales, telles quelles. C'est-à-dire que le "rebase" n'aurait pas dû changer grand-chose. Cela a abouti aux mêmes résultats que ceux décrits précédemment.

Mise à jour n°3 :

Sinon, si j'omets la stratégie et remplace la dernière commande par :

git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a

Je n'ai plus les problèmes de rebasement "rien à commettre", mais je reste avec les autres conflits.

Mise à jour avec un dépôt de jouets qui recrée le problème :

test_squash.sh (c'est le fichier que vous exécutez réellement) :

#========================================================
# 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 (utilisé par 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

P.S. : Oui, je sais que certains d'entre vous grimacent quand ils me voient utiliser emacs comme éditeur de repli.

P.P.S. : Nous savons que nous devrons détruire tous nos clones du dépôt existant après le rebasement. (Dans la lignée de "tu ne rebaseras pas un référentiel après qu'il ait été publié").

P.P.P.S : Est-ce que quelqu'un peut me dire comment ajouter une prime à ceci ? Je ne vois cette option nulle part sur cet écran, que je sois en mode édition ou en mode affichage.

0 votes

Plus pertinent que les scripts utilisés est l'action finale tentée - il semble assez sûr d'être une liste de pick and squash intermixés, non ? Et y-a-t-il des commits de fusion dans la branche rebasée ? rebase -p de toute façon)

0 votes

Je ne suis pas sûr de ce que vous entendez par "dernière tentative d'action", mais c'est es c'est juste une liste de choix et de squash mélangés, avec les 400 derniers environ qui sont tous des choix. Il n'y a pas de fusions dans cette liste, bien que le rebasement lui-même effectue ses propres fusions. D'après ce que j'ai lu, "rebase -p" n'est pas recommandé en mode interactif (qui dans mon cas n'est pas si interactif que ça, bien sûr). À partir de kernel.org/pub/software/scm/git/docs/git-rebase.html : "Ceci utilise la machinerie --interactive en interne, mais la combiner avec l'option --interactive explicitement n'est généralement pas une bonne idée."

0 votes

Par "dernière tentative d'action", j'entends la liste de choix/quash transmise à l'équipe d'intervention. rebase --interactive - Il s'agit en quelque sorte d'une liste d'actions que git doit tenter. J'espérais que vous pourriez être en mesure de réduire cela à un seul squash qui causait des conflits, et éviter toute la complexité supplémentaire de vos scripts d'aide. L'autre information manquante est le moment où les conflits se produisent - lorsque git applique les correctifs pour former la courge, ou lorsqu'il essaie de dépasser la courge et d'appliquer le correctif suivant ? (Et êtes-vous sûr que rien de mauvais ne se produit avec votre kludge GIT_EDITOR ? Un autre vote pour un cas de test simple).

161voto

hlidka Points 509

Si cela ne vous dérange pas de créer une nouvelle branche, voici comment j'ai réglé le problème :

Être sur le principal :

# 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"

3 votes

La solution la plus rapide et efficace :)

2 votes

C'était une excellente suggestion ! Elle a très bien fonctionné pour consolider rapidement plusieurs commits.

10 votes

C'est une solution bien plus sûre. J'ai aussi ajouté --squash à la fusion. Être : git merge --squash original_messy_branch

105voto

Jefromi Points 127932

Très bien, je suis assez confiant pour donner une réponse. Je vais peut-être devoir la modifier, mais je crois savoir quel est votre problème.

Le scénario de test de votre repo jouet contient une fusion - pire, il contient une fusion avec des conflits. Et vous rebasez à travers la fusion. Sans -p (ce qui ne fonctionne pas totalement avec -i ), les fusions sont ignorées. Cela signifie que quoi que vous ayez fait dans votre résolution de conflit n'est pas là lorsque le rebasement tente d'extraire le prochain commit, donc son patch peut ne pas s'appliquer. (Je crois que ceci est montré comme un conflit de fusion parce que git cherry-pick peut appliquer le patch en faisant une fusion à trois voies entre le commit original, le commit courant, et l'ancêtre commun).

Malheureusement, comme nous l'avons noté dans les commentaires, -i y -p (préserver les fusions) ne s'entendent pas très bien. Je sais que l'édition et la reformulation fonctionnent, et que la réorganisation ne fonctionne pas. Cependant, je croire qu'il fonctionne bien avec les courges. Ce n'est pas documenté, mais cela a fonctionné pour les cas de test que je décris ci-dessous. Si votre cas est beaucoup, beaucoup plus complexe, vous risquez d'avoir beaucoup de mal à faire ce que vous voulez, bien que ce soit toujours possible. (Morale de l'histoire : nettoyez avec rebase -i avant la fusion).

Supposons donc que nous ayons un cas très simple, où nous voulons écraser ensemble A, B et C :

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

Maintenant, comme je l'ai dit, s'il n'y avait pas de conflits dans X, git rebase -i -p fonctionne comme on peut s'y attendre.

S'il y a des conflits, les choses deviennent un peu plus délicates. La fusion sera bien écrasée, mais lorsqu'elle essaiera de recréer la fusion, les conflits se reproduiront. Vous devrez les résoudre à nouveau, les ajouter à l'index, puis utiliser la commande git rebase --continue pour passer à autre chose. (Bien sûr, vous pouvez les résoudre à nouveau en vérifiant la version issue du commit de fusion d'origine).

Si vous avez rerere activé dans votre repo ( rerere.enabled défini à true), ce sera beaucoup plus facile - git sera capable de concernant utiliser le concernant filaire concernant Il ne vous reste plus qu'à l'inspecter pour vous assurer qu'elle a bien fonctionné, à ajouter les fichiers à l'index et à poursuivre. (Vous pouvez même aller plus loin en activant l'option rerere.autoupdate et il les ajoutera pour vous, de sorte que la fusion n'échouera même pas). Je suppose, cependant, que vous n'avez jamais activé rerere, donc vous allez devoir faire la résolution de conflit vous-même.*

* Ou, vous pouvez essayer le <a href="http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/rerere-train.sh;h=2cfe1b936b0feef1bd40947ce6ab249f62a6ad55;hb=HEAD" rel="noreferrer"><code>rerere-train.sh</code></a> script de git-contrib, qui tente de "Prime [the] rerere database from existing merge commits" - en gros, il vérifie tous les commits de fusion, essaie de les fusionner, et si la fusion échoue, il saisit les résultats et les montre à <code>git-rerere</code> . Cela peut prendre du temps, et je ne l'ai jamais utilisé, mais cela pourrait être très utile.

0 votes

P.S. En repensant à mes commentaires, je vois que j'aurais dû m'en rendre compte plus tôt. J'ai demandé si vous rebasiez une branche contenant des fusions, et vous avez dit qu'il n'y avait pas de fusions dans la liste interactive de rebasement, ce qui n'est pas la même chose. -p option.

0 votes

Je vais certainement donner un coup de pouce. Depuis que j'ai posté ce message, j'ai aussi remarqué que dans un peu de je peux simplement taper git commit -a -m"Some message" y git rebase --continue et il continuera. Cela fonctionne même sans le -p mais il fonctionne encore mieux avec l'option -p (puisque je ne fais pas de réorganisation, il semble que l'option -p est bien). Quoi qu'il en soit, je vous tiendrai au courant.

0 votes

Réglage de git config --global rerere.enabled true y git config --global rerere.autoupdate true avant d'exécuter l'exemple de test résout le problème principal. Cependant, il est intéressant de noter qu'il ne préserve pas les fusions, même en spécifiant --preserve-merges . Cependant, si je n'ai pas ces paramètres et que je tape git commit -a -m"Meaningful commit" y git rebase --continue tout en spécifiant --preserve-merges mais il préserve les fusions. Quoi qu'il en soit, merci de m'aider à résoudre ce problème !

9voto

Felix Dombek Points 2130

Si vous voulez créer exactement un commit à partir d'une longue branche de commits, dont certains sont des commits de fusion, la manière la plus simple est de réinitialiser votre branche au point précédant le premier commit tout en gardant tous vos changements, puis de les ré-engager :

git reset $(git merge-base origin/master @)
git add .
git commit

Remplacer origin/master avec le nom de la branche à partir de laquelle vous avez bifurqué.

El add . est nécessaire parce que les fichiers qui ont été nouvellement ajoutés apparaissent comme non suivis après la réinitialisation.

6voto

user28186 Points 81

J'étais à la recherche d'une exigence similaire, c'est-à-dire supprimer les commits intermédiaires de ma branche de développement, j'ai trouvé cette procédure qui a fonctionné pour moi.
sur ma branche de travail

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 nous avons connecté le dernier commit au commit de départ de la branche ! Aucun problème de conflit de fusion ! Dans ma pratique d'apprentissage, j'en suis arrivé à cette conclusion à ce stade, y a-t-il une meilleure approche pour cet objectif ?

0 votes

FYI - Je viens juste d'essayer ceci et j'ai trouvé que faire le checkout de mybranch-end-commit ne me donne pas les suppressions qui ont eu lieu dans les commits intermédiaires. Il ne vérifie donc que les fichiers qui existaient dans mybranch-end-commit.

3voto

JonoB Points 162

Construire sur @ hlidka Suite à l'excellente réponse de l'auteur qui minimise l'intervention manuelle, j'ai voulu ajouter une version qui préserve tous les nouveaux commits sur master qui ne sont pas dans la branche à écraser.

Comme je pense qu'ils pourraient être facilement perdus dans le git reset dans cet exemple.

# 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X