Suddividere un commit precedente in più commit


1225

Senza creare una filiale e fare un sacco di lavoro funky su una nuova filiale, è possibile suddividere un singolo commit in alcuni commit diversi dopo che è stato eseguito il commit nel repository locale?


36
Una buona fonte per imparare come farlo è Pro Git §6.4 Git Tools - Riscrivere la storia , nella sezione "Dividere un commit".

2
I documenti collegati al commento sopra sono eccellenti, meglio spiegati delle risposte sottostanti.
Blaisorblade,

2
Suggerisco l'uso di questo alias stackoverflow.com/a/19267103/301717 . Permette di dividere un commit usandogit autorebase split COMMIT_ID
Jérôme Pouiller l'

La cosa più semplice da fare senza un rebase interattivo è (probabilmente) creare un nuovo ramo a partire dal commit prima di quello che si desidera dividere, selezionare -n il commit, ripristinare, stash, eseguire lo spostamento del file, riapplicare lo stash e eseguire il commit delle modifiche, quindi unire con il ramo precedente o selezionare i commit che sono seguiti. (Quindi passa il nome della filiale precedente all'attuale capo.) (Probabilmente è meglio seguire i consigli degli MBO e fare un rebase interattivo.) (Copiato dalla risposta del 2010 sotto)
William Pursell,

1
Ho riscontrato questo problema dopo aver schiacciato accidentalmente due commit durante un rebase in un commit precedente. Il mio modo di risolvere il problema è stato quello di checkout la schiacciata commettere, git reset HEAD~, git stash, poi git cherry-pickil primo commit all'interno della zucca, poi git stash pop. Il mio caso cherry-pick è abbastanza specifica qui, ma git stashe git stash pop'abbastanza a portata di mano per gli altri.
SOF

Risposte:


1801

git rebase -i lo farà.

Innanzitutto, inizia con una directory di lavoro pulita: non git statusdeve mostrare modifiche, eliminazioni o aggiunte in sospeso.

Ora, devi decidere quali commit vuoi dividere.

A) Dividere il commit più recente

Per dividere il commit più recente, per prima cosa:

$ git reset HEAD~

Ora commetti i pezzi individualmente nel solito modo, producendo tutti gli impegni di cui hai bisogno.

B) Dividere un commit più indietro

Ciò richiede la riformulazione , ovvero la riscrittura della storia. Per trovare il commit corretto, hai diverse opzioni:

  • Se fossero stati tre i commit, allora

    $ git rebase -i HEAD~3
    

    dov'è 3quanti commettono indietro.

  • Se era più indietro nell'albero di quanto tu voglia contare, allora

    $ git rebase -i 123abcd~
    

    dove si 123abcdtrova SHA1 del commit che si desidera suddividere.

  • Se ti trovi in ​​un ramo diverso (ad es. Un ramo di funzionalità) che prevedi di unire in master:

    $ git rebase -i master
    

Quando viene visualizzata la schermata di modifica del rebase, trova il commit che desideri suddividere. All'inizio di quella riga, sostituisci pickcon edit( ein breve). Salvare il buffer ed uscire. Rebase ora si interromperà subito dopo il commit che si desidera modificare. Poi:

$ git reset HEAD~

Commetti i pezzi individualmente nel solito modo, producendo tutti gli impegni di cui hai bisogno, quindi

$ git rebase --continue

2
Grazie per questa risposta Volevo avere alcuni file precedentemente impegnati nell'area di gestione temporanea, quindi le istruzioni per me erano leggermente diverse. Prima che potessi git rebase --continue, in realtà ho dovuto git add (files to be added), git commite poi git stash(per i file rimanenti). Dopo git rebase --continue, git checkout stash .ricevevo i file rimanenti
Eric Hu,

18
La risposta di manojlds in realtà ha questo link alla documentazione su git-scm , che spiega anche il processo di suddivisione degli commit molto chiaramente.

56
Ti consigliamo inoltre di git add -paggiungere solo sezioni parziali di file, possibilmente con l' eopzione di modificare le differenze per eseguire solo il commit di parte di un pezzo. git stashè utile anche se vuoi portare avanti un po 'di lavoro ma rimuoverlo dal commit corrente.
Craig Ringer,

2
Se vuoi dividere e riordinare i commit, quello che mi piace fare è prima dividere e poi riordinare separatamente usando un altro git rebase -i HEAD^3comando. In questo modo se la divisione va male non è necessario annullare altrettanto lavoro.
David M. Lloyd,

4
@kralyk I file appena impegnati in HEAD verranno lasciati sul disco dopo git reset HEAD~. Non sono persi.
Wayne Conrad,

