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

Salut j'ai besoin de fusionner deux branches comme celle-ci.

c'est juste un exemple de ce qui se passe, je travaille avec des centaines de fichiers qui ont besoin de 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 le contenu correct juste de la branche "notre" Et quand je le sauvegarde, le fichier disparaît de la liste non fusionnée. Mais depuis que j'ai des centaines de fichiers comme cela, ce n'est pas une option.

j'ai pensé que cette approche Amenez - moi où je veux être-dites facilement quel fichier à partir de quelle branche je veux garder.

mais je suppose que j'ai mal compris le concept des commandes git checkout --ours/theirs 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

13
demandé sur Charlestone 2016-09-11 19:19:41

1 réponses

c'est surtout une bizarrerie de la façon dont git checkout fonctionne à l'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 aussi git add les mêmes chemins:

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

mais ceci 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 la manière la plus rapide est d'utiliser la commande du milieu. (Soit dit en passant ,le -- ici est de s'assurer que Git peut distinguer entre un nom de chemin et un nom d'option ou de branche. Par exemple , si vous avez un fichier nommé --theirs , il ressemblera à une option, mais -- dira à Git que non, c'est vraiment un nom de chemin.)

pour voir comment tout cela fonctionne à l'interne, et pourquoi vous avez besoin du git add séparé sauf quand vous ne le faites pas, lire la suite. :-) Tout d'abord, faisons un rapide examen du processus de fusion.

Fusionner, partie 1: comment fusionner commence

quand vous courez:

$ git merge commit-or-branch

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

ayant trouvé une base de fusion appropriée, 1 Git produit alors deux git diff listings: un de la base de fusion à HEAD , et un de la base de fusion à le commit que vous avez identifié. Cela donne "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 ils ne l'ont pas fait, Git peut simplement prendre votre version.

pour les fichiers où ils ont fait un changement et que vous n'avez pas fait, Git peut simplement prendre leur version.

