web-dev-qa-db-fra.com

Git dit que la branche est fusionnée, mais les changements ne sont apparemment pas présents

Je me suis mis dans une situation qui n'a pas de sens pour moi. Je vais essayer de le décrire du mieux que je peux.

J'ai une branche de développement et j'ai fusionné master via git checkout develpment && git merge master. Je n'ai eu aucun conflit de fusion ici.

Il y a un commit spécifique qui m'intéresse, disons que c'est abcd123. Quand je lance git branch --contains abcd123, il indique que development et master contiennent abcd123. Quand je fais git log, ça montre abcd123 dans la liste des validations, à la fois sur development et sur master.

git show abcd123 montre que contient les modifications apportées à deux fichiers. Mais je n'arrive pas à trouver ces changements. Quand je regarde les fichiers, je ne vois pas ces changements, ni sur development ni sur master. Quand j'inspecte git log -- path/to/file1, Je ne vois pas abcd123, pareil pour git log -- path/to/file2.

Que se passe t-il ici? Comment le commit peut-il être présent, mais les changements ne sont apparemment pas là?

Il est possible que abcd123 a été initialement introduit dans une autre branche (autre que development) qui a été fusionnée dans master. Je ne sais pas si cela pourrait faire une différence.

Au fait, quand j'essaie git checkout master && git merge development (après avoir fusionné master dans development comme indiqué ci-dessus), je reçois beaucoup de conflits de fusion, y compris file1 et file2. Cela semble donc montrer que master n'a pas été réellement fusionné dans development - ne devrait pas git merge development réussir si git merge master a déjà été exécuté? Pour créer plus de confusion, git branch --merged development indique que master a été fusionné dans development. Je suppose que cela est compatible avec git merge master ....

Tout conseil à ce stade est très apprécié.

EDIT: À ce stade, il semble que le problème soit dû à une fusion qui a échoué ou a été gâché d'une manière ou d'une autre. Si quelqu'un continue de lire, je pense que la direction que prend la réponse de Torek semble la plus fructueuse.

11
Robert Dodier

Cette réponse est longue, car il se passe beaucoup de choses ici. Le résumé TL; DR, cependant, est probablement que vous voulez --full-history.

Il y a ici plusieurs problèmes distincts qui doivent être démêlés:

  • L'expression "afficher les modifications" ou ce que vous voyez dans git log -p ou git show, conduit souvent les gens sur la mauvaise voie pour interpréter ce que Git stocke.
  • Le git log la commande vous ment parfois (surtout autour des fusions), dans le seul but de ne pas vous submerger d'informations inutiles.
  • Quelle git merge ne peut être un peu délicat. C'est simple en principe, mais la plupart des gens ne l'obtiennent pas tout de suite.

Commits ordinaires

Examinons d'abord les commits ordinaires les plus courants de Git. Un commit, dans Git, est un snapshot (du contenu du fichier par nom de fichier). Vous faites un commit, puis vous changez quelques choses et git add un ou deux fichiers modifiés et faites un deuxième commit, et le second commit a tous les mêmes fichiers comme premier commit, à l'exception de ceux écrasés par le git add.

Cela vaut la peine de les dessiner en tant que parties du graphique commit commit de Git. Notez que chaque commit a son propre ID de hachage unique (une de ces chaînes impossibles à retenir comme 93aefc ou badf00d ou cafedad), plus l'ID d'un commit parent (ou précédent). Le hachage de validation parent permet à Git d'enchaîner ces choses ensemble, à l'envers:

... <-E <-F <-G ...

où chaque lettre majuscule remplace un ID de hachage, et les flèches indiquent que chaque validation "pointe" vers son parent. Normalement, nous n'avons pas besoin de dessiner les flèches internes (elles ne sont pas très intéressantes à la fin), je les dessine donc comme:

...--E--F--G   <-- master

Le nommaster, cependant, mérite toujours une flèche, car le commit vers lequel cette flèche pointe changera avec le temps.

