Déplacer les commit les plus récents vers une nouvelle branche avec Git


4986

Je voudrais déplacer les dernières validations que je me suis engagé à maîtriser dans une nouvelle branche et les reprendre avant que ces validations ne soient effectuées. Malheureusement, mon Git-fu n'est pas encore assez fort, une aide?

Ie comment puis-je aller de là

master A - B - C - D - E

pour ça?

newbranch     C - D - E
             /
master A - B 

114
Remarque: J'ai posé la question opposée ici
Benjol


7
Les commentaires ont-ils été purgés ici? Je pose la question car lors de ma visite bimensuelle à cette question, je défile toujours par ce commentaire.
Tejas Kale

Réponses:


6453

Passer à une succursale existante

Si vous souhaitez déplacer vos commits vers une branche existante , cela ressemblera à ceci:

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' --keepoption conserve toutes les modifications non validées que vous pourriez avoir dans des fichiers non liés, ou annule si ces modifications doivent être écrasées - de la même manière que ce qui se git checkoutproduit. S'il échoue, git stashvos modifications et réessayez, ou utilisez --hardpour perdre les modifications (même à partir de fichiers qui n'ont pas changé entre les validations!)

Déménagement dans une nouvelle succursale

Cette méthode fonctionne en créant une nouvelle branche avec la première commande ( git branch newbranch) mais sans y basculer. Ensuite, nous restaurons la branche actuelle (maître) et passons à la nouvelle branche pour continuer à travailler.

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.

Mais assurez-vous du nombre de commits pour revenir. Alternativement, au lieu de HEAD~3, vous pouvez simplement fournir le hachage du commit (ou la référence comme origin/master) que vous souhaitez revenir, par exemple:

git reset --keep a1b2c3d4

AVERTISSEMENT: avec Git version 2.0 et versions ultérieures, si vous placez plus tard git rebasela nouvelle branche sur la branche d'origine ( master), vous aurez peut-être besoin d'une --no-fork-pointoption explicite lors du rebase pour éviter de perdre les validations que vous avez déplacées de la branche principale. La branch.autosetuprebase alwaysmise en place rend cela plus probable. Voir la réponse de John Mellor pour plus de détails.


249
Et en particulier, n'essayez pas de remonter plus loin que le dernier point où vous avez poussé les validations vers un autre référentiel à partir duquel quelqu'un d'autre aurait pu tirer.
Greg Hewgill

107
Vous vous demandez si vous pouvez expliquer POURQUOI cela fonctionne. Pour moi, vous créez une nouvelle branche, supprimez 3 commits de l'ancienne branche sur laquelle vous êtes toujours, puis vérifiez la branche que vous avez créée. Alors, comment les commits que vous avez supprimés apparaissent-ils comme par magie dans la nouvelle branche?
Jonathan Dumaine

142
@Jonathan Dumaine: Parce que j'ai créé la nouvelle branche avant de supprimer les validations de l'ancienne branche. Ils sont toujours là dans la nouvelle succursale.
sykora

86
les branches dans git ne sont que des marqueurs qui pointent vers des commits dans l'histoire, rien n'est cloné, créé ou supprimé (sauf les marqueurs)
knittl

218
Notez également: ne faites pas cela avec des modifications non validées dans votre copie de travail! Ça m'a juste mordu! :(
Adam Tuttle

1039

Pour ceux qui se demandent pourquoi cela fonctionne (comme je l'étais au début):

Vous voulez revenir à C et déplacer D et E vers la nouvelle branche. Voici à quoi cela ressemble au début:

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

Après git branch newBranch:

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

Après git reset --hard HEAD~2:

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

Puisqu'une branche n'est qu'un pointeur, master a indiqué le dernier commit. Lorsque vous avez créé newBranch , vous avez simplement créé un nouveau pointeur vers le dernier commit. Ensuite , en utilisant git resetvous avez déplacé le maître Backpointer deux commits. Mais comme vous n'avez pas déplacé newBranch , cela indique toujours le commit qu'il a fait à l'origine.


56
J'ai également dû faire un git push origin master --forcepour que la modification apparaisse dans le référentiel principal.
Dženan

9
Cette réponse entraîne la perte des commits: la prochaine fois que vous git rebase, les 3 commits seront supprimés en silence newbranch. Voir ma réponse pour plus de détails et des alternatives plus sûres.
John Mellor

14
@John, c'est absurde. Redéfinir sans savoir ce que vous faites provoque la perte des commits. Si vous avez perdu des commits, je suis désolé pour vous, mais cette réponse n'a pas perdu vos commits. Notez que cela origin/mastern'apparaît pas dans le diagramme ci-dessus. Si vous poussiez origin/masterpuis apportiez les modifications ci-dessus, bien sûr, les choses deviendraient drôles. Mais c'est un problème "Docteur, ça fait mal quand je fais ça". Et c'est hors de portée de ce que la question d'origine demandait. Je vous suggère d'écrire votre propre question pour explorer votre scénario au lieu de détourner celui-ci.
Ryan Lundy

1
@John, dans votre réponse, vous avez dit "Ne faites pas ça! git branch -t newbranch". Revenez en arrière et relisez les réponses. Personne n'a suggéré de faire ça.
Ryan Lundy

1
@ Kyralessa, bien sûr, mais si vous regardez le diagramme dans la question, il est clair qu'ils veulent newbranchêtre basés sur leur masterbranche locale existante . Après avoir effectué la réponse acceptée, lorsque l'utilisateur se déplace à courir git rebasedans newbranch, git leur rappellera qu'ils ont oublié de mettre la branche en amont, donc ils vont courir git branch --set-upstream-to=masterensuite git rebaseet ont le même problème. Ils peuvent tout aussi bien utiliser git branch -t newbranchen premier lieu.
John Mellor

455

En général...

La méthode exposée par sykora est la meilleure option dans ce cas. Mais parfois, ce n'est pas le plus simple et ce n'est pas une méthode générale. Pour une méthode générale, utilisez git cherry-pick :

Pour réaliser ce que OP souhaite, c'est un processus en 2 étapes:

Étape 1 - Notez les validations du master que vous souhaitez sur un newbranch

Exécuter

git checkout master
git log

Notez les hachages (par exemple 3) des commits que vous souhaitez newbranch. Ici, je vais utiliser:
C commit: 9aa1233
D commit: 453ac3d
E commit:612ecb3

Remarque: vous pouvez utiliser les sept premiers caractères ou l'intégralité du hachage de validation

Étape 2 - Mettez-les sur le newbranch

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

OU (sur Git 1.7.2+, utilisez des plages)

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

git cherry-pick applique ces trois commits à newbranch.


13
L'OP n'essayait-il pas de passer de maître à newbranch? Si vous faites votre choix pendant que vous êtes sur master, vous ajouterez à la branche master - en ajoutant des commits qu'elle avait déjà, en fait. Et cela ne recule pas non plus vers la tête de la branche vers B. Ou y a-t-il quelque chose de subtil et de cool que je ne reçois pas?
RaveTheTadpole

6
Cela fonctionne très bien si vous validez accidentellement la mauvaise branche non principale, alors que vous auriez dû créer une nouvelle branche de fonctionnalité.
julianc le

5
+1 pour une approche utile dans certaines situations. C'est bien si vous souhaitez uniquement extraire vos propres validations (qui sont entrecoupées d'autres) dans une nouvelle branche.
Tyler V.

8
C'est une meilleure réponse. De cette façon, vous pouvez déplacer les validations vers n'importe quelle branche.
skywinder

8
L'ordre de la cueillette des cerises est-il important?
kon psych

328

Encore une autre façon de le faire, en utilisant seulement 2 commandes. Maintient également intact votre arbre de travail actuel.

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

Ancienne version - avant d'en savoir plusgit branch -f

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

Être capable de le pushfaire .est une bonne astuce à savoir.


1
Répertoire actuel. Je suppose que cela ne fonctionnerait que si vous êtes dans un répertoire supérieur.
aragaer

1
La poussée locale est induisant un sourire, mais à la réflexion, en quoi est-ce différent git branch -fici?
jthill

2
@GerardSexton .est le directeur actuel. git peut pousser vers REMOTES ou GIT URLs. path to local directoryest prise en charge la syntaxe des URL Git. Voir la section URL GIT dans git help clone.
faible

31
Je ne sais pas pourquoi ce n'est pas noté plus haut. Très simple, et sans le petit mais potentiel danger de réinitialisation de git --hard.
Godsmith

4
@Godsmith Je suppose que les gens préfèrent trois commandes simples à deux commandes légèrement plus obscures. De plus, les réponses les plus votées obtiennent plus de votes positifs par nature d'être affichées en premier.
JS_Riddler

323

La plupart des réponses précédentes sont dangereusement erronées!

Ne faites pas cela:

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

Comme la prochaine fois que vous exécuterez git rebase(ou git pull --rebase) ces 3 commits seront éliminés en silence newbranch! (voir explication ci-dessous)

Au lieu de cela, procédez comme suit:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • Tout d'abord, il supprime les 3 commits les plus récents ( --keepest similaire --hard, mais plus sûr, car échoue plutôt que de supprimer les modifications non validées).
  • Ensuite, il bifurque newbranch.
  • Ensuite, il sélectionne ces 3 commits newbranch. Puisqu'ils ne sont plus référencés par une branche, il le fait en utilisant le reflog de git : HEAD@{2}est le commit qui HEADfaisait référence à 2 opérations auparavant, c'est-à-dire avant que nous 1. vérifions newbranchet 2. utilisé git resetpour rejeter les 3 commits.

Attention: le reflog est activé par défaut, mais si vous l'avez désactivé manuellement (par exemple en utilisant un dépôt git "nu"), vous ne pourrez pas récupérer les 3 commits après avoir exécuté git reset --keep HEAD~3.

Une alternative qui ne repose pas sur le reflog est:

# 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

(si vous préférez, vous pouvez écrire @{-1}- la branche précédemment vérifiée - au lieu de oldbranch).


Explication technique

Pourquoi git rebaserejeter les 3 commits après le premier exemple? C'est parce git rebaseque sans argument active l' --fork-pointoption par défaut, qui utilise le reflog local pour essayer d'être robuste contre la branche en amont poussée de force.

Supposons que vous ayez dérivé origine / master lorsqu'il contenait des validations M1, M2, M3, puis que vous ayez effectué trois validations vous-même:

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

mais ensuite quelqu'un réécrit l'histoire en poussant force / origin pour supprimer M2:

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

En utilisant votre reflog local, vous git rebasepouvez voir que vous êtes issu d'une incarnation antérieure de la branche origine / maître, et donc que les validations M2 et M3 ne font pas vraiment partie de votre branche de sujet. Par conséquent, il suppose raisonnablement que puisque M2 a été supprimé de la branche en amont, vous ne le souhaitez plus dans votre branche de sujet une fois que la branche de thème est rebasée:

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

Ce comportement est logique et est généralement la bonne chose à faire lors du rebasage.

Donc, la raison pour laquelle les commandes suivantes échouent:

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

est parce qu'ils laissent le reflog dans le mauvais état. Git voit newbranchcomme ayant bifurqué la branche amont lors d'une révision qui inclut les 3 validations, puis reset --hardréécrit l'historique de l'amont pour supprimer les validations, et donc la prochaine fois que vous l'exécutez, git rebaseil les supprime comme toute autre validation qui a été supprimée de l'amont.

Mais dans ce cas particulier, nous voulons que ces 3 commits soient considérés comme faisant partie de la branche du sujet. Pour y parvenir, nous devons bifurquer en amont lors de la révision précédente qui n'inclut pas les 3 commits. C'est ce que mes solutions suggérées font, donc elles laissent toutes deux le reflog dans l'état correct.

Pour plus de détails, voir la définition de --fork-pointdans les documents git rebase et git merge-base .


15
Cette réponse dit "Ne faites pas ça!" au-dessus de quelque chose que personne n'a suggéré de faire.
Ryan Lundy

4
La plupart des gens ne réécrivent pas l'historique publié, en particulier sur master. Alors non, ils n'ont pas dangereusement tort.
Walf

4
@Kyralessa, le auquel -tvous faites référence se git branchproduit implicitement si vous avez git config --global branch.autosetuprebase alwaysdéfini. Même si vous ne le faites pas, je vous ai déjà expliqué que le même problème se produit si vous configurez le suivi après avoir exécuté ces commandes, comme l'OP a probablement l'intention de le faire compte tenu de leur question.
John Mellor

2
@RockLee, oui, la manière générale de résoudre de telles situations est de créer une nouvelle branche (newbranch2) à partir d'un point de départ sûr, puis de sélectionner tous les commits que vous souhaitez conserver (de badnewbranch à newbranch2). La sélection de cerises donnera aux commits de nouveaux hachages, vous pourrez donc rebaser en toute sécurité newbranch2 (et pouvez maintenant supprimer badnewbranch).
John Mellor

2
@Walf, vous avez mal compris: git rebase est conçu pour être robuste contre les amonts ayant leur histoire réécrite. Malheureusement, les effets secondaires de cette robustesse affectent tout le monde, même si ni eux ni leur amont ne réécrivent jamais l'histoire.
John Mellor

150

Solution beaucoup plus simple utilisant git stash

Voici une solution beaucoup plus simple pour les validations dans la mauvaise branche. Commencer sur une branche masterqui a trois commits erronés:

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

Quand l'utiliser?

  • Si votre objectif principal est de revenir en arrière master
  • Vous souhaitez conserver les modifications de fichiers
  • Vous ne vous souciez pas des messages sur les commits erronés
  • Tu n'as pas encore poussé
  • Vous voulez que ce soit facile à mémoriser
  • Vous ne voulez pas de complications comme des branches temporaires / nouvelles, la recherche et la copie de hachages de validation et d'autres maux de tête

Ce que cela fait, par numéro de ligne

  1. Annule les trois derniers commits (et leurs messages) master, mais laisse tous les fichiers de travail intacts
  2. Cache toutes les modifications du fichier de travail, ce qui rend l' masterarborescence de travail exactement égale à l'état HEAD ~ 3
  3. Bascule vers une branche existante newbranch
  4. Applique les modifications cachées à votre répertoire de travail et efface la cachette

Vous pouvez maintenant utiliser git addet git commitcomme vous le feriez normalement. Tous les nouveaux commits seront ajoutés à newbranch.

Ce que cela ne fait pas

  • Il ne laisse pas de branches temporaires aléatoires encombrer votre arbre
  • Il ne conserve pas les messages de commit erronés, vous devrez donc ajouter un nouveau message de commit à ce nouveau commit
  • Mise à jour! Utilisez la flèche vers le haut pour faire défiler votre tampon de commande pour réappliquer le commit précédent avec son message de commit (merci @ARK)

Buts

Le PO a déclaré que l'objectif était de "ramener le maître avant que ces commits ne soient effectués" sans perdre les modifications et cette solution le fait.

Je le fais au moins une fois par semaine lorsque je fais accidentellement de nouveaux commits au masterlieu de develop. Habituellement, je n'ai qu'une seule validation de restauration, auquel cas l'utilisation git reset HEAD^de la ligne 1 est un moyen plus simple de restaurer une seule validation.

Ne faites pas cela si vous avez poussé les changements de master en amont

Quelqu'un d'autre a peut-être tiré ces changements. Si vous ne réécrivez que votre maître local, il n'y a aucun impact lorsqu'il est poussé en amont, mais pousser un historique réécrit aux collaborateurs peut provoquer des maux de tête.


4
Merci, je suis tellement content d'avoir lu tant de choses sur le passé / à travers ici, car c'est aussi un cas d'utilisation assez courant pour moi. Sommes-nous si atypiques?
Jim Mack

6
Je pense que nous sommes totalement typiques et "Oups, je me suis engagé à maîtriser par erreur" est le cas d'utilisation le plus courant pour le besoin d'annuler une poignée ou moins de commits. Heureusement, cette solution est si simple que je l'ai mémorisée maintenant.
Slam

9
Ce devrait être la réponse acceptée. C'est simple, facile à comprendre et facile à retenir
Sina Madani

1
Je ne pense pas que le stockage soit nécessaire. Je l'ai juste fait sans et j'ai bien travaillé.
Un Campos

1
Vous pouvez également récupérer facilement vos messages de validation si vous les avez dans votre historique CLI (ligne de commande). Il se trouve que j'avais à la fois les commandes git addet git commitque j'ai utilisées, donc tout ce que j'avais à faire était d'appuyer sur la flèche vers le haut et d'entrer quelques fois et boum! Tout était de retour, mais sur la bonne branche maintenant.
Luke Gedeon

30

Cela ne les "déplace" pas au sens technique mais cela a le même effet:

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
Tu ne peux pas utiliser rebasepour la même chose?
Bergi

Oui, vous pouvez également utiliser rebasesur la branche détachée dans le scénario ci-dessus.
Sukima

24

Pour ce faire sans réécrire l'historique (c'est-à-dire si vous avez déjà poussé les commits):

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

