I commit Git vengono duplicati nello stesso ramo dopo aver eseguito un rebase


131

Capisco lo scenario presentato in Pro Git su The Perils of Rebasing . L'autore fondamentalmente ti dice come evitare commit duplicati:

Non rebase i commit che hai inviato a un repository pubblico.

Ti dirò la mia situazione particolare perché penso che non si adatti esattamente allo scenario Pro Git e mi ritrovo comunque con commit duplicati.

Diciamo che ho due filiali remote con le loro controparti locali:

origin/master    origin/dev
|                |
master           dev

Tutti e quattro i rami contengono gli stessi commit e inizierò lo sviluppo in dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Dopo un paio di commit, spingo le modifiche a origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Devo tornare a masterper fare una soluzione rapida:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

E tornando a devriformulare le modifiche per includere la soluzione rapida nel mio sviluppo effettivo:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Se mostro la cronologia dei commit con GitX / gitk, noto che origin/devora contiene due commit identici C5'e C6'che sono diversi da Git. Ora se spingo le modifiche a origin/devquesto è il risultato:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Forse non capisco completamente la spiegazione in Pro Git, quindi vorrei sapere due cose:

  1. Perché Git duplica questi commit durante il rebase? C'è una ragione particolare per farlo invece di limitarsi a fare domanda C5e C6dopo C7?
  2. Come posso evitarlo? Sarebbe saggio farlo?

Risposte:


87

Non dovresti usare rebase qui, sarà sufficiente una semplice unione. Il libro Pro Git che hai collegato spiega fondamentalmente questa situazione esatta. Il funzionamento interno potrebbe essere leggermente diverso, ma ecco come lo visualizzo:

  • C5e C6vengono temporaneamente ritirati dadev
  • C7 viene applicato a dev
  • C5e C6vengono riprodotti sopra C7, creando nuovi diff e quindi nuovi commit

Quindi, nel tuo devramo, C5ed C6effettivamente non esistono più: sono adesso C5'e C6'. Quando spingi a origin/dev, git vede C5'e C6'come nuovi esegue il commit e li aggiunge alla fine della cronologia. In effetti, se guardi le differenze tra C5e C5'in origin/dev, noterai che sebbene il contenuto sia lo stesso, i numeri di riga sono probabilmente diversi, il che rende diverso l'hash del commit.

Ribadirò la regola di Pro Git: mai rebase commit che siano mai esistiti da nessuna parte tranne che nel tuo repository locale . Usa invece l'unione.


Ho lo stesso problema, come posso correggere la cronologia del mio ramo remoto ora, c'è qualche altra opzione oltre a eliminare il ramo e ricrearlo con la selezione di ciliegie ??
Wazery

1

2
Dici "C5 e C6 vengono temporaneamente estratti da dev ... C7 viene applicato a dev". Se questo è il caso, allora perché C5 e C6 vengono visualizzati prima di C7 nell'ordine dei commit su origin / dev?
KJ50

@ KJ50: Perché C5 e C6 erano già stati spinti a origin/dev. Quando devviene ribasato, la sua cronologia viene modificata (C5 / C6 temporaneamente rimossi e riapplicati dopo C7). Modificare la cronologia dei repository inviati è generalmente una Really Bad Idea ™ a meno che tu non sappia cosa stai facendo. In questo semplice caso, il problema potrebbe essere risolto eseguendo un push forzato da deva origin/devdopo il rebase e notificando a chiunque altro origin/devstia lavorando che probabilmente sta per avere una brutta giornata. La risposta migliore, ancora una volta, è "non farlo ... usa invece l'unione"
Justin ᚅᚔᚈᚄᚒᚔ

3
Una cosa da notare: gli hash di C5 e C5 'sono certamente diversi, ma non perché i numeri di riga siano diversi, ma per i seguenti due fatti di cui uno qualunque è sufficiente per la differenza: 1) l'hash di cui stiamo parlando è l'hash dell'intero albero sorgente dopo il commit, non l'hash della differenza delta, e quindi C5 'contiene tutto ciò che proviene da C7, mentre C5 no, e 2) Il genitore di C5' è diverso da C5, e questa informazione è incluso anche nel nodo radice di un albero di commit che influisce sul risultato hash.
Ozgur Murat

113

Risposta breve

Hai omesso il fatto di aver eseguito git push, ottenuto il seguente errore e quindi eseguito git pull:

To git@bitbucket.org:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Nonostante Git cerchi di essere d'aiuto, il suo consiglio "git pull" molto probabilmente non è quello che vuoi fare .

Se sei:

  • Lavorare su un "ramo di caratteristica" o "ramo developer" da solo , quindi è possibile eseguire git push --forcel'aggiornamento del telecomando con i tuoi post-commit rebase ( come da risposta di user4405677 ).
  • Lavorando su un ramo con più sviluppatori contemporaneamente, probabilmente non dovresti usarlogit rebase in primo luogo. Per aggiornare devcon le modifiche da master, dovresti, invece di correre git rebase master dev, correre git merge mastermentre sei acceso dev( come da risposta di Justin ).

