Sposta i commit più recenti in una nuova filiale con Git


4986

Mi piacerebbe spostare gli ultimi numerosi commit che mi sono impegnato a padroneggiare in una nuova succursale e riportare il master prima che questi vengano effettuati. Sfortunatamente, il mio Git-fu non è ancora abbastanza forte, qualche aiuto?

Vale a dire come posso andare da questo

master A - B - C - D - E

a questo?

newbranch     C - D - E
             /
master A - B 

114
Nota: ho fatto la domanda opposta qui
Benjol il


7
I commenti qui sono stati eliminati? Chiedo perché durante la mia visita bimestrale a questa domanda, scorro sempre per quel commento.
Tejas Kale

Risposte:


6453

Passare a una filiale esistente

Se vuoi spostare i tuoi commit in una succursale esistente , sarà simile al seguente:

git checkout existingbranch
git merge master         # Bring the commits here
git checkout master
git reset --keep HEAD~3  # Move master back by 3 commits.
git checkout existingbranch

L' --keepopzione conserva tutte le modifiche non impegnate che potresti avere in file non correlati o interrompe se tali modifiche dovessero essere sovrascritte, analogamente a ciò che git checkoutfa. Se si interrompe, git stashle modifiche e riprovare, oppure utilizzare --hardper perdere le modifiche (anche da file che non sono cambiati tra i commit!)

Trasferirsi in una nuova filiale

Questo metodo funziona creando un nuovo ramo con il primo comando ( git branch newbranch) ma senza passare ad esso. Quindi eseguiamo il rollback del ramo corrente (master) e passiamo al nuovo ramo per continuare a lavorare.

git branch newbranch      # Create a new branch, containing all current commits
git reset --keep HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
git checkout newbranch    # Go to the new branch that still has the desired commits
# Warning: after this it's not safe to do a rebase in newbranch without extra care.

Ma assicurati quanti si impegnano a tornare indietro. In alternativa, invece di HEAD~3, puoi semplicemente fornire l'hash del commit (o il riferimento simile origin/master) a cui vuoi tornare indietro, ad esempio:

git reset --keep a1b2c3d4

ATTENZIONE: con Git versione 2.0 e successive, se in seguito git rebaseil nuovo ramo sul ramo originale ( master), potresti aver bisogno di --no-fork-pointun'opzione esplicita durante il rebase per evitare di perdere i commit che hai spostato dal ramo principale. Avere branch.autosetuprebase alwaysimpostato rende questo più probabile. Vedi la risposta di John Mellor per i dettagli.


249
E in particolare, non tentare di risalire oltre il punto in cui hai spinto per l'ultima volta si trasferisce in un altro repository da cui qualcun altro avrebbe potuto estrarre.
Greg Hewgill,

107
Mi chiedo se puoi spiegare PERCHÉ funziona. Per me stai creando un nuovo ramo, rimuovendo 3 commit dal vecchio ramo in cui sei ancora e quindi controllando il ramo che hai creato. Quindi, come vengono mostrati magicamente i commit che hai rimosso nella nuova filiale?
Jonathan Dumaine,

142
@Jonathan Dumaine: Perché ho creato il nuovo ramo prima di rimuovere i commit dal vecchio ramo. Sono ancora lì nella nuova filiale.
Sykora,

86
i rami in git sono solo dei marcatori che indicano i commit nella storia, non c'è nulla da clonare, creare o eliminare (tranne i marcatori)
knittl

