web-dev-qa-db-fra.com

git checkout --ours ne supprime pas les fichiers de la liste des fichiers non fusionnés

Salut, je dois fusionner deux branches comme ça.

Ceci est juste un exemple de ce qui se passe, je travaille avec des centaines de fichiers qui nécessitent une résolution.

git merge branch1
...conflicts...
git status
....
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#   both added:   file1
#   both added:   file2
#   both added:   file3
#   both added:   file4
git checkout --ours file1
git chechout --theirs file2
git checkout --ours file3
git chechout --theirs file4
git commit -a -m "this should work"
U   file1
fatal: 'commit' is not possible because you have unmerged files.
Please, fix them up in the work tree, and then use 'git add/rm <file>' as
appropriate to mark resolution and make a commit, or use 'git commit -a'.

Quand je fais git merge tool, il y a juste le contenu de la branche 'ours' et quand je l'enregistre, le fichier disparaît de la liste non fusionnée. Mais comme j'ai des centaines de fichiers comme celui-ci, ce n'est pas une option.

Je pensais que cette approche m'apporterait où je veux être - dites facilement quel fichier de quelle branche je veux garder.

Mais je suppose que j'ai mal compris le concept de git checkout --ours/theirs commandes après une fusion.

Pourriez-vous s'il vous plaît me fournir quelques informations, comment gérer cette situation? J'utilise git 1.7.1

21
Charlestone

C'est surtout une bizarrerie de la façon dont git checkout Fonctionne en interne. Les gens de Git ont tendance à laisser l'implémentation dicter l'interface.

Le résultat final est qu'après git checkout Avec --ours Ou --theirs, Si vous voulez résoudre le conflit, vous devez également git add Les mêmes chemins:

git checkout --ours -- path/to/file
git add path/to/file

Mais c'est pas le cas avec d'autres formes de git checkout:

git checkout HEAD -- path/to/file

ou:

git checkout MERGE_HEAD -- path/to/file

(ceux-ci sont subtilement différents de plusieurs façons). Dans certains cas, cela signifie que le moyen le plus rapide consiste à utiliser la commande intermédiaire. (Par ailleurs, le -- Ici est pour s'assurer que Git peut faire la distinction entre un nom de chemin et un nom d'option ou de branche. Par exemple, si vous avez un fichier nommé --theirs, Il ressemblera à un , mais -- dira à Git que non, c'est vraiment un nom de chemin.)

Pour voir comment tout cela fonctionne en interne et pourquoi vous avez besoin du git add Séparé, sauf si vous ne le faites pas, lisez la suite. :-) Tout d'abord, examinons rapidement le processus de fusion.

Fusionner, partie 1: comment la fusion commence

Lorsque vous exécutez:

$ git merge commit-or-branch

la première chose que fait Git est de trouver la base de fusion entre la validation nommée et la validation actuelle (HEAD). (Notez que si vous fournissez un nom de branche ici, comme dans git merge otherbranch, Git le traduit en commit-ID, à savoir la pointe de la branche. Il enregistre l'argument du nom de la branche pour le message de journal de fusion éventuel, mais a besoin de l'ID de validation pour trouver la base de fusion.)

Ayant trouvé une base de fusion appropriée,1 Git produit ensuite deux listes git diff: Une de la base de fusion vers HEAD, et une de la base de fusion vers la validation que vous avez identifiée. Cela obtient "ce que vous avez changé" et "ce qu'ils ont changé", que Git doit maintenant combiner.

Pour les fichiers où vous avez fait un changement et qui ne l'ont pas été, Git peut simplement prendre votre version.

Pour les fichiers où ils ont fait un changement et vous ne l'avez pas fait, Git peut simplement prendre leur version.

Pour les fichiers où vous avez tous deux apporté des modifications, Git doit effectuer un vrai travail de fusion. Il compare les modifications, ligne par ligne, pour voir s'il peut les combiner. S'il peut les combiner, il le fait. Si les fusions semblent - basées, là encore, sur des comparaisons purement ligne par ligne - être en conflit, Git déclare un "conflit de fusion" pour ce fichier (et continue et essaie de fusionner de toute façon, mais laisse les marqueurs de conflit en place).