Una spiegazione leggermente più lunga

Ogni hash di commit in Git si basa su una serie di fattori, uno dei quali è l'hash del commit che lo precede.

Se riordini i commit, cambierai gli hash dei commit; il rebasing (quando fa qualcosa) cambierà gli hash di commit. Con ciò, il risultato dell'esecuzione git rebase master dev, dove non devè sincronizzato con master, creerà nuovi commit (e quindi hash) con lo stesso contenuto di quelli attivi devma con i commit masterinseriti prima di essi.

Puoi finire in una situazione come questa in diversi modi. Due modi in cui posso pensare:

  • Potresti avere impegni su mastercui vuoi basare il tuo devlavoro
  • Potresti avere commit su devche sono già stati inviati a un telecomando, che poi procedi a modificare (riformulare i messaggi di commit, riordinare i commit, squash commit, ecc.)

Comprendiamo meglio cosa è successo: ecco un esempio:

Hai un repository:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Set iniziale di commit lineari in un repository

Si procede quindi alla modifica dei commit.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Qui è dove devi credermi sulla parola: ci sono molti modi per cambiare i commit in Git. In questo esempio ho cambiato l'ora di C3, ma tu stai inserendo nuovi commit, cambiando i messaggi di commit, riordinando i commit, schiacciare i commit insieme, ecc.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

Lo stesso si impegna con i nuovi hash

È qui che è importante notare che gli hash di commit sono diversi. Questo è un comportamento previsto poiché hai cambiato qualcosa (qualsiasi cosa) su di loro. Va bene, MA:

Un registro grafico che mostra che il master non è sincronizzato con il telecomando

Provare a spingere mostrerà un errore (e suggerirà che dovresti eseguire git pull).

$ git push origin master
To git@bitbucket.org:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Se corriamo git pull, vediamo questo registro:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Oppure, mostrato in un altro modo:

Un registro grafico che mostra un commit di unione

E ora abbiamo commit duplicati a livello locale. Se dovessimo eseguire git push, li invieremo al server.

Per evitare di arrivare a questa fase, avremmo potuto correre git push --force(dove invece abbiamo corso git pull). Ciò avrebbe inviato i nostri commit con i nuovi hash al server senza problemi. Per risolvere il problema in questa fase, possiamo ripristinare prima di eseguire git pull:

Guarda il reflog ( git reflog) per vedere qual era l'hash del commit prima di essere eseguito git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Sopra vediamo che ba7688aera il commit in cui eravamo prima di correre git pull. Con quell'hash di commit in mano possiamo reimpostare su that ( git reset --hard ba7688a) e quindi eseguire git push --force.

E abbiamo finito.

Ma aspetta, ho continuato a basare il lavoro sui commit duplicati

Se in qualche modo non hai notato che i commit erano duplicati e hai continuato a lavorare sopra i commit duplicati, hai davvero combinato un pasticcio per te stesso. La dimensione del pasticcio è proporzionale al numero di commit che hai in cima ai duplicati.

Che aspetto ha:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Log di Git che mostra i commit lineari sopra i commit duplicati

Oppure, mostrato in un altro modo:

Un grafico di log che mostra i commit lineari sopra i commit duplicati

In questo scenario vogliamo rimuovere i commit duplicati, ma mantenere i commit che abbiamo basato su di essi: vogliamo mantenere da C6 a C10. Come per la maggior parte delle cose, ci sono diversi modi per farlo:

O:

  • Crea un nuovo ramo all'ultimo commit duplicato 1 , cherry-pickogni commit (da C6 a C10 compreso) su quel nuovo ramo e considera quel nuovo ramo come canonico.
  • Esegui git rebase --interactive $commit, dov'è $commitil commit prima di entrambi i commit duplicati 2 . Qui possiamo eliminare completamente le righe per i duplicati.

1 Non importa quale dei due scegli, ba7688ao 2a2e220funziona bene.

2 Nell'esempio sarebbe 85f59ab.

TL; DR

Imposta advice.pushNonFastForwardsu false:

git config --global advice.pushNonFastForward false

1
Va bene seguire il consiglio "git pull ..." fintanto che ci si rende conto che i puntini di sospensione nascondono l'opzione "--rebase" (aka "-r"). ;-)
G. Sylvie Davies

4
Consiglierei di usare git push's al --force-with-leasegiorno d'oggi perché è un valore predefinito migliore
Whymarrh

4
È questa risposta o una macchina del tempo. Grazie!
ZeMoon

