Perché git-rebase mi dà unire conflitti quando tutto quello che sto facendo è schiacciare i commit?


146

Abbiamo un repository Git con oltre 400 commit, la prima dozzina di questi sono stati un sacco di tentativi ed errori. Vogliamo ripulire questi commit eliminandone molti in un unico commit. Naturalmente, git-rebase sembra la strada da percorrere. Il mio problema è che finisce con unire conflitti e questi conflitti non sono facili da risolvere. Non capisco perché ci dovrebbero essere dei conflitti, dato che sto solo schiacciando i commit (non eliminando o riorganizzando). Molto probabilmente, questo dimostra che non capisco completamente come git-rebase faccia le sue zucche.

Ecco una versione modificata degli script che sto usando:


repo_squash.sh (questo è lo script effettivamente eseguito):


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 (questo script è utilizzato solo da 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: (questo file è utilizzato solo da 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

Lascerò i contenuti del "nuovo messaggio" alla tua immaginazione. Inizialmente, l'ho fatto senza l'opzione "--strategy theirs" (vale a dire, usando la strategia predefinita, che se capisco correttamente la documentazione è ricorsiva, ma non sono sicuro di quale strategia ricorsiva venga utilizzata), e inoltre non funziona. Inoltre, dovrei sottolineare che, usando il codice commentato in repo_squash_helper.sh, ho salvato il file originale su cui funziona lo script sed e ho eseguito lo script sed contro di esso per assicurarmi che stesse facendo quello che volevo ( era). Ancora una volta, non so nemmeno perché ci sarebbe un conflitto, quindi non sembra importare così tanto quale strategia viene utilizzata. Qualsiasi consiglio o intuizione sarebbe utile, ma soprattutto voglio solo far funzionare questo schiacciare.

Aggiornato con ulteriori informazioni dalla discussione con Jefromi:

Prima di lavorare sul nostro enorme repository "reale", ho usato script simili su un repository di test. Era un repository molto semplice e il test ha funzionato in modo pulito.

Il messaggio che ricevo quando fallisce è:

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

Questa è la prima scelta dopo il primo commit di squash. L'esecuzione git statusproduce una directory di lavoro pulita. Se poi faccio un messaggio git rebase --continue, ricevo un messaggio molto simile dopo qualche altro commit. Se lo faccio di nuovo, ricevo un altro messaggio molto simile dopo un paio di dozzine di commit. Se lo faccio ancora una volta, questa volta passa circa un centinaio di commit e genera questo messaggio:

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

Se poi corro git status, ottengo:

# 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

Il bit "entrambi modificato" suona strano per me, poiché questo era solo il risultato di una scelta. Vale anche la pena notare che se guardo al "conflitto", si riduce a una singola riga con una versione che inizia con un carattere [tab] e l'altra con quattro spazi. Sembrava che potesse essere un problema con il modo in cui ho impostato il mio file di configurazione, ma non contiene nulla del genere. (Ho notato che core.ignorecase è impostato su true, ma evidentemente git-clone lo ha fatto automaticamente. Non sono del tutto sorpreso dal fatto che l'origine originale fosse su un computer Windows.)

Se correggo manualmente file_X.cpp, in seguito fallisce poco dopo con un altro conflitto, questa volta tra un file (CMakeLists.txt) che una versione pensa dovrebbe esistere e una versione pensa che non dovrebbe. Se risolvo questo conflitto dicendo che voglio questo file (cosa che faccio), qualche commit dopo ottengo un altro conflitto (in questo stesso file) dove ora ci sono alcune modifiche piuttosto banali. È ancora solo il 25% circa del conflitto.

Vorrei anche sottolineare, poiché questo potrebbe essere molto importante, che questo progetto è iniziato in un repository svn. Quella storia iniziale molto probabilmente è stata importata da quel repository svn.

Aggiornamento n. 2:

Su un'allodola (influenzata dai commenti di Jefromi), ho deciso di fare il mio cambio repo_squash.sh per essere:

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

E poi, ho appena accettato le voci originali, così come sono. Vale a dire, il "rebase" non avrebbe dovuto cambiare nulla. È finito con gli stessi risultati descritti in precedenza.

Aggiornamento n. 3:

In alternativa, se ometto la strategia e sostituisco l'ultimo comando con:

git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a

Non ricevo più i problemi di rebase "niente da commettere", ma sono ancora rimasto con gli altri conflitti.

Aggiornamento con repository di giocattoli che ricrea il problema:

test_squash.sh (questo è il file che effettivamente esegui):

#========================================================
# 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 (utilizzato da 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

PS: Sì, conosco alcuni di voi rabbrividire quando mi vedete usare emacs come editor fall-back.

PPS: Sappiamo che dovremo spazzare via tutti i nostri cloni del repository esistente dopo il rebase. (Sulla falsariga di "non rifare il repository dopo che è stato pubblicato".)

PPPS: Qualcuno può dirmi come aggiungere una taglia a questo? Non vedo l'opzione da nessuna parte in questa schermata, sia in modalità modifica che in modalità visualizzazione.


Più pertinente degli script utilizzati è l'ultima azione tentata: sembra abbastanza sicuro che sia un elenco di pick and squash mescolati, giusto? E c'è qualche commit di merge nel ramo rinnovato? (Anche se non lo stai usando rebase -pcomunque)
Cascabel,

Non sono sicuro di cosa intendi per "azione tentata finale", ma è solo un elenco di pick e squash mescolati, con gli ultimi 400 circa che sono tutti pick. Non ci sono fusioni in quell'elenco, anche se lo stesso rebasing sta eseguendo le proprie fusioni. Secondo quanto ho letto, "rebase -p" non è raccomandato con la modalità interattiva (che nel mio caso non è poi così interattiva, ovviamente). Da kernel.org/pub/software/scm/git/docs/git-rebase.html : "Questo utilizza internamente la macchina --interactive, ma combinarla esplicitamente con l'opzione --interactive non è generalmente una buona idea"
Ben Hocking

Con "azione tentata finale" intendevo l'elenco di pick / squash restituito a rebase --interactive: quelli sono una specie di elenco di azioni che Git deve tentare. Speravo che potessi essere in grado di ridurlo a una singola compressione che causava conflitti ed evitare tutta la complessità aggiuntiva dei tuoi script di supporto. L'altra informazione mancante è quando si verificano i conflitti: quando git applica le patch per formare la zucca o quando tenta di passare oltre la zucca e applicare la patch successiva? (E sei sicuro che non accada nulla di brutto con il tuo kludge GIT_EDITOR? Un altro voto per un semplice test.)
Cascabel,

Grazie per i commenti utili. Ho aggiornato la domanda per riflettere le mie risposte.
Ben Hocking,

2
Il caso del repository di giocattoli è molto, molto più facile da capire rispetto ai tuoi script - e mostra che gli script non hanno nulla a che fare con esso, solo il fatto che stai cercando di rebase un ramo contenente una fusione con risoluzione dei conflitti (una fusione leggermente malvagia ).
Cascabel,

Risposte:


69

Va bene, sono abbastanza sicuro di dare una risposta. Forse dovrò modificarlo, ma credo di sapere qual è il tuo problema.

Il tuo caso di test di repo giocattolo ha una fusione in esso - peggio ancora, ha una fusione con conflitti. E ti stai ribellando attraverso l'unione. Senza -p(che non funziona completamente con -i), le fusioni vengono ignorate. Ciò significa che qualsiasi cosa tu abbia fatto nella risoluzione del conflitto non è presente quando rebase tenta di selezionare il prossimo commit, quindi la sua patch potrebbe non essere applicabile. (Credo che questo sia mostrato come un conflitto di unione perché git cherry-pickpuò applicare la patch facendo una fusione a tre vie tra il commit originale, il commit corrente e l'antenato comune.)