312

Dal manuale di git-rebase (sezione IMPEGNI DI SPLITTING)

In modalità interattiva, è possibile contrassegnare i commit con l'azione "modifica". Tuttavia, ciò non significa necessariamente che git rebase si aspetti che il risultato di questa modifica sia esattamente un commit. In effetti, puoi annullare il commit oppure puoi aggiungere altri commit. Questo può essere usato per dividere un commit in due:

  • Inizia un rebase interattivo con git rebase -i <commit>^, dove si <commit>trova il commit che vuoi dividere. In effetti, qualsiasi intervallo di commit lo farà, purché contenga quel commit.

  • Contrassegna il commit che desideri dividere con l'azione "modifica".

  • Quando si tratta di modificare quel commit, eseguire git reset HEAD^. L'effetto è che HEAD viene riavvolto di uno e l'indice segue l'esempio. Tuttavia, l'albero di lavoro rimane lo stesso.

  • Ora aggiungi le modifiche all'indice che vuoi avere nel primo commit. Puoi usare git add(possibilmente in modo interattivo) o git gui(o entrambi) per farlo.

  • Effettua il commit dell'indice attuale con qualunque messaggio di commit sia appropriato ora.

  • Ripeti gli ultimi due passaggi fino a quando l'albero di lavoro non è pulito.

  • Continua il rebase con git rebase --continue.


12
Su Windows devi usare ~invece di ^.
Kevin Kuszyk,

13
Avvertenza: con questo approccio ho perso il messaggio di commit.
user420667

11
@ user420667 Sì, certo. resetDopotutto, stiamo eseguendo il commit - messaggio incluso. La cosa prudente da fare, se sai che stai per dividere un commit ma vuoi conservare parte / tutto il suo messaggio, è prendere una copia di quel messaggio. Quindi, git showesegui il commit prima di rebaseing, o se lo dimentichi o preferisci questo: tornaci in seguito tramite il reflog. Nessuno di questi verrà effettivamente "perso" fino a quando non sarà spazzato via in 2 settimane o altro.
underscore_d

