Extraire une autre branche lorsqu'il y a des modifications non validées sur la branche actuelle

352

La plupart du temps, lorsque j'essaie de retirer une autre branche existante, Git ne me permet pas si j'ai des modifications non validées sur la branche actuelle. Je vais donc devoir valider ou cacher ces modifications en premier.

Cependant, Git m'autorise parfois à extraire une autre branche sans valider ou masquer ces modifications, et il portera ces modifications à la branche que j'extraye.

Quelle est la règle ici? Est-il important que les modifications soient échelonnées ou non? Porter les changements dans une autre branche n'a aucun sens pour moi, pourquoi git le permet-il parfois? Autrement dit, est-ce utile dans certaines situations?

Xufeng
la source

Réponses:

354

Notes préliminaires

L'observation ici est que, après avoir commencé à travailler dans branch1(en oubliant ou en ne réalisant pas qu'il serait bon de passer d'abord à une autre branche branch2), vous exécutez:

git checkout branch2

Parfois Git dit "OK, tu es sur branch2 maintenant!" Parfois, Git dit "Je ne peux pas faire ça, je perdrais certains de vos changements."

Si Git ne vous laisse pas le faire, vous devez valider vos modifications, pour les enregistrer quelque part de façon permanente. Vous voudrez peut-être utiliser git stashpour les enregistrer; c'est l'une des choses pour lesquelles il est conçu. Notez que git stash saveou signifiegit stash push en fait "Validez toutes les modifications, mais sur aucune branche du tout, puis supprimez-les de l'endroit où je suis maintenant." Cela permet de basculer: vous n'avez désormais plus de modifications en cours. Vous pouvez ensuite git stash applyles après avoir changé.

Barre latérale: git stash savec'est l'ancienne syntaxe; git stash pusha été introduit dans Git version 2.13, pour résoudre certains problèmes avec les arguments git stashet permettre de nouvelles options. Les deux font la même chose, lorsqu'ils sont utilisés de manière simple.

Vous pouvez arrêter de lire ici, si vous le souhaitez!

Si Git ne vous laisse pas changer, vous avez déjà un remède: utilisez git stashou git commit; ou, si vos modifications sont faciles à recréer, utilisez-les git checkout -fpour la forcer. Cette réponse concerne le moment où Git vous autorisera git checkout branch2même si vous avez commencé à apporter des modifications. Pourquoi ça marche parfois , et pas d' autres fois?

La règle ici est simple dans un sens, et compliquée / difficile à expliquer dans un autre:

Vous pouvez changer de branche avec des modifications non validées dans l'arborescence de travail si et seulement si ladite commutation ne nécessite pas d'altérer ces modifications.

C'est-à-dire et veuillez noter que cela est encore simplifié; il y a des cas d'angle extra-difficiles avec des git adds, git rms et autres par étapes - supposons que vous soyez allumé branch1. A git checkout branch2devrait faire ceci:

  • Pour chaque fichier qui se trouve dans branch1et non dans branch2, 1 supprimez ce fichier.
  • Pour chaque fichier qui est dedans branch2et pas dedans branch1, créez ce fichier (avec le contenu approprié).
  • Pour chaque fichier qui se trouve dans les deux branches, si la version de branch2est différente, mettez à jour la version de l'arborescence de travail.

Chacune de ces étapes pourrait encombrer quelque chose dans votre arbre de travail:

  • La suppression d'un fichier est "sûre" si la version dans l'arborescence est la même que la version validée dans branch1; c'est "dangereux" si vous avez apporté des modifications.
  • Créer un fichier tel qu'il apparaît branch2est "sûr" s'il n'existe pas maintenant. 2 Il est «dangereux» s'il existe maintenant mais a un «mauvais» contenu.
  • Et bien sûr, remplacer la version d'arbre de travail d'un fichier par une version différente est "sûr" si la version d'arbre de travail est déjà validée branch1.