Une fois que Git a fusionné tout ce qu'il peut, il termine la fusion, car il n'y a pas eu de conflits, ou s'arrête avec un conflit de fusion.


1La base de fusion est évidente si vous dessinez le graphique de validation. Sans dessiner le graphique, c'est un peu mystérieux. C'est pourquoi je dis toujours aux gens de dessiner le graphique, ou du moins autant que nécessaire pour avoir du sens.

La définition technique est que la base de fusion est le nœud "LCA (ancêtre commun le plus bas)" du graphe de validation. En termes moins techniques, il s'agit du commit le plus récent où votre branche actuelle rejoint la branche que vous fusionnez. C'est-à-dire qu'en enregistrant les ID de validation parent de chaque fusion, Git est capable de trouver le dernier moment où les deux branches étaient ensemble, et donc de comprendre ce que vous avez fait et ce qu'elles ont fait. Pour que cela fonctionne, Git doit enregistrer chaque fusion. Plus précisément, il doit écrire les deux (ou tous, pour les fusionnements dits "octopus") dans le nouveau commit de fusion.

Dans certains cas, il existe plusieurs bases de fusion appropriées. Le processus dépend ensuite de votre stratégie de fusion. La stratégie par défaut récursive fusionnera les multiples bases de fusion pour produire une "base de fusion virtuelle". C'est assez rare pour que vous puissiez l'ignorer pour l'instant.


Merge, partie 2: arrêt avec un conflit et "index" de Git

Lorsque Git s'arrête de cette façon, il doit vous donner une chance de résoudre les conflits. Mais cela signifie également qu'il doit enregistrer les conflits, et c'est là que "l'index" de Git - également appelé "la zone de transit", et parfois "le cache" - gagne vraiment son existence.

Pour chaque fichier intermédiaire de votre arbre de travail, l'index a jusqu'à quatre entrées, plutôt qu'une seule entrée. Au plus, trois d'entre eux sont réellement en cours d'utilisation, mais il y a quatre emplacements, qui sont numérotés, 0 À 3.

L'emplacement zéro est utilisé pour les fichiers résolus. Lorsque vous travaillez avec Git et ne faites pas de fusion, seulement le slot zéro est utilisé. Lorsque vous modifiez un fichier dans l'arborescence de travail, il comporte des "modifications non planifiées", puis vous git add Le fichier et les modifications sont écrits dans le référentiel, mettant à jour l'emplacement zéro; vos modifications sont désormais "échelonnées".

Les emplacements 1 à 3 sont utilisés pour les fichiers non résolus. Lorsque git merge Doit s'arrêter avec un conflit de fusion, il laisse l'emplacement zéro vide et écrit tout dans les emplacements 1, 2 et 3. La version base de fusion du fichier est enregistrée dans l'emplacement 1, la version --ours est enregistrée dans l'emplacement 2 et la version --theirs est enregistrée dans l'emplacement 3. Ces entrées d'emplacement non nulles permettent à Git de savoir que le fichier n'est pas résolu.2

Lorsque vous résolvez des fichiers, vous les git add, Ce qui efface toutes les entrées des emplacements 1 à 3 et écrit une entrée à emplacement zéro, par étapes pour la validation. C'est ainsi que Git sait que le fichier est résolu et prêt pour un nouveau commit. (Ou, dans certains cas, vous git rm Le fichier, auquel cas Git écrit une valeur spéciale "supprimée" dans l'emplacement zéro, effaçant à nouveau les emplacements 1-3.)