4
~e ^sono cose diverse, anche su Windows. Vuoi ancora il cursore ^, quindi dovrai solo fuggire come appropriato per il tuo guscio. In PowerShell, lo è HEAD`^. Con cmd.exe, puoi raddoppiarlo per sfuggire come HEAD^^. Nella maggior parte delle (tutte?) Shell, puoi circondare con virgolette come "HEAD^".
AndrewF,

7
Puoi anche fare git commit --reuse-message=abcd123. L'opzione corta per questo è -C.
j0057

41

Utilizzare git rebase --interactiveper modificare quel commit precedente, eseguirlo git reset HEAD~e quindi git add -pper aggiungerne alcuni, quindi effettuare un commit, quindi aggiungerne un altro e effettuare un altro commit, tutte le volte che si desidera. Quando hai finito, corri git rebase --continuee avrai tutti i commit suddivisi in precedenza nel tuo stack.

Importante : nota che puoi giocare e apportare tutte le modifiche che desideri e non devi preoccuparti di perdere le vecchie modifiche, perché puoi sempre correre git reflogper trovare il punto nel tuo progetto che contiene le modifiche che desideri (chiamiamolo a8c4ab) e quindi git reset a8c4ab.

Ecco una serie di comandi per mostrare come funziona:

mkdir git-test; cd git-test; git init

ora aggiungi un file A

vi A

aggiungi questa riga:

one

git commit -am one

quindi aggiungi questa riga ad A:

two

git commit -am two

quindi aggiungi questa riga ad A:

three

git commit -am three

ora il file A è simile al seguente:

one
two
three

e il nostro git logsembra il seguente (bene, io usogit log --pretty=oneline --pretty="%h %cn %cr ---- %s"

bfb8e46 Rose Perrone 4 seconds ago ---- three
2b613bc Rose Perrone 14 seconds ago ---- two
9aac58f Rose Perrone 24 seconds ago ---- one

Diciamo che vogliamo dividere il secondo commesso, two.

git rebase --interactive HEAD~2

Questo fa apparire un messaggio simile al seguente:

pick 2b613bc two
pick bfb8e46 three

Cambia il primo pickin un eper modificare quel commit.

git reset HEAD~

git diff ci mostra che abbiamo appena messo in scena il commit che abbiamo fatto per il secondo commit:

diff --git a/A b/A
index 5626abf..814f4a4 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two

Mettiamo in scena quel cambiamento e aggiungiamo "e un terzo" a quella linea nel file A.

git add .

Questo di solito è il punto durante un rebase interattivo in cui verremmo eseguiti git rebase --continue, perché di solito vogliamo solo tornare indietro nella nostra pila di commit per modificare un commit precedente. Ma questa volta, vogliamo creare un nuovo commit. Quindi correremo git commit -am 'two and a third'. Ora modifichiamo il file Ae aggiungiamo la linea two and two thirds.

git add . git commit -am 'two and two thirds' git rebase --continue

Abbiamo un conflitto con il nostro impegno, three , quindi risolviamolo:

Cambieremo

one
<<<<<<< HEAD
two and a third
two and two thirds
=======
two
three
>>>>>>> bfb8e46... three

per

one
two and a third
two and two thirds
three

git add .; git rebase --continue

Ora il nostro git log -passomiglia a questo:

commit e59ca35bae8360439823d66d459238779e5b4892
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 13:57:00 2013 -0700

    three

diff --git a/A b/A
index 5aef867..dd8fb63 100644
--- a/A
+++ b/A
@@ -1,3 +1,4 @@
 one
 two and a third
 two and two thirds
+three

commit 4a283ba9bf83ef664541b467acdd0bb4d770ab8e
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 14:07:07 2013 -0700

    two and two thirds

diff --git a/A b/A
index 575010a..5aef867 100644
--- a/A
+++ b/A
@@ -1,2 +1,3 @@
 one
 two and a third
+two and two thirds

commit 704d323ca1bc7c45ed8b1714d924adcdc83dfa44
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 14:06:40 2013 -0700

    two and a third

diff --git a/A b/A
index 5626abf..575010a 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two and a third

commit 9aac58f3893488ec643fecab3c85f5a2f481586f
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 13:56:40 2013 -0700

    one

diff --git a/A b/A
new file mode 100644
index 0000000..5626abf
--- /dev/null
+++ b/A
@@ -0,0 +1 @@
+one

38

Le risposte precedenti hanno riguardato l'uso di git rebase -i per modificare il commit che si desidera dividere e il commit in parti.

Funziona bene quando si suddividono i file in diversi commit, ma se si desidera suddividere le modifiche ai singoli file, è necessario sapere di più.

Avendo ottenuto il commit che desideri dividere, utilizzandolo rebase -ie contrassegnandolo edit, hai due opzioni.

  1. Dopo l'uso git reset HEAD~, scorrere le patch singolarmente utilizzando git add -pper selezionare quelle desiderate in ciascun commit

  2. Modifica la copia di lavoro per rimuovere le modifiche che non desideri; commettere quello stato provvisorio; e quindi ritirare il commit completo per il round successivo.

L'opzione 2 è utile se stai dividendo un commit di grandi dimensioni, in quanto ti consente di verificare che le versioni provvisorie vengano compilate ed eseguite correttamente come parte dell'unione. Questo procede come segue.

Dopo aver utilizzato rebase -ie editeseguito il commit, utilizzare

git reset --soft HEAD~

per annullare il commit, ma lasciare i file di commit nell'indice. Puoi anche fare un reset misto omettendo --soft, a seconda di quanto vicino al risultato finale sarà il tuo commit iniziale. L'unica differenza è se inizi con tutte le modifiche messe in scena o con tutte non messe in scena.

Ora entra e modifica il codice. Puoi rimuovere le modifiche, eliminare i file aggiunti e fare tutto ciò che vuoi per costruire il primo commit della serie che stai cercando. Puoi anche crearlo, eseguirlo e confermare che hai un set coerente di sorgenti.

Una volta che sei soddisfatto, metti in scena / deseleziona i file secondo necessità (mi piace usarlo git guiper questo) e apporta le modifiche tramite l'interfaccia utente o la riga di comando

git commit

Questo è il primo commit fatto. Ora vuoi ripristinare la tua copia di lavoro allo stato che aveva dopo il commit che stai suddividendo, in modo da poter apportare più modifiche per il tuo prossimo commit. Per trovare lo sha1 del commit che stai modificando, usa git status. Nelle prime righe dello stato vedrai il comando rebase attualmente in esecuzione, in cui puoi trovare lo sha1 del tuo commit originale:

$ git status
interactive rebase in progress; onto be83b41
Last commands done (3 commands done):
   pick 4847406 US135756: add debugging to the file download code
   e 65dfb6a US135756: write data and download from remote
  (see more in file .git/rebase-merge/done)
...

In questo caso, il commit che sto modificando ha sha1 65dfb6a. Sapendo ciò, posso controllare il contenuto di quel commit sulla mia directory di lavoro usando il modulo git checkoutche accetta sia un commit che un percorso di file. Qui utilizzo .come posizione del file per sostituire l'intera copia di lavoro:

git checkout 65dfb6a .

Non perdere il punto alla fine!

Questo verificherà e metterà in scena i file come erano dopo il commit che stai modificando, ma rispetto al commit precedente che hai effettuato, quindi qualsiasi modifica già impegnata non farà parte del commit.

Puoi andare avanti ora e commetterlo così com'è per terminare la divisione, oppure andare di nuovo in giro, eliminando alcune parti del commit prima di effettuare un altro commit temporaneo.

Se si desidera riutilizzare il messaggio di commit originale per uno o più commit, è possibile utilizzarlo direttamente dai file di lavoro del rebase:

git commit --file .git/rebase-merge/message

Infine, dopo aver eseguito tutte le modifiche,

git rebase --continue

proseguirà e completerà l'operazione di rebase.


3
Grazie!!! Questa dovrebbe essere la risposta accettata. Mi avrebbe risparmiato un sacco di tempo e dolore oggi. È l'unica risposta in cui il risultato del commit finale porta allo stesso stato del commit in modifica.
Doug Coburn,

1
Mi piace il modo in cui usi il messaggio di commit originale.
Salamandar,

Usando l'opzione 2, quando lo faccio git checkout *Sha I'm Editing* .dice sempre Updated 0 paths from *Some Sha That's Not In Git Log*e non dà cambiamenti.
Noumenon,

18

git rebase --interactivepuò essere utilizzato per suddividere un commit in commit più piccoli. I documenti Git su rebase presentano una concisa procedura dettagliata del processo - Splitting Commits :

In modalità interattiva, è possibile contrassegnare i commit con l'azione "modifica". Tuttavia, ciò non significa necessariamente che git rebaseil risultato di questa modifica sia esattamente un commit. In effetti, puoi annullare il commit oppure puoi aggiungere altri commit. Questo può essere usato per dividere un commit in due:

  • Inizia un rebase interattivo con git rebase -i <commit>^, dove si <commit>trova il commit che vuoi dividere. In effetti, qualsiasi intervallo di commit lo farà, purché contenga quel commit.

  • Contrassegna il commit che desideri dividere con l'azione "modifica".

  • Quando si tratta di modificare quel commit, eseguire git reset HEAD^. L'effetto è che HEAD viene riavvolto di uno e l'indice segue l'esempio. Tuttavia, l'albero di lavoro rimane lo stesso.

  • Ora aggiungi le modifiche all'indice che vuoi avere nel primo commit. Puoi usare git add(possibilmente in modo interattivo) o git gui (o entrambi) per farlo.

  • Effettua il commit dell'indice attuale con qualunque messaggio di commit sia appropriato ora.

  • Ripeti gli ultimi due passaggi fino a quando l'albero di lavoro non è pulito.

  • Continua il rebase con git rebase --continue.

Se non si è assolutamente sicuri che le revisioni intermedie siano coerenti (compilano, superano la suite di test, ecc.) È necessario utilizzare git stashper nascondere le modifiche non ancora impegnate dopo ogni commit, test e modificare il commit se sono necessarie correzioni .


In Windows, ricorda che ^è un carattere di escape per la riga di comando: dovrebbe essere raddoppiato. Per esempio, pubblica git reset HEAD^^invece di git reset HEAD^.
Frédéric,

@ Frédéric: s Non mi sono mai imbattuto in questo. Almeno in PowerShell questo non è il caso. Quindi usando ^due volte ripristina due commit sopra l'attuale HEAD.
Farway,

@Farway, provalo in una classica riga di comando. PowerShell è un'altra vera bestia, il suo carattere di fuga è il backtilt.
Frédéric,

Riassumendo: "HEAD^"in cmd.exe o PowerShell, HEAD^^in cmd.exe, HEAD`^in PowerShell. È utile imparare come funzionano le shell - e la tua particolare shell (ovvero come un comando si trasforma in singole parti che vengono passate al programma) in modo da poter adattare i comandi online ai caratteri giusti per la tua particolare shell. (Non specifico per Windows.)
AndrewF