La création d'une nouvelle branche ( git checkout -b newbranch) est toujours considérée comme "sûre": aucun fichier ne sera ajouté, supprimé ou modifié dans l'arborescence de travail dans le cadre de ce processus, et l'index / zone de transfert est également intact. (Attention: il est sûr de créer une nouvelle branche sans changer le point de départ de la nouvelle branche; mais si vous ajoutez un autre argument, par exemple git checkout -b newbranch different-start-point, cela pourrait devoir changer les choses, pour passer à different-start-point. Git appliquera alors les règles de sécurité de la caisse comme d'habitude .)


1 Cela nécessite que nous définissions ce que signifie pour un fichier d'être dans une branche, ce qui à son tour nécessite de définir correctement le mot branche . (Voir aussi Qu'entendons-nous exactement par "branche"? ) Ici, ce que je veux vraiment dire, c'est le commit auquel le nom de branche se résout: un fichier dont le chemin est dans if produit un hachage. Ce fichier n'est pas dans si vous obtenez un message d'erreur à la place. L'existence d'un chemin dans votre index ou votre arbre de travail n'est pas pertinente pour répondre à cette question particulière. Ainsi, le secret ici est d'examiner le résultat de chaqueP branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path. Cela échoue parce que le fichier est "dans" au plus une branche, ou nous donne deux ID de hachage. Si les deux ID de hachage sont identiques , le fichier est le même dans les deux branches. Aucun changement n'est requis. Si les ID de hachage diffèrent, le fichier est différent dans les deux branches et doit être modifié pour changer de branche.

L'idée clé ici est que les fichiers dans les validations sont gelés pour toujours. Les fichiers que vous éditez ne sont évidemment pas figés. Nous ne nous penchons, au moins initialement, que sur les décalages entre deux commits gelés. Malheureusement, nous - ou Git - devons également traiter les fichiers qui ne figurent pas dans la validation à partir de laquelle vous allez basculer et qui se trouvent dans la validation vers laquelle vous allez basculer. Cela conduit aux complications restantes, car les fichiers peuvent également exister dans l'index et / ou dans l'arborescence de travail, sans avoir à exister ces deux validations gelées particulières avec lesquelles nous travaillons.

2 Il peut être considéré comme "sorte de coffre-fort" s'il existe déjà avec le "bon contenu", de sorte que Git n'a pas à le créer après tout. Je me souviens au moins de certaines versions de Git autorisant cela, mais les tests montrent maintenant qu'il est considéré comme "dangereux" dans Git 1.8.5.4. Le même argument s'appliquerait à un fichier modifié qui se trouve être modifié pour correspondre à la branche to-be-switch-to. Encore une fois, le 1.8.5.4 dit simplement "serait écrasé", cependant. Voir également la fin des notes techniques: ma mémoire peut être défectueuse car je ne pense pas que les règles de l'arborescence de lecture ont changé depuis que j'ai commencé à utiliser Git dans la version 1.5.


Est-il important que les modifications soient échelonnées ou non?

Oui, à certains égards. En particulier, vous pouvez mettre en place une modification, puis "dé-modifier" le fichier d'arborescence de travail. Voici un fichier en deux branches, différent dans branch1et branch2:

$ git show branch1:inboth
this file is in both branches
$ git show branch2:inboth
this file is in both branches
but it has more stuff in branch2 now
$ git checkout branch1
Switched to branch 'branch1'
$ echo 'but it has more stuff in branch2 now' >> inboth

À ce stade, le fichier d'arbre de travail inbothcorrespond à celui de branch2, même si nous sommes allumés branch1. Cette modification n'est pas mise en scène pour la validation, ce qui est git status --shortillustré ici:

$ git status --short
 M inboth

L'espace-alors-M signifie "modifié mais pas par étapes" (ou plus précisément, la copie d'arbre de travail diffère de la copie par étapes / index).