pour les fichiers où vous avez tous les deux fait des changements, Git doit faire un vrai travail de fusion. Il compare les changements, ligne par ligne, pour voir si elle pouvez les combiner. Si elle peut les combiner, il le fait. Si les fusions semblent-basées, encore une fois, sur des comparaisons purement ligne par ligne-à conflit, Git déclare un "conflit de fusion" pour ce fichier (et va de l'avant et tente 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-parce qu'il n'y a pas eu de conflits-ou s'arrête avec un conflit de fusion.


1 la base de fusion est évidente si vous dessinez le graphe de propagation. Sans dessiner le graphe, c'est un peu mystérieux. C'est pourquoi je dis toujours aux gens de dessiner le graphe, ou au moins, autant que nécessaire pour faire sens.

la définition technique est que la base de fusion est le noeud" Lower common ancêtre " (LCA) dans le graphe de propagation. En termes moins techniques, c'est le plus récent commit où votre branche actuelle rejoint la direction générale, vous êtes à la fusion. C'est-à-dire, en enregistrant les identifiants de propagation de chaque Fusion, Git est capable de trouver la dernière fois où les deux branches étaient ensemble, et donc de comprendre à la fois ce que vous avez fait, et ce qu'ils ont fait. Pour que cela fonctionne, Git doit enregistrer chaque Fusion. Plus précisément, il doit écrire les deux (ou la totalité, pour les fusions dites "octopus") parent IDs dans le nouveau commit de fusion.

dans certains cas, il y a plus d'un base de fusion appropriée. Le processus dépend alors de votre stratégie de fusion. La stratégie par défaut recursive fusionnera les multiples bases de fusion pour produire une"base de fusion virtuelle". C'est assez rare que vous pouvez l'ignorer pour l'instant.


Fusionner, partie 2: l'arrêt d'un conflit, et Git est "index",

quand Git s'arrête de cette façon, il doit vous donner une chance de résoudre les conflits. Mais cela signifie aussi qu'il a besoin de enregistrer les conflits, et c'est là que "l'index"de Git-aussi appelé "la zone de mise en scène", et parfois "la cache"-gagne vraiment son existence.

pour chaque fichier de mise en scène dans votre arbre de travail, l'index a jusqu'à quatre entrées, plutôt que seulement une entrée. Au plus trois d'entre eux sont effectivement en usage, mais il y a quatre emplacements, qui sont numérotés, 0 à 3 .

Logement zéro est utilisé pour les fichiers traités. Lorsque vous travaillez avec Git et ne pas faire des fusions, seulement slot zero est utilisé. Lorsque vous éditez un fichier dans l'arbre de travail, il a des "changements non marqués", puis vous git add le fichier et les changements sont écrits dans le dépôt, en mettant à jour le slot zéro; vos changements sont maintenant "échelonnés".

Les fentes

1-3 sont utilisées pour les fichiers non résolus. Lorsque git merge doit s'arrêter avec un conflit de fusion, il laisse le slot zéro vide, et écrit tout aux slots 1, 2, et 3. La version merge base du fichier est enregistrée dans la fente 1, la version --ours est enregistrée dans la fente 2, et la version --theirs est enregistrée dans la fente 3. Ces entrées non nulles sont la façon dont Git sait que le fichier n'est pas résolu. 2

Comme vous résoudre les fichiers, vous git add , qui efface toutes les entrées 1 à 3 et écrit une entrée slot-zero, staged-for-commit. C'est ainsi que Git sait que le fichier est résolu et prêt pour une nouvelle propagation. (Ou, dans certains cas, vous git rm le fichier, dans lequel cas Git écrit une valeur spéciale "enlevé" à la fente zéro, effaçant de nouveau les fentes 1-3.)


2 il 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é à la fois dans la nôtre et la leur. Puis :1:new est laissé vide et :2:new et :3:new enregistrent le conflit add/add. Ou, supposons que le fichier f existe dans la base, est modifié dans notre branche principale, et est enlevé 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 Modifier / Supprimer conflit.

pour modifier / modifier les conflits, les trois fentes sont occupées; seulement lorsqu'un fichier est manquant, l'une de ces fentes est vide. Il est logiquement impossible d'avoir deux slots vides: il n'y a pas de conflit supprimer/supprimer, ni de conflit 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! En tout cas, c'est l'existence même d'une certaine valeur(s) dans les cases 1, 2 et/ou 3 qui indiquent que le dossier n'est pas résolu.


Fusionner, partie 3: finition de la fusion

une fois tous les dossiers sont résolus-toutes les entrées sont seulement dans les fentes zéro numérotées-vous pouvez git commit le résultat de la fusion. Si git merge est capable de faire la fusion sans assistance, il exécute normalement git commit pour vous, mais la propagation réelle se fait toujours en exécutant git commit .

la commande commit fonctionne de la même manière qu'elle le fait toujours: elle transforme le contenu de l'index en tree objects et écrit un nouveau commit. La seule chose spéciale à propos d'une commit de fusion est qu'elle a plus d'un ID de commit parent. 3 L'extra parents viennent à partir d'un fichier git merge laisse derrière lui. Le message de fusion par défaut vient é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 de la nouvelle propagation est déterminé par le contenu de l'index. De plus, une fois la nouvelle propagation terminée, l'index est encore complet : il contient toujours le même contenu. Par défaut, git commit ne fera pas d'autre nouvelle propagation à ce point car il voit que l'index correspond à la propagation HEAD . Il appelle cela "vide" et nécessite --allow-empty pour faire une commit supplémentaire, mais l'index n'est pas vide . Il est encore tout à fait plein-il est juste plein de la même chose que le HEAD commit.


3 cela suppose que vous faites une vraie Fusion, pas une fusion de squash. Lors de la réalisation d'une fusion squash, git merge délibérément n'écrit pas l'ID parent supplémentaire dans le fichier supplémentaire, de sorte que le nouveau commit de fusion a seulement un parent célibataire. (Pour une raison quelconque, git merge --squash supprime également la propagation automatique, comme si elle incluait aussi le drapeau --no-commit . Il n'est pas clair pourquoi, puisque vous pourrait juste exécuter git merge --squash --no-commit si vous voulez la propagation automatique supprimée.)

une fusion de squash n'enregistre pas son(ses) autre (s) parent (s). Cela signifie que si nous allons à la fusion à nouveau , quelque temps plus tard, Git ne saura pas où démarrer les diffs de . Cela signifie que vous ne devez généralement que squash-fusionner si vous prévoyez d'abandonner l'autre branche. (Il y a des façons délicates de combiner les fusions squash et les fusions réelles, mais elles dépassent largement la portée de cette réponse.)


comment git checkout branch utilise l'index

avec tout cela hors du chemin, nous devons ensuite regarder comment git checkout utilise l'index de Git, aussi. Rappelez-vous, dans l'usage normal, seul le zéro de fente est occupé, et l'index a une entrée pour chaque dossier échelonné. De plus, cette entrée correspond au fichier ( HEAD ) commit sauf si vous avez modifié le fichier et git add -ed le résultat. Il y a aussi qui correspond au fichier dans l'arbre de travail à moins que vous n'ayez modifié le fichier. 4

si vous êtes sur une branche et vous git checkout certains autre branche, Git tente de passer à l'autre branche. Pour que cela réussisse, Git a à remplacer l'entrée d'index pour chaque fichier avec l'entrée qui va avec l'autre branche.

disons, juste pour être concret, que vous êtes sur master et vous faites git checkout branch . Git comparera chaque entrée courante de l'index avec l'entrée de l'index qu'elle devrait être sur la propagation de la branche branch . C'est-à-dire, pour le dossier README.txt , les master contenus sont-ils les mêmes que ceux de branch , ou sont-ils différents?