Si nous choisissons un commit comme G et le visualisons sans en utilisant git log -p ou git show, nous verrons chaque fichier dans son intégralité, exactement comme il est stocké dans le commit. En fait, c'est ce qui se passe lorsque nous utilisons git checkout pour le vérifier: nous extrayons tous les fichiers dans leur intégralité, dans l'arborescence de travail, afin de pouvoir les voir et les travailler. Mais quand on le voit avec git log -p ou git show, Git ne nous montre pas tout; il nous montre seulement ce que changé.

Pour ce faire, Git extrait à la fois le commit et son commit parent, puis exécute un gros git diff sur la paire. Quelle que soit la différence entre le parent F et l'enfant G, c'est ce qui changé, c'est donc ce que git log -p ou git show vous montre.

Fusionner valide

C'est très bien pour les validations ordinaires, monoparentales, mais cela ne fonctionne pas pour les validations merge. Un commit de fusion est tout simplement un commit avec deux (ou plus, mais nous ne nous inquiéterons pas dans ce cas) des commits parents. Vous les obtenez en faisant un succès git merge, et nous pourrions dessiner comme ça. Nous commençons par les deux branches (qui partent d'un point de départ):

       H--I   <-- development (HEAD)
      /
...--E--F--G   <-- master

puis nous exécutons git merge master.1 Git essaie maintenant de combiner les deux branches. S'il réussit, il fait un nouveau commit qui a deux parents:

       H--I--J   <-- development (HEAD)
      /     /
...--E--F--G   <-- master

Le nom development pointe désormais vers le nouveau commit de fusion J. Les parenthèses (HEAD) indique ici qu'il s'agit de notre branche actuelle. Cela nous indique quel nom est déplacé: nous faisons un nouveau commit - y compris tout nouveau commit de fusion - et development est le nom de la branche qui change pour pointer vers le nouveau commit.

Si nous ne nous soucions pas de la façon dont le contenu (les différents fichiers validés) du commit de fusion sont déterminés, tout cela est assez simple. Le commit de fusion est comme n'importe quel autre commit: il a un instantané complet, un tas de fichiers avec du contenu. Nous vérifions le commit de fusion, et ces contenus arrivent dans notre arbre de travail comme d'habitude.

Mais quand nous allons à view le commit de fusion ... eh bien, Git diffère normalement un commit contre son parent. La fusion a deux parents, un pour chaque branche. Avec lequel Git doit-il se différencier, pour vous montrer les changements?

Ici, git log et git show adopter différentes approches. Lorsque vous affichez le commit avec git log, il affiche rien du tout par défaut. Il ne choisira pas I- vs -J, et il ne choisira pas non plus G- vs -J! Cela ne montre rien du tout, car git log -p.


1Dans certains workflows Git, la fusion de de master dans toute autre branche est déconseillée. Cela peut fonctionner, cependant, et puisque vous l'avez fait, exécutons-le.


Affichage des validations de fusion

Le git show commande fait quelque chose de différent et de mieux. Il fonctionne deuxgit diffs, un pour I- vs -J et un pour G- vs -J. Il essaie ensuite de combiner les deux différences, vous montrant seulement ce qui a changé dans les deux. Autrement dit, où J est différent de I mais pas d'une manière particulièrement intéressante, Git supprime la différence. Où J est différent de G mais pas d'une manière particulièrement intéressante, Git supprime également cette différence. C'est probablement le mode le plus utile, c'est donc ce que git show spectacles. C'est encore assez imparfait, mais rien de ce que vous pouvez faire ici n'est parfait à toutes fins.

Tu peux faire git log faites la même chose en ajoutant --cc à la git log -p options. Ou, vous pouvez changer la façon dont soitgit log ougit show montre un commit de fusion en utilisant -m (notez un tiret pour -m, deux pour --cc, au fait).

Le -m option indique à Git qu'à des fins de visualisation, il devrait diviser la fusion. Maintenant, Git compare I à J, pour vous montrer tout ce que vous avez apporté lors de la fusion de G. Ensuite, Git compare G à la version supplémentaire séparée de J, pour vous montrer tout ce que vous avez apporté lors de la fusion de I. Le diff résultant est généralement très grand mais (ou parce que) il vous montre tout.