$ git checkout branch2
error: Your local changes ...

OK, passons maintenant à la copie de l'arborescence de travail, qui, nous le savons déjà, correspond également à la copie branch2.

$ git add inboth
$ git status --short
M  inboth
$ git checkout branch2
Switched to branch 'branch2'

Ici, les copies mises en scène et de travail correspondaient toutes les deux à ce qui se trouvait branch2, donc le paiement a été autorisé.

Essayons une autre étape:

$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches

La modification que j'ai apportée est maintenant perdue dans la zone de transit (car le paiement est écrit dans la zone de transit). Ceci est un peu un cas d'angle. Le changement n'est pas parti, mais le fait que je l'avais mis en scène, est parti.

Préparons une troisième variante du fichier, différente de chaque copie de branche, puis définissons la copie de travail pour qu'elle corresponde à la version actuelle de la branche:

$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth

Les deux Ms ici signifient: le fichier intermédiaire diffère du HEADfichier et le fichier d'arborescence de travail diffère du fichier intermédiaire. La version de l'arbre de travail correspond à la version branch1(aka HEAD):

$ git diff HEAD
$

Mais git checkoutne permettra pas le paiement:

$ git checkout branch2
error: Your local changes ...

Définissons la branch2version comme version de travail:

$ git show branch2:inboth > inboth
$ git status --short
MM inboth
$ git diff HEAD
diff --git a/inboth b/inboth
index ecb07f7..aee20fb 100644
--- a/inboth
+++ b/inboth
@@ -1 +1,2 @@
 this file is in both branches
+but it has more stuff in branch2 now
$ git diff branch2 -- inboth
$ git checkout branch2
error: Your local changes ...

Même si la copie de travail actuelle correspond à celle de branch2, le fichier intermédiaire ne le fait pas, donc a git checkoutperdrait cette copie et git checkoutest rejeté.

Notes techniques - uniquement pour les fous curieux :-)

L' indice d' implémentation sous-jacent de tout cela est l' indice de Git . L'index, également appelé "zone de transit", est l'endroit où vous générez le prochain commit: il commence par correspondre au commit actuel, c'est-à-dire, tout ce que vous avez extrait maintenant, puis à chaque fois que vous git addun fichier, vous remplacez la version d'index avec tout ce que vous avez dans votre arbre de travail.

N'oubliez pas que l' arborescence est l'endroit où vous travaillez sur vos fichiers. Ici, ils ont leur forme normale, plutôt qu'une forme spéciale uniquement utile à Git comme ils le font dans les commits et dans l'index. Vous extrayez donc un fichier d' une validation, via l'index, puis dans l'arborescence de travail. Après l'avoir changé, vous git addle mettez à l'index. Il y a donc en fait trois emplacements pour chaque fichier: la validation actuelle, l'index et l'arbre de travail.

Lorsque vous exécutez git checkout branch2, ce que Git fait sous les couvertures, c'est de comparer le commit de tipbranch2 à ce qui se trouve à la fois dans le commit actuel et dans l'index maintenant. Tout fichier qui correspond à ce qui existe maintenant, Git peut laisser seul. Tout est intact. N'importe quel fichier identique dans les deux validations , Git peut également rester seul - et ce sont ceux qui vous permettent de changer de branche.

Une grande partie de Git, y compris la commutation de validation, est relativement rapide en raison de cet index. Ce qui est réellement dans l'index n'est pas chaque fichier lui-même, mais plutôt le hachage de chaque fichier . La copie du fichier lui-même est stockée en tant que ce que Git appelle un objet blob , dans le référentiel. Cela est similaire à la façon dont les fichiers sont également stockés dans les validations: les validations ne contiennent pas réellement les fichiers , elles conduisent simplement Git à l'ID de hachage de chaque fichier. Git peut donc comparer les ID de hachage - actuellement des chaînes de 160 bits - pour décider si les validations X et Y ont le même fichier ou non. Il peut ensuite comparer ces ID de hachage à l'ID de hachage dans l'index également.