218
Nota anche: non farlo con modifiche senza commit nella tua copia di lavoro! Questo mi ha appena morso! :(
Adam Tuttle,

1039

Per quelli che si chiedono perché funzioni (come ero all'inizio):

Vuoi tornare a C e spostare D ed E sul nuovo ramo. Ecco come appare inizialmente:

A-B-C-D-E (HEAD)
        ↑
      master

Dopo git branch newBranch:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

Dopo git reset --hard HEAD~2:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

Poiché un ramo è solo un puntatore, il master ha indicato l'ultimo commit. Quando hai creato newBranch , hai semplicemente creato un nuovo puntatore all'ultimo commit. Quindi usando git resette hai spostato indietro il puntatore principale di due commit. Ma dal momento che non hai spostato newBranch , indica ancora il commit originariamente fatto.


56
Avevo anche bisogno di fare una git push origin master --forcemodifica perché il cambiamento venisse visualizzato nel repository principale.
Dženan,

9
Questa risposta causa la perdita di commit: la prossima volta git rebase, i 3 commit verranno automaticamente scartati newbranch. Vedi la mia risposta per dettagli e alternative più sicure.
John Mellor,

14
@ John, questa è una sciocchezza. Rebasing senza sapere cosa stai facendo causa la perdita di impegni. Se hai perso i commit, mi dispiace per te, ma questa risposta non ha perso i tuoi commit. Si noti che origin/masternon appare nel diagramma sopra. Se avessi spinto origin/mastere apportato le modifiche sopra, le cose sarebbero andate bene. Ma questo è un problema "Dottore, mi fa male quando faccio questo". Ed è fuori portata per quello che la domanda originale ha posto. Ti suggerisco di scrivere la tua domanda per esplorare il tuo scenario invece di dirottare questo.
Ryan Lundy,

1
@Giovanni, nella tua risposta, hai detto "Non farlo! git branch -t newbranch". Torna indietro e leggi di nuovo le risposte. Nessuno ha suggerito di farlo.
Ryan Lundy,

1
@Kyralessa, certo, ma se guardi il diagramma nella domanda, è chiaro che vogliono newbranchessere basati sul loro masterramo locale esistente . Dopo aver eseguito la risposta accettata, quando l'utente ottiene intorno a correre git rebasein newbranch, git ricorderà loro che si sono dimenticati di impostare il ramo a monte, in modo da faranno correre git branch --set-upstream-to=masterpoi git rebasee hanno lo stesso problema. Possono anche usare git branch -t newbranchin primo luogo.
John Mellor,

455

In generale...

Il metodo esposto da Sykora è l'opzione migliore in questo caso. Ma a volte non è il più semplice e non è un metodo generale. Per un metodo generale usa git cherry-pick :

Per ottenere ciò che l'OP vuole, è un processo in 2 fasi:

Passaggio 1: annotare quali commit dal master si desidera su a newbranch

Eseguire

git checkout master
git log

Nota gli hash di (diciamo 3) i commit che desideri newbranch. Qui userò:
C commit: 9aa1233
D commit: 453ac3d
E commit:612ecb3

Nota: è possibile utilizzare i primi sette caratteri o l'intero hash di commit

Passaggio 2: mettili sul newbranch

git checkout newbranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

OPPURE (su Git 1.7.2+, utilizzare gli intervalli)

git checkout newbranch
git cherry-pick 612ecb3~1..9aa1233

git cherry-pick applica questi tre commit a newbranch.


13
L'OP non stava forse cercando di spostarsi da master a newbranch? Se scegli la ciliegia mentre sei sul master, aggiungeresti al ramo master - aggiungendo i commit che aveva già, in effetti. E non si sposta indietro né sulla testa del ramo su B. O c'è qualcosa di sottile e freddo che non sto ottenendo?
RaveTheTadpole,

6
Funziona molto bene se si commette accidentalmente il ramo non master errato, quando si sarebbe dovuto creare un nuovo ramo di funzionalità.
luglio

5
+1 per un approccio utile in alcune situazioni. Questo è utile se vuoi solo inserire i tuoi commit (che sono intervallati da altri) in un nuovo ramo.
Tyler V.

8
È una risposta migliore. In questo modo puoi spostare i commit in qualsiasi ramo.
skywinder,

8
L'ordine della raccolta delle ciliegie è importante?
kon psych,

328

Ancora un altro modo per farlo, usando solo 2 comandi. Inoltre mantiene intatto il tuo attuale albero di lavoro.

git checkout -b newbranch # switch to a new branch
git branch -f master HEAD~3 # make master point to some older commit

Vecchia versione - prima che ne venissi a conoscenzagit branch -f

git checkout -b newbranch # switch to a new branch
git push . +HEAD~3:master # make master point to some older commit 

Essere in grado di pushfarlo .è un bel trucco da sapere.


1
Directory corrente. Immagino che funzionerebbe solo se ti trovi in ​​una directory superiore.
Aragaer,

1
La spinta locale sta inducendo un sorriso, ma riflettendo, come è diverso git branch -fqui?
jillill

2
@GerardSexton .è l'attuale direttore. git può eseguire il push su REMOTES o GIT URL. path to local directoryè supportata la sintassi degli URL Git. Vedi la sezione URL GIT in git help clone.
debole

31
Non so perché questo non sia valutato più in alto. Semplicemente morto, e senza il piccolo ma potenziale pericolo di reset git - duro.
Godsmith,

4
@Godsmith La mia ipotesi è che le persone preferiscano tre semplici comandi a due comandi leggermente più oscuri. Inoltre, le risposte più votate ottengono più voti per natura di essere visualizzate per prime.
JS_Riddler,

323

La maggior parte delle risposte precedenti sono pericolosamente sbagliate!

Non farlo:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

La prossima volta che eseguirai git rebase(o git pull --rebase) quei 3 commit verrebbero scartati silenziosamente newbranch! (vedi spiegazione sotto)

Invece fai questo:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • Per prima cosa elimina i 3 commit più recenti ( --keepè come --hard, ma più sicuro, poiché fallisce piuttosto che eliminare le modifiche non confermate).
  • Quindi si stacca newbranch.
  • Quindi raccoglie i 3 commit su Cherry newbranch. Dato che non sono più referenziati da un ramo, lo fa usando reflog di git : HEAD@{2}è il commit che HEADfaceva riferimento a 2 operazioni fa, vale a dire prima che noi 1. uscissimo newbranche 2. git resetscartassimo i 3 commit.

Attenzione: il reflog è abilitato di default, ma se lo hai disabilitato manualmente (ad es. Utilizzando un repository git "nudo"), non sarai in grado di riavere i 3 commit dopo l'esecuzione git reset --keep HEAD~3.

Un'alternativa che non si basa sul reflog è:

# newbranch will omit the 3 most recent commits.
git checkout -b newbranch HEAD~3
git branch --set-upstream-to=oldbranch
# Cherry-picks the extra commits from oldbranch.
git cherry-pick ..oldbranch
# Discards the 3 most recent commits from oldbranch.
git branch --force oldbranch oldbranch~3

(se preferisci puoi scrivere @{-1}- il ramo precedentemente estratto - invece di oldbranch).


Spiegazione tecnica

Perché git rebasescartare i 3 commit dopo il primo esempio? È perché git rebasesenza argomenti abilita l' --fork-pointopzione per impostazione predefinita, che utilizza il reflog locale per cercare di essere robusto contro il ramo a monte che viene forzato.

Supponiamo che tu abbia ramificato origin / master quando conteneva commit di M1, M2, M3, quindi hai effettuato tre commit da solo:

M1--M2--M3  <-- origin/master
         \
          T1--T2--T3  <-- topic

ma poi qualcuno riscrive la storia spingendo forzatamente origin / master per rimuovere M2:

M1--M3'  <-- origin/master
 \
  M2--M3--T1--T2--T3  <-- topic

Usando il tuo reflog locale, git rebasepuoi vedere che hai biforcato da una precedente incarnazione del ramo origine / master, e quindi che i commit M2 e M3 non fanno realmente parte del ramo topic. Quindi si presume ragionevolmente che da quando M2 è stato rimosso dal ramo a monte, non lo si desidera più nemmeno nel ramo argomento una volta che il ramo argomento è stato modificato:

M1--M3'  <-- origin/master
     \
      T1'--T2'--T3'  <-- topic (rebased)

Questo comportamento ha un senso ed è generalmente la cosa giusta da fare durante il rebasing.

Quindi il motivo per cui i seguenti comandi falliscono:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

è perché lasciano il reflog nello stato sbagliato. Git vede newbranchcome aver biforcato il ramo upstream in una revisione che include i 3 commit, quindi reset --hardriscrive la cronologia dell'upstream per rimuovere i commit, quindi la prossima volta che li esegui git rebaseli scarta come qualsiasi altro commit che è stato rimosso dall'upstream.

Ma in questo caso particolare vogliamo che quei 3 commit vengano considerati come parte del ramo dell'argomento. Per raggiungere questo obiettivo, dobbiamo sborsare l'upstream alla revisione precedente che non include i 3 commit. Questo è quello che fanno le mie soluzioni suggerite, quindi entrambi lasciano il reflog nello stato corretto.

Per maggiori dettagli, consultare la definizione di --fork-pointnei documenti git rebase e git merge-base .


15
Questa risposta dice "NON farlo!" sopra qualcosa che nessuno ha suggerito di fare.
Ryan Lundy,

4
La maggior parte delle persone non riscrive la storia pubblicata, specialmente su master. Quindi no, non sono pericolosamente sbagliati.
Walf,

4
@Kyralessa, il -triferimento a cui ti riferisci git branchavviene implicitamente se hai git config --global branch.autosetuprebase alwaysimpostato. Anche se non lo fai, ti ho già spiegato che lo stesso problema si verifica se imposti il ​​tracciamento dopo aver eseguito questi comandi, come probabilmente l'OP intende fare in base alla loro domanda.
John Mellor,

2
@ RockLee, sì, il modo generale per risolvere tali situazioni è creare un nuovo ramo (newbranch2) da un punto di partenza sicuro, quindi selezionare tutti gli commit che si desidera mantenere (da badnewbranch a newbranch2). Cherry-picking darà ai commit nuovi hash, quindi sarai in grado di rifare in sicurezza newbranch2 (e ora puoi eliminare badnewbranch).
John Mellor,

2
@Walf, hai frainteso: git rebase è progettato per essere robusto contro i flussi verso l'alto che hanno riscritto la loro storia. Sfortunatamente, gli effetti collaterali di quella solidità riguardano tutti, anche se né loro né il loro monte hanno mai riscritto la storia.
John Mellor,

150

Soluzione molto più semplice usando git stash

Ecco una soluzione molto più semplice per il commit nel ramo sbagliato. A partire da una filiale mastercon tre commit errati:

git reset HEAD~3
git stash
git checkout newbranch
git stash pop

Quando usarlo?

  • Se il tuo scopo principale è tornare indietro master
  • Vuoi mantenere le modifiche ai file
  • Non ti interessano i messaggi sui commit errati
  • Non hai ancora spinto
  • Vuoi che sia facile da memorizzare
  • Non vuoi complicazioni come rami temporanei / nuovi, trovare e copiare hash di commit e altri mal di testa

Cosa fa questo, per numero di riga

  1. Annulla gli ultimi tre commit (e i relativi messaggi) master, lasciando intatti tutti i file di lavoro
  2. Elimina tutte le modifiche al file di lavoro, rendendo l' masteralbero di lavoro esattamente uguale allo stato HEAD ~ 3
  3. Passa a un ramo esistente newbranch
  4. Applica le modifiche nascoste alla directory di lavoro e cancella la scorta

Ora puoi usare git adde git commitcome faresti normalmente. Tutti i nuovi commit verranno aggiunti a newbranch.

Cosa non fa

  • Non lascia rami casuali temporanei che ingombrano il tuo albero
  • Non conserva i messaggi di commit errati, quindi dovrai aggiungere un nuovo messaggio di commit a questo nuovo commit
  • Aggiornare! Utilizzare la freccia su per scorrere il buffer dei comandi per riapplicare il commit precedente con il relativo messaggio di commit (grazie @ARK)

obiettivi

Il PO ha dichiarato che l'obiettivo era "riportare il padrone a prima che questi impegni fossero fatti" senza perdere le modifiche e questa soluzione lo fa.

Lo faccio almeno una volta alla settimana quando faccio accidentalmente nuovi commit masterinvece di develop. Di solito ho solo un commit per il rollback, nel qual caso l'uso git reset HEAD^sulla linea 1 è un modo più semplice per eseguire il rollback di un solo commit.

Non farlo se hai spinto a monte le modifiche del master

Qualcun altro potrebbe aver apportato tali modifiche. Se stai solo riscrivendo il tuo master locale, non c'è alcun impatto quando viene spinto a monte, ma inviare una cronologia riscritta ai collaboratori può causare mal di testa.


4
Grazie, sono così felice di aver letto oltre / così tanto per arrivare a qui, perché è un caso d'uso abbastanza comune anche per me. Siamo così atipici?
Jim Mack,

6
Penso che siamo del tutto tipici e che "oops ho commesso per padroneggiare per errore" è il caso d'uso più comune per la necessità di ripristinare una manciata o meno di commit. Fortunatamente questa soluzione è così semplice che l'ho memorizzata ora.
Slam

9
Questa dovrebbe essere la risposta accettata. È semplice, facile da capire e facile da ricordare
Sina Madani,

1
Non credo che lo stashing sia necessario. L'ho fatto senza e ho funzionato bene.
A Campos,

1
Puoi anche recuperare facilmente i tuoi messaggi di commit, se ti capita di averli nella cronologia della CLI (riga di comando). Mi è capitato di avere entrambi i comandi git adde git commitche ho usato, quindi tutto quello che dovevo fare era premere la freccia su ed entrare alcune volte e boom! Tutto era tornato, ma ora sul ramo giusto.
Luke Gedeon,

30

Questo non li "sposta" in senso tecnico ma ha lo stesso effetto:

A--B--C  (branch-foo)
 \    ^-- I wanted them here!
  \
   D--E--F--G  (branch-bar)
      ^--^--^-- Opps wrong branch!

While on branch-bar:
$ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range)

A--B--C  (branch-foo)
 \
  \
   D-(E--F--G) detached
   ^-- (branch-bar)

Switch to branch-foo
$ git cherry-pick E..G

A--B--C--E'--F'--G' (branch-foo)
 \   E--F--G detached (This can be ignored)
  \ /
   D--H--I (branch-bar)

Now you won't need to worry about the detached branch because it is basically
like they are in the trash can waiting for the day it gets garbage collected.
Eventually some time in the far future it will look like:

A--B--C--E'--F'--G'--L--M--N--... (branch-foo)
 \
  \
   D--H--I--J--K--.... (branch-bar)

1
Non puoi usare rebaseper la stessa cosa?
Bergi,

Sì, è possibile utilizzare in alternativa rebaseil ramo distaccato nello scenario sopra.
Sukima,

24

Per fare ciò senza riscrivere la cronologia (ovvero se hai già eseguito il commit):

git checkout master
git revert <commitID(s)>
git checkout -b new-branch
git cherry-pick <commitID(s)>

Entrambi i rami possono quindi essere spinti senza forza!


Ma poi devi affrontare lo scenario di ripristino, che, a seconda delle circostanze, può essere molto più complicato. Se ripristini un commit sulla succursale, Git vedrà comunque quei commit come hanno avuto luogo, quindi per annullare ciò, devi ripristinare il ripristino. Questo brucia un bel po 'di persone, specialmente quando ripristinano un'unione e provano a fondere il ramo, solo per scoprire che Git crede che abbia già unito quel ramo (che è del tutto vero).
Makoto,

1
Ecco perché alla fine seleziono i commit su una nuova filiale. In questo modo git li vede come nuovi commit, il che risolve il problema.
teh_senaus,

Questo è più pericoloso di quanto sembri, dal momento che stai cambiando lo stato della storia del repository senza capire veramente le implicazioni di questo stato.
Makoto,

5
Non seguo il tuo argomento - il punto di questa risposta è che non stai cambiando la cronologia, semplicemente aggiungendo nuovi commit (che annullano efficacemente il ripristino delle modifiche). Questi nuovi commit possono essere spinti e uniti normalmente.
teh_senaus,

13

Aveva proprio questa situazione:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

Mi sono esibito:

git branch newbranch 
git reset --hard HEAD~8 
git checkout newbranch

Mi aspettavo che il commit sarei il HEAD, ma il commit L è adesso ...

Per essere sicuri di atterrare nel posto giusto nella storia, è più facile lavorare con l'hash del commit

git branch newbranch 
git reset --hard #########
git checkout newbranch

9

Come posso andare da questo

A - B - C - D - E 
                |
                master

a questo?

A - B - C - D - E 
    |           |
    master      newbranch

Con due comandi

  • git branch -m master newbranch

dando

A - B - C - D - E 
                |
                newbranch

e

  • git branch master B

dando

A - B - C - D - E
    |           |
    master      newbranch

Sì, funziona ed è abbastanza semplice. La GUI di Sourcetree è un po 'confusa sulle modifiche apportate alla shell git, ma dopo un recupero va tutto bene.
lars k.

Sì, sono come nella domanda. I primi due diagrammi intendono essere equivalenti a quelli della domanda, ridisegnando il modo in cui vorrei a scopo illustrativo nella risposta. In pratica, rinomina il ramo principale come newbranch e crea un nuovo ramo principale dove lo desideri.
Ivan,

Credo che questa sia la risposta giusta.
Leonardo Herrera

4

Se hai solo bisogno di spostare tutti i tuoi commit non compressi in una nuova filiale , allora devi solo,

  1. crea un nuovo ramo da quello attuale:git branch new-branch-name

  2. spingere la tua nuova filiale :git push origin new-branch-name

  3. ripristina il vecchio ramo (corrente) all'ultimo stato push / stabile:git reset --hard origin/old-branch-name

Alcune persone hanno anche altri upstreamspiuttosto che origin, dovrebbero usare appropriatiupstream


3

1) Crea un nuovo ramo, che sposta tutte le modifiche in new_branch.