Il existe d'autres façons d'essayer de trouver ce qui est arrivé à un fichier, mais nous devons attendre un moment avant d'accéder à votre:

git log -- path/to/file1

commander. Tout comme nous avons vu git log sauter fusionne, il peut sauter encore plus de choses ici (mais il existe des moyens d'empêcher Git de faire ça).

Réaliser des fusions: comment Git construit le contenu de la fusion

Regardons à nouveau ce graphique de pré-fusion:

       H--I   <-- development (HEAD)
      /
...--E--F--G   <-- master

Ici, il y a deux validations sur la branche development qui ne sont pas sur la branche master et deux validations sur master qui ne sont pas sur development. Valider E (avec tous les validations antérieures) est sur les deux branches. Commit E est spécial, cependant: c'est le le plus récent2 engager c'est sur les deux branches. Commit E est ce que Git appelle la base de fusion.

Pour effectuer une fusion, Git exécute efficacement deux git diff commandes:

git diff E I
git diff E G

Le premier produit un ensemble de modifications apportées à divers fichiers, qui sont "ce que nous avons fait sur la branche development". Il s'agit en fait de la somme de H et I s'ils sont traités comme des correctifs. La seconde produit un ensemble de modifications — probablement différent — de divers fichiers, "ce que nous avons fait sur master", et comme auparavant, c'est en fait la somme de F et G comme patchs.

Git essaie alors de combiner ces deux différences. Tout ce qui est complètement indépendant entre eux, il prend les deux ensembles de modifications, les applique au contenu de commit E, et l'utilise comme résultat. Partout où les deux ensembles de modifications touchent la même ligne dans le fichier même, Git essaie de voir s'il ne peut prendre qu'une seule copie de cette modification. Si les deux corrigent l'orthographe d'un mot à la ligne 33 du fichier README, Git peut simplement prendre une copie du correctif d'orthographe. Mais partout où les deux ensembles de modifications touchent la même ligne du même fichier, mais apportent une modification différente, Git déclare un "conflit de fusion", jette ses mains métaphoriques en l'air et fait = vous corrigez le désordre résultant.

Si vous (ou quiconque fait la fusion) le souhaitez, ils peuvent empêcher Git de valider le résultat de la fusion même si Git pense que tout s'est bien passé: git merge --no-commit master arrête Git après avoir tout combiné. À ce stade, vous pouvez ouvrir des fichiers d'arborescence de travail dans votre éditeur, changer eux, les réécrire, git add le fichier modifié et git commit la fusion pour mettre quelque chose dans la fusion qui ne provenait pas de any des trois entrées (base et deux embouts).

Dans tous les cas, la clé pour comprendre tout cela est le concept du commit merge base. Si vous vous asseyez et dessinez le graphique de validation, la base de fusion est généralement assez évidente à moins que le graphique ne devienne complètement incontrôlable (ce qui arrive souvent, en fait). Vous pouvez également demander à Git de trouver la base de fusion pour vous - ou fusionner les bases, au pluriel, dans certains cas:

git merge-base --all master development

Cela imprime un ID de hachage. Dans notre cas hypothétique ici, ce serait l'ID de hachage de commit E. Une fois que vous avez cela, vous pouvez exécuter git diff manuellement, pour voir ce qui est arrivé à chaque fichier. Ou vous pouvez exécuter un fichier individuel git diff:

git diff E development -- path/to/file1
git diff E master -- path/to/file1

Notez que si vous remplacez les noms master et development par les ID de hachage des commits qui étaient actuels avant vous a fait un git merge, cela fonctionne même après la fusion. Cela vous dira ce que Git pensait qu'il devrait combiner pour path/to/file1. Cela, à son tour, vous dira si Git n'a pas vu le changement, ou si celui qui a fait la fusion overrode Git, ou a mal géré une fusion conflictuelle.

Une fois que vous avez effectué une fusion, une fusion ultérieure trouvera une base de fusion différente:

       H--I--J----K   <-- development
      /     /
...--E--F--G--L--M   <-- master

Nous examinons maintenant les deux astuces de branche et remontons l'historique (dans le sens gauche), en suivant les deux fourches d'une fusion comme J, pour trouver le premier commit auquel nous pouvons accéder depuis both conseils de branche. À partir de K, nous revenons à J, puis à la fois I et G. À partir de M, nous revenons à L, puis à G. Nous constatons que G est sur les deux branches, donc commit G est la nouvelle base de fusion. Git se déroulera:

git diff G K
git diff G M

pour obtenir les deux changements à appliquer au merge-base commit G.


2"Le plus récent" se réfère ici à la validation graphique ordre, plutôt qu'aux horodatages, bien qu'il s'agisse probablement aussi de la validation avec l'horodatage le plus récent qui se trouve sur les deux branches.


Git log et simplifications qui mentent, encore une fois

Nous avons déjà vu que git log -p saute juste les validations de fusion. Vous ne voyez pas du tout de différence, comme si la fusion était totalement magique. Mais lorsque vous exécutez:

git log -- path/to/file1

quelque chose d'autre, encore plus insidieux, se produit. Ceci est décrit, quoique de façon plutôt opaque, dans le (long) git log documentation dans la section intitulée Simplification de l'historique .

Dans notre exemple ci-dessus, supposons que git log marche de K vers l'arrière. Il trouve J, qui est une fusion. Il inspecte ensuite à la fois I et G, en les comparant à J après avoir exclu tout sauf le fichier que vous consultez. , il s'agit simplement de comparer path/to/file1 dans les trois commits.

Si un côté de la fusion ne le fait pas affiche tout changement à path/to/file1, cela signifie que le résultat de la fusion J n'était pas différent de l'entrée (I ou G). Git appelle cela "TREESAME". Si la fusion J, après avoir été supprimée dans ce fichier, correspond à I ou G de la même manière supprimée, alors J est TREESAME à I ou G (ou peut-être les deux). Dans ce cas, Git sélectionne le ou l'un des parents TREESAME et regarde seulement sur ce chemin. Disons qu'il choisit I (le long de la ligne du haut) plutôt que G (le bas).

Dans notre cas, cela signifie que si quelqu'un a laissé tomber le ballon pendant une fusion, il perd une modification qui devait entrer dans J de F, git log ne le montre jamais. La commande log regarde K, puis J, puis regarde mais supprime G, et ne regarde que I, puis H, puis E, puis tout commit antérieur. Il ne regarde jamais commit F du tout! Nous ne voyons donc pas le changement vers path/to/file1 de F.

La logique ici est simple; Je citerai le git log documentation directement mais ajoutez un peu d'emphase:

[Mode par défaut] Simplifie l'historique à l'historique le plus simple expliquant l'état final de l'arborescence. Plus simple car il taille certaines branches latérales si le résultat final est le même ...

Étant donné que les modifications dans F ont été supprimées, Git déclare qu'elles ne sont pas pertinentes. Vous n'avez pas besoin de les voir! Nous allons simplement ignorer entièrement ce côté de la fusion!

Vous pouvez le vaincre complètement avec --full-history. Cela indique à Git pas de tailler l'un ou l'autre côté d'une fusion: il devrait regarder les deux historiques. Cela trouvera commit F pour vous. Ajouter -m -p devrait également trouver les modifications ont été supprimées, car il trouvera tous les commits qui touchent le fichier:

git log -m -p --full-history -- path/to/file1

Si les modifications étaient là (dans commit F dans notre exemple) et ne le sont plus, il n'y a que deux façons de les perdre:

  • Ils ont été annulés (soit avec git revert, ou manuellement) dans un commit ordinaire. Vous verriez cela comme un commit qui touche path/to/file1 dans l'histoire même sans -m; -p vous montrera le diff.

  • Ou, ils ont été perdus en étant abandonnés lors d'une fusion. Vous verriez la fusion même sans -m, mais je ne sais pas avec certitude que celui qui a fait la fusion a laissé tomber la balle ici; mais -m -p affichera les deux différences parent, y compris celle que devrait avoir (mais n'a pas) pris la modification.

16
torek