si le contenu est le même , Git peut prendre facile et juste passer au fichier suivant. Si le contenu est différent , Git doit faire quelque chose à l'entrée d'index. (C'est autour de ce point que git vérifie pour voir si le fichier work-tree diffère aussi de l'Entrée index.)

en particulier, dans le cas où branch 'S fichier diffère de master 's, git checkout doit à remplacer l'entrée d'index avec la version de branch -ou, si README.txt n'existe pas existe dans le tip commit de branch , Git doit remove the index entry. En outre, si git checkout va modifier ou supprimer l'entrée d'index, il doit également modifier ou supprimer le travail-fichier de l'arborescence. Git s'assure que c'est une chose sûre à faire, c'est-à-dire que le fichier work-tree correspond au fichier de propagation master , avant qu'il ne vous permette de changer de branche.

en d'autres termes, c'est comment (et pourquoi) Git découvre S'il est correct de changer les branches-si vous avez des modifications qui seraient assommées en passant de master à branch . Si vous avez des modifications dans votre arbre de travail, mais les fichiers modifiés sont le même dans les deux branches, Git peut simplement laisser les modifications dans l'index et l'arbre de travail. Il peut vous alerter et vous avertira de ces fichiers modifiés "reportés" dans la nouvelle branche: facile, puisqu'il a dû vérifier pour cela de toute façon.

une fois que tous les tests ont passé et Git a décidé qu'il est correct de passer de master à branch - ou si vous avez spécifié --force - git checkout actualise l'index avec tous les changements (ou supprimé) fichiers, et met à jour l'arbre de travail pour correspondre.

notez que toute cette action a utilisé le slot zéro. Il n'y a aucune fente 1-3 entrées du tout, de sorte que git checkout n'a pas à enlever de telles choses. Vous n'êtes pas au milieu d'une fusion conflictuelle, et vous avez lancé git checkout branch pour non seulement vérifier un fichier , mais plutôt un ensemble de fichiers et de branches d'interrupteur.

Note également que vous pouvez, au lieu de vérifier une branche, découvrez un spécifique de commettre. Par exemple, voici comment vous pouvez regarder une propagation précédente:

$ 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 vérifier une branche, sauf qu'au lieu d'utiliser le tip commit de la branche, Git vérifie une commit arbitraire. Au lieu d'être maintenant "sur" la nouvelle branche, vous êtes maintenant sur no 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.


4 L'entrée de l'index peut ne pas correspondre à la version de l'arbre de travail si Git fait des modifications spéciales de fin de CR-LF, ou l'application de filtres smudge. Ça devient assez avancé et la meilleure chose est d'ignorer cette affaire pour l'instant. :- )

5 Plus précisément, cela vous place sur une branche anonyme (sans nom) qui va se développer à partir de la propagation actuelle. Vous resterez en mode tête détachée si vous faites de nouvelles propagations, et dès que vous git checkout une autre commit ou branche, vous y basculerez et Git" abandonnera " les propagations que vous avez faites. Le point de ce mode de tête détachée est à la fois pour vous permettre de regarder autour de et pour vous permettre de faire de nouvelles commits que sera partez si vous ne prenez pas des mesures spéciales pour les sauver. Pour toute personne relativement nouvelle à Git, cependant, ayant commits " just go away "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 indiquera si vous êtes en mode tête détachée. L'utiliser souvent. 6 Si votre Git est vieux (l'OP est 1.7.1, ce 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.

6 certains programmeurs aiment avoir des informations clés git status encodées dans chaque invite de commande. Personnellement, je ne vais pas aussi loin, mais ça peut être une bonne idée.


vérifier des fichiers spécifiques, et pourquoi il résout parfois des conflits de fusion

la commande git checkout a d'autres modes de fonctionnement, bien. En particulier, vous pouvez exécuter git checkout [flags etc] -- path [path ...] pour voir les fichiers. C'est là que les choses deviennent étranges. Notez que lorsque vous utilisez cette forme de la commande, Git ne vérifier pour s'assurer que vous ne écrasez pas vos fichiers. 7

maintenant, au lieu de changer les branches, vous dites à Git d'obtenir un fichier particulier à partir de quelque part, et les déposer dans l'arbre de travail, l'écrasement de ce qui est là, si quoi que ce soit. La question délicate est: où git obtient-il ces fichiers?

en général, il y a trois endroits que Git garde des fichiers:

  • s'engage; 8
  • dans l'index;
  • et dans l'arbre de travail.

la commande checkout peut se lire de l'un ou l'autre des deux premiers endroits, et écrit toujours le résultat à l'arbre de travail.