11

Ora nell'ultimo TortoiseGit su Windows puoi farlo molto facilmente.

Apri la finestra di dialogo rebase, configurala e procedi come segue.

  • Fare clic con il tasto destro del mouse sul commit che si desidera dividere e selezionare "Edit " (tra pick, squash, delete ...).
  • Clicca "Start " per iniziare il rebasing.
  • Una volta arrivato al commit per dividere, controlla il Edit/Splitpulsante " " e fai clic su " Amend" direttamente. Si apre la finestra di dialogo di commit.
    Modifica / Dividi commit
  • Deseleziona i file che desideri inserire in un commit separato.
  • Modifica il messaggio di commit, quindi fai clic su " commit".
  • Fino a quando non ci sono file da impegnare, la finestra di dialogo di commit si aprirà ancora e ancora. Quando non ci sono più file da impegnare, ti verrà comunque chiesto se vuoi aggiungere un altro commit.

Molto utile, grazie TortoiseGit!



8

Si prega di notare che c'è anche git reset --soft HEAD^. È simile a git reset(impostazione predefinita --mixed) ma conserva i contenuti dell'indice. In modo che se hai aggiunto / rimosso file, li hai già nell'indice.

Risulta essere molto utile in caso di commit giganti.


3

Ecco come dividere un commit in IntelliJ IDEA , PyCharm , PhpStorm ecc

  1. Nella finestra di registro Controllo versione, selezionare il commit che si desidera dividere, fare clic con il tasto destro e selezionare Interactively Rebase from Here

  2. contrassegnare quello che si desidera dividere come edit, fare clic suStart Rebasing

  3. Dovresti vedere un tag giallo che indica che HEAD è impostato su quel commit. Fare clic con il tasto destro su quel commit, selezionareUndo Commit

  4. Ora tali commit sono tornati nell'area di gestione temporanea, quindi è possibile eseguire il commit separatamente. Dopo aver eseguito il commit di tutte le modifiche, il vecchio commit diventa inattivo.