C'est ce qui mène à tous les cas de coin excentriques ci-dessus. Nous avons des commits X et Y qui ont tous deux un fichier path/to/name.txt, et nous avons une entrée d'index pour path/to/name.txt. Peut-être que les trois hachages correspondent. Peut-être que deux d'entre eux correspondent et un ne correspond pas. Peut-être que les trois sont différents. Et, nous pourrions aussi avoir another/file.txtce n'est qu'en X ou seulement en Y et est ou n'est pas dans l'index maintenant. Chacun de ces différents cas nécessite sa propre considération: Git doit-il copier le fichier de la validation vers l'index, ou le supprimer de l'index, pour passer de X à Y ? Si oui, il doit égalementcopiez le fichier dans l'arborescence ou supprimez-le de l'arborescence. Et si tel est le cas, les versions d'index et d'arborescence de travail devraient mieux correspondre à au moins une des versions validées; sinon Git va encombrer certaines données.

(Les règles complètes pour tout cela sont décrites dans, non pas la git checkoutdocumentation comme vous pouvez vous y attendre, mais plutôt la git read-treedocumentation, dans la section intitulée "Two Tree Merge" .)

torek
la source
3
... il y a aussi git checkout -m, qui fusionne votre arbre de travail et les modifications d'index dans la nouvelle caisse.
2017
1
Merci pour cette excellente explication! Mais où puis-je trouver les informations dans les documents officiels? Ou sont-ils incomplets? Si oui, quelle est la référence faisant autorité pour git (si tout va bien autre que son code source)?
maximum
1
(1) vous ne pouvez pas, et (2) le code source. Le principal problème est que Git est en constante évolution. Par exemple, en ce moment, il y a une grande poussée pour augmenter ou abandonner SHA-1 avec ou en faveur de SHA-256. Cependant, cette partie particulière de Git est assez stable depuis longtemps et le mécanisme sous-jacent est simple: Git compare l'index actuel aux validations actuelles et cibles et décide quels fichiers modifier (le cas échéant) en fonction de la validation cible , teste ensuite la "propreté" des fichiers d'arborescence de travail si l'entrée d'index doit être remplacée.
torek
6
Réponse courte: il y a une règle, mais il est trop obtus pour que l'utilisateur moyen ait le moindre espoir de comprendre et encore moins de se souvenir.Par conséquent, au lieu de compter sur l'outil pour vous comporter de manière intelligible, vous devriez plutôt vous fier à la convention disciplinée de ne vérifier que lorsque votre la branche actuelle est engagée et propre. Je ne vois pas comment cela répond à la question de savoir quand il serait utile de reporter des changements exceptionnels dans une autre branche, mais je l'ai peut-être manqué parce que j'ai du mal à le comprendre.
Neutrino
2
@HawkeyeParker: cette réponse a subi de nombreuses modifications, et je ne suis pas sûr que l'une d'elles l'ait beaucoup améliorée, mais je vais essayer d'ajouter quelque chose sur ce que cela signifie pour un fichier d'être "dans une branche". En fin de compte, cela va être bancal car la notion de "branche" ici n'est pas correctement définie en premier lieu, mais c'est encore un autre élément.
torek
51

Vous avez deux choix: cachez vos modifications:

git stash

puis plus tard pour les récupérer:

git stash apply

ou placez vos modifications sur une branche pour pouvoir obtenir la branche distante, puis fusionner vos modifications dessus. C'est l'une des meilleures choses à propos de git: vous pouvez créer une branche, vous y engager, puis récupérer d'autres modifications dans la branche sur laquelle vous étiez.

Vous dites que cela n'a aucun sens, mais vous ne le faites que pour pouvoir les fusionner à volonté après avoir fait le pull. De toute évidence, votre autre choix est de valider votre copie de la branche, puis de faire le pull. La présomption est que vous ne voulez pas faire cela (auquel cas je suis perplexe que vous ne vouliez pas de succursale) ou vous avez peur des conflits.