Sfortunatamente, come abbiamo notato nei commenti, -ie -p(preservare le fusioni) non vanno molto d'accordo. So che la modifica / riformulazione funziona e che il riordino no. Tuttavia, credo che funzioni bene con le zucche. Questo non è documentato, ma ha funzionato per i casi di test che descrivo di seguito. Se il tuo caso è molto più complesso, potresti avere molti problemi a fare quello che vuoi, anche se sarà ancora possibile. (Morale della storia: ripulire rebase -i prima di fondersi.)

Supponiamo quindi di avere un caso molto semplice, in cui vogliamo mettere insieme A, B e C:

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

Ora, come ho detto, se non ci fossero conflitti in X, git rebase -i -pfunziona come ti aspetteresti.

Se ci sono conflitti, le cose diventano un po 'più complicate. Farà una buona compressione, ma quando tenterà di ricreare l'unione, i conflitti si ripresenteranno. Dovrai risolverli di nuovo, aggiungerli all'indice, quindi utilizzare git rebase --continueper andare avanti. (Naturalmente, puoi risolverli di nuovo controllando la versione dal commit di unione originale.)

Se vi capita di avere rerereabilitati nel tuo repo ( rerere.enabledimpostato su true), questo sarà modo più semplice - git sarà in grado di ri utilizzare il re con cavo ri soluzione dal momento in cui è stato originariamente aveva i conflitti, e tutto quello che dovete fare è ispezionarlo per assicurarti che abbia funzionato correttamente, aggiungi i file all'indice e continua. (Puoi anche fare un altro passo, accendendo rerere.autoupdate, e li aggiungerai per te, quindi l'unione non fallirà nemmeno). Immagino, tuttavia, che non hai mai abilitato il rerere, quindi dovrai fare tu stesso la risoluzione del conflitto. *