2Il y a quelques cas où l'un de ces trois emplacements est également vide. Supposons que le fichier new n'existe pas dans la base de fusion et est ajouté dans le nôtre et le leur. Ensuite, :1:new Est laissé vide et :2:new Et :3:new Enregistrent le conflit d'ajout/ajout. Ou, supposons que le fichier f existe dans la base, est modifié dans notre branche HEAD, et est supprimé dans leur branche. Puis :1:f Enregistre le fichier de base, :2:f Enregistre notre version du fichier et :3:f Est vide, enregistrant le conflit de modification/suppression.

Pour les conflits de modification/modification, les trois emplacements sont occupés; ce n'est que lorsqu'un fichier est manquant que l'un de ces emplacements est vide. Il est logiquement impossible d'avoir deux emplacements vides: il n'y a pas de conflit de suppression/suppression, ni de conflit de nocreate/add. Mais il y a une certaine bizarrerie avec les conflits renommer, que j'ai omis ici car cette réponse est assez longue! Dans tous les cas, c'est l'existence même de certaines valeurs dans les emplacements 1, 2 et/ou 3 qui marquent le fichier comme non résolu.


Fusionner, partie 3: terminer la fusion

Une fois que tous les fichiers sont résolus (toutes les entrées se trouvent uniquement dans les emplacements numérotés zéro), vous pouvez git commit Le résultat de la fusion. Si git merge Peut effectuer la fusion sans assistance, il exécute normalement git commit Pour vous, mais la validation réelle se fait toujours en exécutant git commit.

La commande commit fonctionne de la même manière que d'habitude: elle transforme le contenu de l'index en objets - arbre et écrit un nouveau commit. La seule chose spéciale à propos d'un commit de fusion est qu'il a plus d'un ID de commit parent.3 Les parents supplémentaires proviennent d'un fichier que git merge Laisse derrière. Le message de fusion par défaut provient également d'un fichier (un fichier séparé dans la pratique, bien qu'en principe ils auraient pu être combinés).

Notez que dans tous les cas, le contenu du nouveau commit est déterminé par le contenu de l'index. De plus, une fois le nouveau commit effectué, l'index est toujours plein: il contient toujours le même contenu. Par défaut, git commit Ne fera pas de nouveau commit à ce stade car il voit que l'index correspond au HEAD commit. Il appelle cela "vide" et nécessite que --allow-empty Fasse un commit supplémentaire, mais l'index n'est pas vide du tout. Il est encore assez plein - il est juste plein de même chose comme le commit HEAD.