Les deux branches peuvent alors être poussées sans force!


Mais alors vous devez faire face au scénario de retour, qui, selon votre situation, peut être beaucoup plus délicat. Si vous annulez un commit sur la branche, Git verra toujours ces commits comme ayant eu lieu, donc pour annuler cela, vous devez annuler le retour. Cela brûle pas mal de gens, surtout lorsqu'ils annulent une fusion et essaient de fusionner la branche en arrière, seulement pour constater que Git pense qu'il a déjà fusionné cette branche (ce qui est tout à fait vrai).
Makoto

1
C'est pourquoi je sélectionne les commits à la fin, sur une nouvelle branche. De cette façon, git les considère comme de nouveaux commits, ce qui résout votre problème.
teh_senaus

C'est plus dangereux qu'il n'y paraît à première vue, car vous modifiez l'état de l'historique du référentiel sans vraiment comprendre les implications de cet état.
Makoto

5
Je ne suis pas votre argument - le point de cette réponse est que vous ne changez pas l'histoire, en ajoutant simplement de nouveaux commits (qui annulent efficacement les modifications). Ces nouveaux commits peuvent être poussés et fusionnés comme d'habitude.
teh_senaus

13

Eu juste cette situation:

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

J'ai joué:

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

Je m'attendais à ce que je devienne le HEAD, mais le commit L l'est maintenant ...