* Oppure, puoi provare lo rerere-train.shscript da git-contrib, che tenta di "Prime [the] rerere database from commit di merge esistenti" - fondamentalmente, controlla tutti i commit di merge, cerca di unirli e se la fusione fallisce, prende i risultati e li mostra a git-rerere. Questo potrebbe richiedere molto tempo e non l'ho mai usato, ma potrebbe essere molto utile.


PS Guardando indietro ai miei commenti, vedo che avrei dovuto prenderlo prima. Ti ho chiesto se stavi eseguendo il rebasing di un ramo contenente unioni e hai detto che non c'erano fusioni nell'elenco di rebase interattivo, che non è la stessa cosa - non c'erano fusioni lì perché non hai fornito l' -popzione.
Cascabel,

Ci proverò sicuramente. Da quando ho pubblicato questo, ho notato anche in alcuni casi, posso semplicemente digitare git commit -a -m"Some message"e git rebase --continue, e continuerà su. Funziona anche senza l' -popzione, ma funziona ancora meglio con l' -popzione (dal momento che non sto facendo alcun riordino, sembra che -pvada bene). Ad ogni modo, ti terrò aggiornato.
Ben Hocking,

L'impostazione git config --global rerere.enabled truee git config --global rerere.autoupdate trueprima dell'esecuzione dell'esempio di test risolve il problema principale. Abbastanza interessante, tuttavia, non preserva le fusioni, anche quando si specifica --preserve-merges. Tuttavia, se non li ho impostati, e digito git commit -a -m"Meaningful commit"e git rebase --continuementre si specifica --preserve-merges, mantiene le fusioni. Comunque, grazie per avermi aiutato a superare questo problema!
Ben Hocking,

84

Se non ti dispiace creare un nuovo ramo, ecco come ho affrontato il problema:

Essere sul principale:

# 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
La soluzione più veloce ed efficiente :)
IslamTaha,

2
Questo è stato un grande suggerimento! Ha funzionato molto bene per consolidare rapidamente diversi commit.
philidem,

3
Questa è una soluzione molto più sicura. Ho anche aggiunto --squash all'unione. Essere: git merge --squash original_messy_branch
jezpez

1
Grazie per avermi salvato la vita e la vita :) La migliore soluzione che chiunque può ottenere

Quando lo fai git diff new_clean_branch..original_messy_branchdovrebbero essere uguali? Per me sono diversi.
Leonardhallis,

5

Stavo cercando un requisito simile, ovvero scartando gli impegni intermedi del mio ramo di sviluppo, ho scoperto che questa procedura ha funzionato per me.
nel mio ramo di lavoro

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 abbiamo collegato l'ultimo commit al commit iniziale della filiale! Nessun problema di conflitto di unione! Nella mia pratica di apprendimento sono giunto a questa conclusione in questa fase, esiste un approccio migliore allo scopo.


Cordiali saluti - Stavo solo provando questo e ho scoperto che fare il checkout di mybranch-end-commit non mi dà le cancellazioni che si sono verificate nelle commit intermedie. Quindi controlla solo i file esistenti in mybranch-end-commit.
ahains il

1

Sulla @ hlidka 'il grande risposta di cui sopra, che riduce al minimo l'intervento manuale, ho voluto aggiungere una versione che conserva tutti i nuovi impegna sul master che non sono nel ramo a squash.

A mio avviso, questi potrebbero essere facilmente persi nel git resetpassaggio in questo esempio.

# 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

0

Si noti che le -Xopzioni di strategia sono state ignorate quando utilizzate in un rebase interattivo.

Vedi commit db2b3b820e2b28da268cc88adff076b396392dfe (luglio 2013, git 1.8.4+),

Non ignorare le opzioni di unione in rebase interattivo

La strategia di unione e le sue opzioni possono essere specificate in git rebase, ma con -- interactive, sono state completamente ignorate.

Firmato-fuori-da: Arnaud Fontaine

Ciò significa -Xche la strategia ora funziona con rebase interattivo, così come rebase semplice, e il tuo script iniziale ora potrebbe funzionare meglio.


0

Stavo incontrando un problema più semplice ma simile, in cui avevo 1) risolto un conflitto di unione su una filiale locale, 2) continuavo a lavorare aggiungendo molti più piccoli impegni, 3) volevo riformulare e colpire i conflitti di unione.

Per me ha git rebase -p -i masterfunzionato. Ha mantenuto la risoluzione originale del conflitto impegnata e mi ha permesso di schiacciare gli altri in cima.

Spero che aiuti qualcuno!


Ho ancora alcuni conflitti da risolvere manualmente quando provo a rebase con -p Certamente ci sono stati molti meno conflitti e si sono limitati al file di progetto piuttosto che a tutti i file di codice con cui stavo entrando in conflitto prima. Apparentemente "Non unire le risoluzioni di conflitto o le modifiche manuali per unire i commit". - stackoverflow.com/a/35714301/727345
JonoB
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.