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
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.
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.
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.
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.)
git checkout branch
Utilise l'indexAvec 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.
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:
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.
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.