Pour être sûr d'atterrir au bon endroit dans l'histoire, il est plus facile de travailler avec le hachage du commit

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

9

Comment puis-je y aller

A - B - C - D - E 
                |
                master

pour ça?

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

Avec deux commandes

  • git branch -m master newbranch

donnant

A - B - C - D - E 
                |
                newbranch

et

  • git branch master B

donnant

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

Oui, cela fonctionne et est assez facile. L'interface utilisateur graphique de Sourcetree est un peu confuse quant aux modifications apportées dans le shell git, mais après une récupération, tout va bien à nouveau.
lars k.

Oui, c'est comme dans la question. Les deux premiers diagrammes sont censés être équivalents à ceux de la question, juste redessinés comme je le voudrais à des fins d'illustration dans la réponse. Renommez fondamentalement la branche principale en nouvelle branche et créez une nouvelle branche principale où vous le souhaitez.
Ivan

Je pense que c'est la bonne réponse.
Leonardo Herrera Il y a

4

Si vous avez juste besoin de déplacer tous vos commits non poussés vers une nouvelle branche , il vous suffit de le faire,

  1. créer une nouvelle branche à partir de la branche actuelle:git branch new-branch-name

  2. Poussez votre nouvelle branche :git push origin new-branch-name

  3. ramenez votre ancienne branche (actuelle) au dernier état poussé / stable:git reset --hard origin/old-branch-name