3Cela suppose que vous effectuez une véritable fusion, pas une fusion de squash. Lors d'une fusion de squash, git merge Délibérément ne le fait pas écrit l'ID parent supplémentaire dans le fichier supplémentaire, de sorte que la nouvelle validation de fusion n'ait qu'un seul parent. (Pour une raison quelconque, git merge --squash Supprime également la validation automatique, comme s'il incluait également le drapeau --no-commit. On ne sait pas pourquoi, puisque vous pourriez juste exécutez git merge --squash --no-commit si vous voulez la validation automatique est supprimée.)

Une fusion de squash n'enregistre pas ses autres parents. Cela signifie que si nous allons fusionner encore, quelque temps plus tard, Git ne saura pas d'où commencer les différences. Cela signifie que vous ne devriez généralement fusionner que si vous prévoyez d'abandonner l'autre branche. (Il existe des moyens délicats de combiner les fusions de squash et les fusions réelles, mais elles sortent bien du cadre de cette réponse.)


Comment git checkout branch Utilise l'index

Avec tout cela à l'écart, nous devons ensuite voir comment git checkout Utilise également l'index de Git. N'oubliez pas qu'en utilisation normale, seul le logement zéro est occupé et l'index a une entrée pour chaque fichier intermédiaire. De plus, cette entrée correspond au commit actuel (HEAD) sauf si vous avez modifié le fichier et git add - édité le résultat. Il correspond également au fichier dans l'arbre de travail sauf si vous avez modifié le fichier.4

Si vous êtes sur une branche et que vous git checkout Une branche autre, Git essaie de passer à l'autre branche. Pour que cela réussisse, Git doit remplacer l'entrée d'index pour chaque fichier par l'entrée qui va avec l'autre branche.

Disons, juste pour le concret, que vous êtes sur master et que vous faites git checkout branch. Git comparera chaque entrée d'index en cours avec l'entrée d'index dont elle devrait être sur le commit le plus à la pointe de la branche branch. Autrement dit, pour le fichier README.txt, Le contenu de master le même que celui de branch, ou sont-ils différents?

Si le contenu est le même, Git peut se détendre et passer au fichier suivant. Si le contenu est différent, Git doit faire quelque chose pour l'entrée d'index. (C'est autour de ce point que Git vérifie si le fichier d'arbre de travail diffère également de l'entrée d'index.)

Plus précisément, dans le cas où le fichier de branch diffère de celui de master, git checkout Doit remplacer l'entrée d'index avec la version de branch— ou, si README.txt n'existe pas existe dans le commit de astuce de branch, Git doit supprimer = l'entrée d'index. De plus, si git checkout Va modifier ou supprimer l'entrée d'index, il doit également modifier ou supprimer le fichier d'arborescence de travail. Git s'assure que c'est une chose sûre à faire, c'est-à-dire que le fichier de l'arbre de travail correspond au fichier de commit master, avant de vous permettre de changer de branche.

En d'autres termes, c'est comment (et pourquoi) Git découvre s'il est correct de changer de branche - si vous avez des modifications qui seraient clobber en passant de master à branch. Si vous avez des modifications dans votre arbre de travail, mais les fichiers modifiés sont les mêmes dans les deux branches, Git peut il suffit de laisser les modifications dans l'index et l'arbre de travail. Il peut et vous alertera sur ces fichiers modifiés "reportés" dans la nouvelle branche: facile, car il devait quand même le vérifier.

Une fois tous les tests réussis et Git a décidé qu'il est OK de passer de master à branch— ou si vous avez spécifié --force - git checkout Met effectivement à jour le index avec tous les fichiers modifiés (ou supprimés) et met à jour l'arborescence de travail pour correspondre.

Notez que toute cette action a utilisé l'emplacement zéro. Il n'y a pas du tout d'entrées 1-3, de sorte que git checkout N'a pas à supprimer de telles choses. Vous n'êtes pas au milieu d'une fusion conflictuelle, et vous avez exécuté git checkout branch Pour non seulement extraire un fichier, mais plutôt un ensemble de fichiers et de branches de commutation.

Notez également que vous pouvez, au lieu d'extraire une branche, extraire un commit spécifique. Par exemple, voici comment vous pourriez regarder un commit précédent:

$ git log
... peruse log output ...
$ git checkout f17c393 # let's see what's in this commit

L'action ici est la même que pour extraire une branche, sauf qu'au lieu d'utiliser la validation tip de la branche, Git extrait une validation arbitraire. Au lieu d'être maintenant "sur" la nouvelle branche, vous êtes maintenant sur non branche:5 Git vous donne une "TÊTE détachée". Pour rattacher votre tête, vous devez git checkout master Ou git checkout branch Pour revenir "sur" la branche.


4L'entrée d'index peut ne pas correspondre à la version de l'arbre de travail si Git effectue des modifications de fin CR-LF spéciales ou applique des filtres de maculage. Cela devient assez avancé et la meilleure chose est d'ignorer ce cas pour l'instant. :-)

5Plus précisément, cela vous place sur une branche anonyme (sans nom) qui se développera à partir de la validation actuelle. Vous resterez en mode détaché HEAD si vous faites de nouveaux commits, et dès que vous git checkout Un autre commit ou branche, vous y basculerez et Git "abandonnera" les commits que vous avez faits. Le point de ce HEAD détaché est à la fois pour vous permettre de regarder autour de vous et pour vous permettre de faire de nouveaux commits qui Will allez-vous-en si vous ne prenez pas de mesures spéciales pour les sauver. Cependant, pour quiconque est relativement nouveau dans Git, avoir des validations "juste s'en aller" n'est pas si bon - alors assurez-vous de savoir que vous êtes dans ce mode "tête détachée", chaque fois que vous y êtes.

La commande git status Vous dira si vous êtes en mode détaché HEAD. Utilisez-le souvent.6 Si votre Git est vieux (l'OP est 1.7.1, qui est très vieux maintenant), git status N'est pas aussi utile que dans les versions modernes de Git, mais c'est toujours mieux que rien.

6Certains programmeurs aiment que les informations clés git status Soient encodées dans chaque invite de commande. Personnellement, je ne vais pas aussi loin, mais cela peut être une bonne idée.


Extraire des fichiers spécifiques et pourquoi cela résout parfois les conflits de fusion

La commande git checkout A cependant d'autres modes de fonctionnement. En particulier, vous pouvez exécuter git checkout [flags etc] -- path [path ...] Pour extraire des fichiers spécifiques. C'est là que les choses deviennent étranges. Notez que lorsque vous utilisez la forme de la commande ceci, Git ne fonctionne pas vérifiez que vous n'écrasez pas vos fichiers.sept

Maintenant, au lieu de changer de branche, vous dites à Git de récupérer des fichiers particuliers quelque part, et de les déposer dans l'arbre de travail, en écrasant tout ce qui s'y trouve, le cas échéant. La question délicate est: où Git obtient-il ces fichiers?

De manière générale, Git conserve trois fichiers:

  • en commits;8
  • dans l'index;
  • et dans l'arbre de travail.

La commande d'extraction peut lire à partir de l'un des deux premiers endroits et écrit toujours le résultat dans l'arbre de travail.

Lorsque git checkout Obtient un fichier à partir d'une validation, il commence par le copie dans l'index. Chaque fois qu'il le fait, il écrit le fichier dans l'emplacement zéro. L'écriture dans l'emplacement zéro efface les emplacements 1 à 3, s'ils sont occupés. Lorsque git checkout Obtient un fichier de l'index, il n'a pas à le copier dans l'index. (Bien sûr que non: il est déjà là!) Voici comment fonctionne git checkout Lorsque vous êtes pas au milieu d'une fusion: vous pouvez git checkout -- path/to/file Pour obtenir la version d'index en arrière.9

Supposons, cependant, que vous êtes au milieu d'une fusion en conflit et que vous allez vers git checkout Un chemin, peut-être avec --ours. (Si vous n'êtes pas au milieu d'une fusion, il n'y a rien dans les emplacements 1-3 et --ours N'a aucun sens.) Vous exécutez donc git checkout --ours -- path/to/file.

Ce git checkout Obtient le fichier de l'index - dans ce cas, de la fente d'index 2. Comme il est déjà dans l'index, Git n'écrit pas à l'index, juste pour l'arbre de travail. Le fichier est donc pas résolu!

Il en va de même pour git checkout --theirs: Il obtient le fichier de l'index (emplacement 3) et ne résout rien.

Mais : si vous git checkout HEAD -- path/to/file, Vous dites à git checkout D'extraire du HEAD commit. Comme il s'agit d'un commit, Git commence par écrire le contenu du fichier dans l'index. Cela écrit l'emplacement 0 et efface 1-3. Et maintenant, le fichier est résolu!

Étant donné que, pendant une fusion conflictuelle, Git enregistre l'ID du commit fusionné dans MERGE_HEAD, Vous pouvez également git checkout MERGE_HEAD -- path/to/file Pour obtenir le fichier de l'autre commit. Cela extrait également une validation, il écrit donc dans l'index et résout le fichier.


septJe souhaite souvent que Git utilise une commande frontale différente pour cela, car nous pourrions alors dire, sans équivoque, que git checkout est sûr, qu'il n'écrasera pas les fichiers sans --force. Mais ce type de git checkout le fait écrase les fichiers, exprès!

8C'est un peu un mensonge, ou du moins un étirement: les commits ne contiennent pas de fichiers directement. Au lieu de cela, les validations contiennent un pointeur (unique) vers un objet arbre. Cet objet d'arborescence contient les ID d'objets d'arborescence supplémentaires et d'objets blob. Les objets blob contiennent le contenu réel du fichier.

Il en va de même pour l'indice. Chaque emplacement d'index contient non pas le fichier réel contenu, mais plutôt les ID de hachage des objets blob dans le référentiel.

Pour nos besoins, cependant, cela n'a pas vraiment d'importance: nous demandons simplement à Git de récupérer commit:path et il trouve les arbres et l'ID de blob pour nous. Ou, nous demandons à Git de récupérer :n:path et il trouve l'ID de blob dans l'entrée d'index pour path pour le slot n. Ensuite, il nous obtient le contenu du fichier, et nous sommes prêts à partir.

Cette syntaxe deux-points et nombre fonctionne partout dans Git, tandis que les drapeaux --ours Et --theirs Fonctionnent uniquement dans git checkout. La drôle de syntaxe du côlon est décrite dans gitrevisions .

9Le cas d'utilisation de git checkout -- path Est le suivant: supposez que, que vous fusionniez ou non, vous avez apporté des modifications à un fichier, testé, trouvé ces modifications fonctionnelles, puis exécuté git add Sur le fichier. Vous avez ensuite décidé d'apporter d'autres modifications, mais vous n'avez pas exécuté à nouveau git add. Vous testez le deuxième ensemble de modifications et constatez qu'elles sont erronées. Si seulement vous pouviez récupérer la version arborescente du fichier dans la version que vous git add - éditée il y a juste un instant .... Aha, vous pouvez: vous git checkout -- path Et Git copie la version d'index, depuis l'emplacement zéro, dans l'arborescence de travail.


Avertissement de comportement subtil

Notez cependant que l'utilisation de --ours Ou --theirs A une autre légère différence subtile en plus du comportement "extraire de l'index et donc ne pas résoudre". Supposons que, dans notre fusion conflictuelle, Git a détecté qu'un fichier a été renommé. Autrement dit, dans la base de fusion, nous avions le fichier doc.txt, Mais maintenant dans HEAD nous avons Documentation/doc.txt. Le chemin dont nous avons besoin pour git checkout --ours Est Documentation/doc.txt. C'est également le chemin dans la validation HEAD, donc c'est OK pour git checkout HEAD -- Documentation/doc.txt.

Mais que se passe-t-il si, dans le commit que nous fusionnons, doc.txt A pas renommé? Dans ce cas, nous devrionsdix être en mesure de git checkout --theirs -- Documentation/doc.txt pour obtenir leurdoc.txt de l'index. Mais si nous essayons de git checkout MERGE_HEAD -- Documentation/doc.txt, Git ne pourra pas trouver le fichier: il n'est pas dans Documentation, dans le commit MERGE_HEAD. Nous devons git checkout MERGE_HEAD -- doc.txt Pour obtenir leur fichier ... et ce serait pas résoudre Documentation/doc.txt. En fait, cela ne ferait que créer ./doc.txt (S'il était renommé, il n'y a presque certainement pas de ./doc.txt, Donc "créer" est une meilleure supposition que "écraser").

Étant donné que la fusion utilise les noms de HEAD, il est généralement suffisamment sûr pour git checkout HEAD -- path D'extraire et de résoudre en une seule étape. Et si vous travaillez sur la résolution de fichiers et que vous avez exécuté git status, Vous devez savoir s'ils ont un fichier renommé, et donc s'il est sûr de git checkout MERGE_HEAD -- path D'extraire et de résoudre en un seul étape en ignorant vos propres modifications. Mais vous devez toujours être conscient de cela et savoir quoi faire s'il y a c'est un changement de nom qui vous préoccupe.


dixJe dis "devrait" ici, pas "peut", car Git actuellement oublie le renommer un peu trop tôt. Donc, si vous utilisez --theirs Pour obtenir un fichier que vous avez renommé en HEAD, vous devez également utiliser l'ancien nom ici, puis renommer le fichier dans l'arborescence.

80
torek