Spiegazione molto chiara ... Mi sono imbattuto in un problema simile che ha duplicato il mio codice 5-6 volte dopo aver tentato ripetutamente di rebase ... solo per essere sicuro che il codice sia aggiornato con il master ... ma ogni volta che è stato spinto nuovi commit nel mio ramo, duplicando anche il mio codice. Puoi dirmi se force push (con opzione di leasing) è sicuro da fare qui se sono l'unico sviluppatore a lavorare sul mio ramo? O fondere il master nel mio invece di ribasare è il modo migliore?
Dhruv Singhal

12

Penso che tu abbia saltato un dettaglio importante quando hai descritto i tuoi passaggi. Più specificamente, il tuo ultimo passaggio, git pushsu dev, ti avrebbe effettivamente dato un errore, poiché normalmente non puoi inviare modifiche non rapide.

Così hai fatto git pullprima dell'ultimo push, che ha portato a un commit di unione con C6 e C6 'come genitori, motivo per cui entrambi rimarranno elencati nel registro. Un formato di log più carino avrebbe potuto rendere più ovvio che si tratta di rami uniti di commit duplicati.

Oppure hai fatto un git pull --rebase(o senza esplicito --rebasese è implicito dalla tua configurazione) invece, che ha ritirato l'originale C5 e C6 nel tuo sviluppatore locale (e ha ulteriormente ribasato i seguenti in nuovi hash, C7 'C5' 'C6' ').

Un modo per uscirne avrebbe potuto essere git push -fquello di forzare la spinta quando ha dato l'errore e cancellare C5 C6 dall'origine, ma se qualcun altro li avesse anche tirati prima che tu li cancellassi, avresti avuto molti più problemi .. fondamentalmente tutti coloro che hanno C5 C6 dovrebbero fare dei passaggi speciali per sbarazzarsene. Che è esattamente il motivo per cui dicono che non dovresti mai riformulare qualcosa che è già pubblicato. Tuttavia, è ancora fattibile se si dice che "la pubblicazione" è all'interno di un piccolo team.


1
L'omissione di git pullè fondamentale. La tua raccomandazione git push -f, sebbene pericolosa, è probabilmente ciò che i lettori stanno cercando.
Whymarrh

Infatti. Quando ho scritto la domanda, l'ho fatto davvero git push --force, solo per vedere cosa avrebbe fatto Git. Da allora ho imparato molto su Git e oggi rebasefa parte del mio normale flusso di lavoro. Tuttavia, lo faccio git push --force-with-leaseper evitare di sovrascrivere il lavoro di qualcun altro.
elitalon

L'uso --force-with-leaseè una buona impostazione predefinita, lascerò un commento anche sotto la mia risposta
Whymarrh

2

Ho scoperto che nel mio caso, questo problema è la conseguenza di un problema di configurazione di Git. (Coinvolgere pull and merge)

Descrizione del problema:

Sintomi: commit duplicati sul ramo figlio dopo il rebase, implicando numerose unioni durante e dopo il rebase.

Flusso di lavoro: ecco i passaggi del flusso di lavoro che stavo eseguendo:

  • Lavora su "Features-branch" (figlio di "Develop-branch")
  • Effettua il commit e il push delle modifiche su "Features-branch"
  • Checkout "Develop-branch" (ramo principale delle funzionalità) e lavoraci.
  • Impegnare e inviare modifiche a "Develop-branch"
  • Effettua il checkout di "Features-branch" e recupera le modifiche dal repository (nel caso in cui qualcun altro si sia impegnato)
  • Ribasare "Features-branch" in "Develop-branch"
  • Spingere la forza delle modifiche su "Feature-branch"

Come conseguenza di questo flusso di lavoro, la duplicazione di tutti i commit di "Feature-branch" dal rebase precedente ... :-(

Il problema era dovuto al richiamo delle modifiche del ramo figlio prima del rebase. La configurazione pull predefinita di Git è "merge". Questo sta cambiando gli indici dei commit eseguiti sul ramo figlio.

La soluzione: nel file di configurazione di Git, configura il pull per funzionare in modalità rebase:

...
[pull]
    rebase = preserve
...

Spero che possa aiutare JN Grx


1

Potresti aver effettuato il pull da un ramo remoto diverso dal tuo attuale. Ad esempio, potresti aver tirato da Master quando il tuo ramo sta sviluppando il monitoraggio dello sviluppo. Git attirerà diligentemente i commit duplicati se estratto da un ramo non tracciato.

In tal caso, puoi procedere come segue:

git reset --hard HEAD~n

dove n == <number of duplicate commits that shouldn't be there.>

Quindi assicurati di tirare dal ramo corretto e quindi esegui:

git pull upstream <correct remote branch> --rebase

Tirare con --rebaseti assicurerà di non aggiungere commit estranei che potrebbero confondere la cronologia dei commit.

Ecco un po 'di mano che tiene per git rebase.

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.