Certaines personnes ont aussi d' autres upstreamsplutôt que origin, ils devraient utiliser appropriésupstream


3

1) Créez une nouvelle branche, qui déplace toutes vos modifications vers new_branch.

git checkout -b new_branch

2) Revenez ensuite à l'ancienne branche.

git checkout master

3) Faites git rebase

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

4) Ensuite, l'éditeur ouvert contient les 3 dernières informations de validation.

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

5) Passez pickà droptous ces 3 commits. Enregistrez puis fermez l'éditeur.

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

6) Les 3 derniers commits sont maintenant supprimés de la branche actuelle ( master). Poussez maintenant la branche avec force, avec un +signe avant le nom de la branche.

git push origin +master

2

Vous pouvez le faire en seulement 3 étapes simples que j'ai utilisées.

1) créer une nouvelle branche où vous souhaitez vous engager votre récente mise à jour.

git branch <branch name>

2) Trouver l'ID de validation récente pour la validation sur une nouvelle branche.

git log

3) Copiez cet ID de validation. Notez que la liste des validations les plus récentes a lieu en haut. afin que vous puissiez trouver votre commit. vous le trouvez également via un message.

git cherry-pick d34bcef232f6c...

vous pouvez également fournir un certain ID de validation.

git cherry-pick d34bcef...86d2aec

Maintenant, votre travail est terminé. Si vous avez choisi le bon identifiant et la bonne branche, vous réussirez. Avant de faire cela, soyez prudent. sinon un autre problème peut survenir.

Maintenant, vous pouvez pousser votre code

git push


0

Une autre façon de procéder:

[1] Renommez la masterbranche en votre newbranch(en supposant que vous êtes sur la masterbranche):

git branch -m newbranch

[2] Créez une masterbranche à partir du commit que vous souhaitez:

git checkout -b master <seven_char_commit_id>

par exemple git checkout -b master a34bc22

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.