Come ripristinare più commit git?


984

Ho un repository git che assomiglia a questo:

A -> B -> C -> D -> HEAD

Voglio che la testa del ramo punti ad A, cioè voglio che B, C, D e HEAD scompaiano e voglio che la testa sia sinonimo di A.

Sembra che io possa provare a riformulare (non si applica, dal momento che ho inserito le modifiche) o ripristinare. Ma come posso ripristinare più commit? Devo ripristinarne uno alla volta? L'ordine è importante?


3
Se vuoi solo ripristinare il telecomando, puoi bloccarlo con qualsiasi cosa! Ma usiamo il quarto commit fa: git push -f HEAD~4:master(supponendo che il ramo remoto sia master). Sì, puoi eseguire qualsiasi commit in questo modo.
u0b34a0f6ae,

21
Se le persone hanno eseguito il pull devi eseguire un commit che ripristina le modifiche usando git revert.
Jakub Narębski,

1
Usa git show HEAD ~ 4 per assicurarti di spingere verso destra sul telecomando
Mâtt Frëëman


5
"L'ordine è importante?" Sì, se i commit influiscono sulle stesse righe negli stessi file. Quindi dovresti iniziare a ripristinare il commit più recente e tornare indietro.
avandeursen,

Risposte:


1336

Espandendo quello che ho scritto in un commento

La regola generale è che non dovresti riscrivere (cambiare) la storia che hai pubblicato, perché qualcuno potrebbe aver basato il proprio lavoro su di esso. Se riscrivi (cambi) la cronologia, avresti problemi con l'unione delle loro modifiche e con l'aggiornamento per esse.

Quindi la soluzione è quella di creare un nuovo commit che ripristini le modifiche che si desidera eliminare. Puoi farlo usando il comando git revert .

Hai la seguente situazione:

A <- B <- C <- D <- master <- HEAD

(le frecce qui si riferiscono alla direzione del puntatore: il riferimento "parent" nel caso di commit, il commit superiore nel caso della testa del ramo (riferimento ramo) e il nome del ramo nel caso del riferimento HEAD).

Quello che devi creare è il seguente:

A <- B <- C <- D <- [(BCD) ^ - 1] <- master <- HEAD