git checkout -b new_branch

2) Quindi tornare al vecchio ramo.

git checkout master

3) Esegui git rebase

git rebase -i <short-hash-of-B-commit>

4) Quindi l'editor aperto contiene le ultime 3 informazioni di commit.

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) Passa picka droptutti e 3 i commit. Quindi salva e chiudi l'editor.

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) Ora gli ultimi 3 commit vengono rimossi dal ramo corrente ( master). Ora spingi il ramo con forza, con il +segno prima del nome del ramo.

git push origin +master

2

Puoi fare questo è solo 3 semplici passaggi che ho usato.

1) crea una nuova filiale in cui desideri eseguire il commit dell'aggiornamento recente.

git branch <branch name>

2) Trova ID commit recenti per commit su nuova filiale.

git log

3) Copia quel commit id nota che l'elenco di commit più recente ha luogo in cima. così puoi trovare il tuo impegno. lo trovi anche tramite messaggio.

git cherry-pick d34bcef232f6c...

puoi anche fornire alcuni ranghi di ID commit.

git cherry-pick d34bcef...86d2aec

Adesso il tuo lavoro è finito. Se hai scelto l'ID corretto e il ramo corretto, avrai successo. Quindi prima di fare questo stai attento. altrimenti può verificarsi un altro problema.

Ora puoi spingere il tuo codice

git push


0

Un altro modo per farlo:

[1] Rinomina il mastertuo ramo nel tuo newbranch(supponendo che tu sia sul masterramo):

git branch -m newbranch

[2] Crea il masterramo dal commit che desideri:

git checkout -b master <seven_char_commit_id>

es. git checkout -b master a34bc22

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.