Rob
la source
1
N'est-ce pas la bonne commande git stash apply? ici les docs.
Thomas8
1
Exactement ce que je cherchais, passer temporairement à différentes branches, rechercher quelque chose et revenir au même état de la branche sur laquelle je travaille. Merci Rob!
Naishta
1
Oui, c'est la bonne façon de procéder. J'apprécie le détail de la réponse acceptée, mais cela rend les choses plus difficiles qu'elles ne devraient l'être.
Michael Leonard
5
De plus, si vous n'avez pas besoin de conserver la cachette, vous pouvez l'utiliser git stash popet elle supprimera la cachette de votre liste si elle s'applique avec succès.
Michael Leonard
1
meilleure utilisation git stash pop, sauf si vous avez l'intention de garder un journal des cachettes dans votre historique de mise en pension
Damilola Olowookere
14

Si la nouvelle branche contient des modifications différentes de la branche actuelle pour ce fichier modifié particulier, elle ne vous permettra pas de changer de branche tant que la modification n'est pas validée ou stockée. Si le fichier modifié est le même sur les deux branches (c'est-à-dire la version validée de ce fichier), vous pouvez basculer librement.

Exemple:

$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "adding file.txt"

$ git checkout -b experiment
$ echo 'goodbye world' >> file.txt
$ git add file.txt
$ git commit -m "added text"
     # experiment now contains changes that master doesn't have
     # any future changes to this file will keep you from changing branches
     # until the changes are stashed or committed

$ echo "and we're back" >> file.txt  # making additional changes
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
    file.txt
Please, commit your changes or stash them before you can switch branches.
Aborting

Cela vaut pour les fichiers non suivis ainsi que les fichiers suivis. Voici un exemple de fichier non suivi.

Exemple:

$ git checkout -b experimental  # creates new branch 'experimental'
$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "added file.txt"

$ git checkout master # master does not have file.txt
$ echo 'goodbye world' > file.txt
$ git checkout experimental
error: The following untracked working tree files would be overwritten by checkout:
    file.txt
Please move or remove them before you can switch branches.
Aborting

Un bon exemple de la raison pour laquelle vous VOULEZ vous déplacer entre les branches tout en apportant des modifications serait si vous réalisiez des expériences sur master, vouliez les valider, mais pas pour master tout de suite ...

$ echo 'experimental change' >> file.txt # change to existing tracked file
   # I want to save these, but not on master

$ git checkout -b experiment
M       file.txt
Switched to branch 'experiment'
$ git add file.txt
$ git commit -m "possible modification for file.txt"
Gordolio
la source
En fait, je ne comprends toujours pas très bien. Dans votre premier exemple, après avoir ajouté "et nous sommes de retour", il est dit que le changement local sera écrasé, quel changement local exactement? "et nous sommes de retour"? Pourquoi git ne porte-t-il pas simplement ce changement à master afin que dans master le fichier contienne "hello world" et "et nous sommes de retour"
Xufeng
Dans le premier exemple, le maître n'a engagé que «hello world». l'expérience a engagé «bonjour le monde \ n au revoir». Pour que le changement de branche ait lieu, le fichier.txt doit être modifié, le problème est qu'il y a des changements non validés "bonjour le monde \ bonjour le monde \ n et nous sommes de retour".
Gordolio
1

La bonne réponse est

git checkout -m origin/master

Il fusionne les modifications de la branche principale d'origine avec vos modifications locales, même non validées.

JD1731
la source
0

Si vous ne souhaitez pas que ces modifications soient validées, faites-le git reset --hard.

Ensuite, vous pouvez passer à la branche souhaitée, mais n'oubliez pas que les modifications non validées seront perdues.

Kacpero
la source