dove "[(BCD) ^ - 1]" indica il commit che ripristina le modifiche nei commit B, C, D. La matematica ci dice che (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, quindi puoi ottenere la situazione richiesta usando i seguenti comandi:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message"

Una soluzione alternativa sarebbe quella di verificare il contenuto del commit A e commettere questo stato:

$ git checkout -f A -- .
$ git commit -a

Quindi avresti la seguente situazione:

A <- B <- C <- D <- A '<- master <- HEAD

Il commit A 'ha gli stessi contenuti del commit A, ma è un commit diverso (messaggio di commit, genitori, data di commit).

La soluzione di Jeff Ferland, modificata da Charles Bailey si basa sulla stessa idea, ma utilizza git reset :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit

40
Se hai aggiunto file in B, C o D. git checkout -f A -- .Non li eliminerai, dovrai farlo manualmente. Ho applicato questa strategia ora, grazie Jakub
oma

18
Quelle soluzioni non sono equivalenti. Il primo non elimina i file appena creati.
m33lky,

10
@Jerry: git checkout foopotrebbe significare checkout branch foo (passa a branch) o checkout file foo (dall'indice). --viene utilizzato per chiarire le ambiguità, ad esempio git checkout -- fooriguarda sempre il file.
Jakub Narębski il

87
Oltre alla grande risposta. Questa scorciatoia funziona per megit revert --no-commit D C B
welldan97

9
@ welldan97: Grazie per un commento. Quando scrivevo questa risposta git revertnon hai accettato più commit; è un'aggiunta abbastanza nuova.
Jakub Narębski,

248

Modo pulito che ho trovato utile

git revert --no-commit HEAD~3..

Questo comando ripristina gli ultimi 3 commit con un solo commit.

Inoltre non riscrive la storia.


16
Questa è la risposta semplice e migliore
Julien Deniau,

3
@JohnLittle mette in scena i cambiamenti. git commitda lì eseguirà effettivamente il commit.
x1a4,

14
Questo non funzionerà se alcuni commit sono commit di merge.
MegaManX,

5
Cosa fanno i due punti alla fine?
Cardamomo,

5
@cardamom Quelli specificano un intervallo. HEAD~3..è lo stesso diHEAD~3..HEAD
Toine H

238

Per fare ciò devi solo usare il comando revert , specificando l'intervallo di commit che vuoi ripristinare.

Tenendo conto del tuo esempio, dovresti farlo (supponendo che tu sia nel ramo 'master'):

git revert master~3..master

Ciò creerà un nuovo commit nel tuo locale con il commit inverso di B, C e D (il che significa che annullerà le modifiche introdotte da questi commit):

A <- B <- C <- D <- BCD' <- HEAD

129
git revert --no-commit HEAD~2..è un modo leggermente più idiomatico per farlo. Se sei nel ramo master, non è necessario specificare nuovamente il master. L' --no-commitopzione consente a git di provare a ripristinare tutti i commit contemporaneamente, invece di sporcare la cronologia con più revert commit ...messaggi (supponendo che sia quello che vuoi).
Kubi,

6
@Victor Ho corretto l'intervallo di commit. L'inizio della gamma è esclusivo, il che significa che non è incluso. Quindi, se si desidera ripristinare gli ultimi 3 commit, è necessario iniziare l'intervallo dal genitore del 3o commit, ovvero master~3.

2
@kubi non c'è modo di includere gli SHA in un messaggio di commit, usando un singolo commit (il tuo metodo ma senza dover inserire manualmente i commit che sono stati ripristinati)?
Chris S,

@ChrisS Il mio primo pensiero sarebbe stato di non utilizzare --no-commit(in modo da ottenere un commit separato per ogni ripristino), e quindi comprimerli tutti insieme in un rebase interattivo. Il messaggio di commit combinato conterrà tutti gli SHA e puoi organizzarli come preferisci usando il tuo editor di messaggi di commit preferito.
Radon Rosborough,

71

Simile alla risposta di Jakub, ciò consente di selezionare facilmente commit consecutivi da ripristinare.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'

9
La tua soluzione ha funzionato bene per me, ma con una leggera modifica. Se abbiamo questo caso Z -> A -> B -> C -> D -> HEAD e se vorrei tornare allo stato A, stranamente dovrei eseguire git revert --no-commit Z .. TESTA
Bogdan,

3
D'accordo con @Bogdan, l'intervallo di ripristino è simile: SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov,

11
L'intervallo è sbagliato. Dovrebbe essere B^..HEAD, altrimenti B è escluso.
tessus,

3
Concordo con @tessus, quindi la cosa giusta da fare sarebbe: git revert --no-commit B^..HEADoppuregit revert --no-commit A..HEAD
Yoho,

64
git reset --hard a
git reset --mixed d
git commit

Questo fungerà da ritorno per tutti in una volta. Dai un buon messaggio di commit.


4
Se vuole HEADapparire, Aallora probabilmente vuole che l'indice corrisponda, quindi git reset --soft Dè probabilmente più appropriato.
CB Bailey,

2
--soft reset non sposta l'indice, quindi quando commette, sembrerebbe che il commit provenga direttamente da un anziché da D. Ciò renderebbe il ramo diviso. --mixed lascia le modifiche, ma sposta il puntatore dell'indice, quindi D diventerà il commit principale.
Jeff Ferland,

5
Sì, penso che git reset - keep sia esattamente quello che ho sopra. È uscito nella versione 1.7.1, rilasciata nell'aprile del 2010, quindi la risposta non era disponibile in quel momento.
Jeff Ferland,

git checkout Aallora git commitsopra non ha funzionato per me, ma questa risposta ha funzionato.
Semplicemente

Perché è git reset --mixed Drichiesto? In particolare perché reset? È perché, senza reimpostare su D, HEAD punta su A, facendo sì che B, C e D siano "penzolanti" e raccolti nella spazzatura - che non è quello che vuole? Ma allora perché --mixed? Hai già risposto "il --softripristino non sposta l'indice ..." Quindi spostando l'indice, questo significa che l'indice conterrà le modifiche di D, mentre la Directory di lavoro conterrà le modifiche di A - in questo modo a git statuso git diff(che confronta l'indice [D] a Directory di lavoro [A]) mostrerà la sostanza; che utente sta tornando da D ad A?
Il pisello rosso

39

Innanzitutto assicurati che la tua copia di lavoro non sia stata modificata. Poi:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

e poi basta impegnarsi. Non dimenticare di documentare qual è il motivo del ripristino.


1
Ha funzionato per me! Si è verificato un problema con le modifiche obsolete del ramo della funzionalità apportate nel ramo di sviluppo (alcune correzioni di bug), pertanto il ramo della funzione doveva sovrascrivere tutte le modifiche apportate in sviluppo (inclusa l'eliminazione di alcuni file).
Silenzioso,

1
Non funziona con i file binari:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux il

2
Questa è una soluzione molto più flessibile della risposta accettata. Grazie!
Brian Kung,

2
ri: i file binari usano l'opzione --binary: git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
weinerk

2
Funzionerà anche se si desidera ripristinare un intervallo di commit che contiene commit di unione. Usando git revert A..Zotterraierror: commit X is a merge but no -m option was given.
Juliusz Gonera il

35

Sono così frustrato che non è possibile rispondere a questa domanda. Ogni altra domanda riguarda come ripristinare correttamente e preservare la storia. Questa domanda dice "Voglio che il capo del ramo punti ad A, cioè voglio che B, C, D e HEAD scompaiano e voglio che la testa sia sinonimo di A."

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

Ho imparato molto leggendo il post di Jakub, ma un ragazzo della compagnia (con accesso per spingere al nostro ramo "testing" senza Pull-Request) ha spinto come 5 cattivi commit cercando di risolvere e correggere e correggere un errore che ha fatto 5 commit fa. Non solo, ma furono accettate una o due richieste pull, che ora erano cattive. Quindi dimenticalo, ho trovato l'ultimo buon commit (abc1234) e ho appena eseguito lo script di base:

git checkout testing
git reset --hard abc1234
git push -f

Ho detto agli altri 5 ragazzi che lavorano in questo repository che è meglio prendere nota dei loro cambiamenti nelle ultime ore e cancellare / ripetere i rami dagli ultimi test. Fine della storia.


2
Non avevo pubblicato i commit, quindi questa era la risposta di cui avevo bisogno. Grazie, @Suamere.
Tom Barron,

Il modo migliore per farlo è git push --force-with-lease, che riscriverà la storia solo se nessun altro si è impegnato nella filiale dopo o all'interno dell'intervallo dei commit da vaporizzare. Se altre persone hanno usato il ramo, la sua storia non dovrebbe mai essere riscritta e l'impegno dovrebbe essere semplicemente ripristinato visibilmente.
Frandroid,

1
@frandroid "la storia non dovrebbe mai essere riscritta", solo l'affare sith in assoluto. La domanda di questo thread, e il punto della mia risposta, è esattamente che per uno scenario particolare, tutta la storia dovrebbe essere cancellata.
Suamere,

@Suamere Certo, questa è la domanda. Ma poiché la tua risposta menziona ciò che dovevi dire agli altri ragazzi, ci sono potenziali problemi. Per esperienza personale, push -f può rovinare la tua base di codice se altre persone si sono impegnate dopo quello che stai cercando di cancellare. --force-with-lease ottiene lo stesso risultato, tranne per il fatto che ti salverà il culo se stavi per rovinare il tuo repo. Perché rischiare? Se --force-with-lease ha esito negativo, puoi vedere quale commit si frappone, valutare correttamente, adeguare e riprovare.
Frandroid,

1
@Suamere Grazie! Sono d'accordo che la domanda afferma chiaramente che vuole riscrivere la storia. Sono nella tua stessa situazione e sto indovinando il PO in quanto qualcuno ha fatto dozzine di brutti ritorni e strani commessi e ripristini di ritorni per caso (mentre ero in vacanza) e lo stato deve essere ripristinato. In ogni caso insieme a un buon avvertimento salutare questa dovrebbe essere la risposta accettata.
Lee Richardson,

9

Questa è un'espansione di una delle soluzioni fornite nella risposta di Jakub

Mi trovavo di fronte a una situazione in cui gli commit di cui avevo bisogno per eseguire il rollback erano in qualche modo complessi, con molti degli commit che erano merge di commit, e avevo bisogno di evitare di riscrivere la storia. Non sono stato in grado di utilizzare una serie di git revertcomandi perché alla fine mi sono imbattuto in conflitti tra le modifiche di inversione aggiunte. Ho finito con i seguenti passaggi.

Innanzitutto, controlla il contenuto del commit di destinazione lasciando HEAD sulla punta del ramo:

$ git checkout -f <target-commit> -- .

(Il - si assicura che <target-commit>sia interpretato come un commit piuttosto che un file; il. Si riferisce alla directory corrente.)

Quindi, determinare quali file sono stati aggiunti nei commit di cui è stato eseguito il rollback e quindi devono essere eliminati:

$ git diff --name-status --cached <target-commit>

I file aggiunti dovrebbero essere visualizzati con una "A" all'inizio della riga e non dovrebbero esserci altre differenze. Ora, se è necessario rimuovere eventuali file, posizionare questi file per la rimozione:

$ git rm <filespec>[ <filespec> ...]

Infine, commetti l'inversione:

$ git commit -m 'revert to <target-commit>'

Se lo si desidera, assicurarsi che siamo tornati allo stato desiderato:

$git diff <target-commit> <current-commit>

Non ci dovrebbero essere differenze.


Sei sicuro di poter gitTESTA con solo la punta del ramo?
Suamere,

2
Questa è stata una soluzione molto migliore per me, dal momento che nella mia ho avuto impegni di fusione.
Sovemp,

3

Il modo semplice per ripristinare un gruppo di commit su un repository condiviso (che le persone usano e che vuoi conservare la cronologia) è usare git revertinsieme a git rev-list. Quest'ultimo ti fornirà un elenco di commit, il primo eseguirà il ripristino stesso.

Ci sono due modi per farlo. Se si desidera ripristinare più commit in un unico commit, utilizzare:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $i; done

questo ripristinerà un gruppo di commit di cui hai bisogno, ma lascia tutte le modifiche sul tuo albero di lavoro, dovresti impegnarle tutte come al solito.

Un'altra opzione è avere un unico commit per ogni modifica ripristinata:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

Ad esempio, se hai un albero di commit come

 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

per ripristinare le modifiche da eee a bbb , eseguire

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done

ho appena usato questo. Grazie!
Ran Biron,

2

Nessuno di quelli ha funzionato per me, quindi ho avuto tre commit da ripristinare (gli ultimi tre commit), quindi ho fatto:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

Ha funzionato come un fascino :)


2
Questa è un'opzione praticabile solo se le modifiche non sono ancora state inviate.
kboom,

0

Secondo me un modo molto semplice e pulito potrebbe essere:

tornare ad A

git checkout -f A

punta la testa del padrone allo stato attuale

git symbolic-ref HEAD refs/heads/master

Salva

git commit

1
Potete per favore spiegare il motivo del downvoting?
nulll

Funziona benissimo. Qual è lo scopo di dare un voto negativo per questa risposta utile o qualcuno può spiegare qual è la migliore pratica?
Levent Divilioglu,

È lo stesso di git checkout master; git reset --hard A? O se non potessi spiegare qualcosa in più su ciò che fa?
MM

funziona, ma HEAD simbolico-ref sembra non essere un comando "sicuro"
Sérgio

err non voglio fix master, voglio fix a branch
Sérgio

-6

Se si desidera ripristinare temporaneamente i commit di una funzione, è possibile utilizzare la serie dei seguenti comandi.

Ecco come funziona

git log --pretty = oneline | grep 'feature_name' | cut -d '' -f1 | xargs -n1 git ripristina --no-edit

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.