quand git checkout reçoit un fichier d'un commit, il le Copie d'abord à l'index . Chaque fois qu'il fait cela, il écrit le fichier de logement zéro. L'écriture au slot zéro efface les fentes 1-3, si elles sont occupées. Lorsque git checkout reçoit un fichier de l'index, il n'a pas à le copier dans l'index. (Bien sûr que non: elle est déjà là!) C'est ainsi que git checkout fonctionne lorsque vous êtes pas au milieu d'une fusion: vous pouvez git checkout -- path/to/file pour obtenir l'index de la version dos. 9

supposons, cependant, que vous êtes au milieu d'une fusion conflictuelle et vont à git checkout quelque chemin, peut-être avec --ours . (Si vous n'êtes pas au milieu d'une fusion, il n'y a rien dans les fentes 1-3, et --ours n'a aucun sens.) Si vous exécutez 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 c'est déjà dans l'index, Git n'écrit pas à l'index, juste à l'arbre de travail. Donc le fichier est et non résolu!

la même chose vaut pour git checkout --theirs : il obtient le fichier de l'index (slot 3), et ne résout rien.

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

puisque, lors d'une fusion conflictuelle, Git enregistre L'ID de commit en cours de fusion dans MERGE_HEAD , vous pouvez aussi git checkout MERGE_HEAD -- path/to/file pour obtenir le fichier de l'autre commit. Cela, aussi, des extraits de commit, donc il écrit à l'index, résolvant le dossier.


7 j'aimerais souvent que Git utilise une commande front-end différente pour cela, puisque nous pourrions alors dire, sans équivoque, que git checkout est sûr, qu'il ne va pas écraser les fichiers sans --force . Mais ce genre de git checkout "ne overwrite des fichiers, sur le but!

8 C'est un un peu de mensonge, ou au moins un stretch: commits ne contient pas de fichiers directement. Au lieu de cela, s'engage contenir une (seule) pointeur vers un arbre objet. Cet objet tree contient les IDs des objets tree supplémentaires et des objets blob . Les objets blob contiennent contenu du fichier.

il en va de même pour l'indice. Chaque fente d'index contient, pas le fichier réel contenus , mais plutôt les ID de hachage des objets blob dans le dépôt.

pour nos besoins, cependant, cela n'a pas vraiment d'importance: nous demandons juste à Git de récupérer commit:path et il trouve les arbres et la tache pour nous. Ou, nous demandons à Git de récupérer :n:path et il trouve le blob ID dans l'entrée d'index pour path pour slot n . Puis ça nous donne le contenu du fichier, et on peut y aller.

cette syntaxe de deux points fonctionne partout dans Git, tandis que les drapeaux --ours et --theirs ne fonctionnent que dans git checkout . La syntaxe drôle de colon est décrite dans gitrevisions .

9 le cas d'utilisation de git checkout -- path est le suivant: supposons que, que vous fusionniez ou non, vous apportiez des modifications à un fichier, testé, trouvé ces changements ont fonctionné, puis git add a été versé au dossier. Puis vous avez décidé de faire plus de changements, mais vous n'avez pas encore lancé git add . Vous testez la deuxième série de changements et constatez qu'ils sont erronés. Si seulement vous pouviez obtenir la version de l'arbre de travail du fichier retour à la version que vous git add -ed il ya juste un instant.... Aha, vous peut : vous git checkout -- path et git copie la version d'index, de la fente zéro, de retour à l'arbre 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" extrait de l'index et donc ne résout pas". Supposons que, dans notre Fusion conflictuelle, Git ait détecté qu'un fichier était renommé . C'est-à-dire, dans la base de Fusion, nous avions le fichier doc.txt , mais maintenant dans HEAD nous avons Documentation/doc.txt . Le chemin que nous avons besoin pour git checkout --ours est Documentation/doc.txt . C'est aussi le chemin dans le HEAD commit, donc C'est OK pour git checkout HEAD -- Documentation/doc.txt .

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

parce que la fusion utilise les noms de HEAD , il est généralement assez sûr de git checkout HEAD -- path pour extraire-et-résoudre en une seule étape. Et si vous travaillez sur la résolution de fichiers et que vous avez lancé git status , vous devez savoir s'ils ont un fichier renommé, et donc s'il est sûr de git checkout MERGE_HEAD -- path pour extraire-et-résoudre en une seule étape en rejetant vos propres modifications. Mais vous devez être conscient de cela, et savoir ce qu'il faut faire s'il y a est un renommé à être concerné.


10 je dis "devrait" ici, pas "peut", parce que Git actuellement oublie le renommer un peu trop tôt. Donc, si vous utilisez --theirs pour obtenir un fichier que vous avez renommé dans HEAD , vous devez utiliser l'ancien nom ici aussi, et ensuite renommer le fichier dans l'arbre de travail.

44
répondu torek 2018-03-18 19:42:19