2

La cosa più semplice da fare senza un rebase interattivo è (probabilmente) creare un nuovo ramo a partire dal commit prima di quello che si desidera dividere, selezionare -n il commit, ripristinare, stash, eseguire lo spostamento del file, riapplicare lo stash e eseguire il commit delle modifiche, quindi unire con il precedente ramo o selezionare i commit successivi. (Quindi passa il nome del ramo precedente all'attuale capo.) (Probabilmente è meglio seguire i consigli degli MBO e fare un rebase interattivo.)


secondo gli standard SO in questi giorni questo dovrebbe essere qualificato come non-risposta; ma questo può ancora essere utile per gli altri, quindi se non ti dispiace per favore sposta questo nei commenti del post originale
YakovL

@YakovL Sembra ragionevole. Per quanto riguarda l'azione minima, non eliminerò la risposta, ma non obietterei se lo facesse qualcun altro.
William Pursell,

questo sarebbe molto più semplice di tutti i rebase -isuggerimenti. Penso che questo non abbia attirato molta attenzione a causa della mancanza di formattazione. Forse potresti rivederlo, ora che hai 126k punti e probabilmente sai come SO. ;)
erikbwork


1

Se hai questo:

A - B <- mybranch

Dove hai eseguito il commit di alcuni contenuti in commit B:

/modules/a/file1
/modules/a/file2
/modules/b/file3
/modules/b/file4

Ma vuoi dividere B in C - D e ottenere questo risultato:

A - C - D <-mybranch

Ad esempio, puoi dividere il contenuto in questo modo (contenuto di directory diverse in commit diversi) ...

Ripristina il ramo al commit prima di quello da suddividere:

git checkout mybranch
git reset --hard A

Crea primo commit (C):

git checkout B /modules/a
git add -u
git commit -m "content of /modules/a"

Crea secondo commit (D):

git checkout B /modules/b
git add -u
git commit -m "content of /modules/b"

Cosa succede se ci sono commit sopra B?
CoolMind,

1

Sono passati più di 8 anni, ma forse qualcuno lo troverà comunque utile. Sono stato in grado di fare il trucco senza rebase -i. L'idea è di condurre git allo stesso stato di prima git commit:

# first rewind back (mind the dot,
# though it can be any valid path,
# for instance if you want to apply only a subset of the commit)
git reset --hard <previous-commit> .

# apply the changes
git checkout <commit-you-want-to-split>

# we're almost there, but the changes are in the index at the moment,
# hence one more step (exactly as git gently suggests):
# (use "git reset HEAD <file>..." to unstage)
git reset

Dopo questo vedrai questo splendente Unstaged changes after reset:e il tuo repository è in uno stato come se stessi per eseguire il commit di tutti questi file. Da ora in poi puoi facilmente impegnarlo di nuovo come fai di solito. Spero che sia d'aiuto.


0

Un rapido riferimento ai comandi necessari, perché sostanzialmente so cosa fare ma dimentico sempre la sintassi giusta:

git rebase -i <sha1_before_split>
# mark the targeted commit with 'edit'
git reset HEAD^
git add ...
git commit -m "First part"
git add ...
git commit -m "Second part"
git rebase --continue

Crediti al post sul blog di Emmanuel Bernard .

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.