Aller au contenu
English

Stratégies de fusion

  • Leçon 9
  • 40 min
  • Module 3 : Collaborer par la revue de code

Ce que vous saurez faire

Choisissez comment intégrer une demande de tirage (pull request) à main et sachez ce qui arrive à ses commits (enregistrements). Après cette leçon, vous pourrez expliquer l’historique conservé ou réécrit par un commit de fusion, un squash et un rebase.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le dossier ~/projects/nextia-learning-labs. Vous l’avez cloné dans Les étapes d’une demande de tirage : préparer le laboratoire d’exercice. S’il manque, faites l’étape 2 de cette section.

  • Réinitialisez le laboratoire pratique pour obtenir les mêmes résultats que dans cette leçon. Dans ~/projects, exécutez :

    Fenêtre de terminal
    sh nextia-learning-labs/C14/setup.sh m03 --reset
  • Allez ensuite dans ~/projects/c14-lab/ticket-notes. git status affiche On branch main (sur la branche main) et working tree clean (répertoire de travail propre). La branche add-faq, que vous fusionnerez de trois façons, se trouve dans votre clone. Le script l’a créée.

  • La fusion, le squash et le rebase de cette leçon créent de nouveaux commits. Vos nouvelles empreintes diffèrent de celles présentées ici. Les empreintes des commits de préparation, comme 6b53600, sont les mêmes.

La branche add-faq d’Amira ajoute un nouveau fichier, faq.txt, en deux commits. Elle a créé la branche à partir de e058b3f. Ensuite, Sam a ajouté un commit à main :

Terminal (Amira, ticket-notes)
git log --oneline --graph main add-faq
Sortie (premières lignes)
* 2e9e37c (HEAD -> main, origin/main, origin/HEAD) Add the on-call number
| * 6c3828f (origin/add-faq, add-faq) Add a network question to the FAQ
| * 6b53600 Add FAQ
|/
* e058b3f Add ticket 103

main et add-faq ont divergé : chacune possède un commit que l’autre n’a pas. Un service hébergé propose trois façons d’intégrer add-faq à main. Toutes donnent les mêmes fichiers. Elles donnent des historiques différents.

  • Commit de fusion. Git crée un nouveau commit avec deux parents : le dernier commit de main et le dernier commit de la branche. Les commits de la branche conservent leurs empreintes et font désormais partie de main. Vous avez fait cela avec git merge --no-ff dans la première leçon de ce module.
  • Squash. Un squash (regroupement de commits) combine tous les changements d’une branche en un nouveau commit sur la branche cible. Le nouveau commit a un seul parent. Les commits de la branche ne font pas partie de main.
  • Rebase. Un rebase (rebasage) copie les commits d’une branche sur un nouveau commit de départ, un à la fois. Chaque copie est un nouveau commit avec une nouvelle empreinte. Ensuite, main avance jusqu’à la dernière copie par avance rapide (fast-forward).
Quatre petits historiques. Départ : main contient e058b3f, puis 2e9e37c. add-faq part de e058b3f et contient 6b53600, puis 6c3828f. Commit de fusion : main reçoit un nouveau commit, 47b3604, avec deux parents, 2e9e37c et 6c3828f. Les commits de la branche font donc partie de main. Squash : main reçoit un nouveau commit, edddd6f, après 2e9e37c. 6b53600 et 6c3828f ne sont pas dans main. Rebase : main reçoit deux nouveaux commits, c8c08d3 et da418c8, après 2e9e37c. Ce sont des copies de 6b53600 et 6c3828f avec de nouvelles empreintes. Les anciens commits ne sont pas dans main.
Le même départ, trois résultats. Les cercles pleins sont de nouveaux commits. Les cercles en pointillés sont des commits qui ne font pas partie de main.

Pourquoi un rebase change-t-il les empreintes? L’empreinte d’un commit est calculée à partir de tout son contenu, y compris l’empreinte de son parent. Une copie avec un nouveau parent est un commit différent. Elle reçoit donc une empreinte différente, même si ses changements sont identiques.

Exemple détaillé : essayer les trois méthodes côte à côte

Section intitulée « Exemple détaillé : essayer les trois méthodes côte à côte »

Amira veut voir chaque résultat sans modifier main ni add-faq. Elle utilise donc des branches temporaires : de nouvelles étiquettes de branche qu’elle supprime après le test. Une branche ne coûte presque rien, et les vraies branches ne bougent pas.

Elle crée une copie de main appelée try-merge et y fusionne add-faq :

Terminal (Amira, ticket-notes)
git switch -c try-merge main
git merge --no-ff --no-edit add-faq
git log --oneline --graph -5
Sortie
Switched to a new branch 'try-merge'
Merge made by the 'ort' strategy.
faq.txt | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 faq.txt
* 47b3604 (HEAD -> try-merge) Merge branch 'add-faq' into try-merge
|\
| * 6c3828f (origin/add-faq, add-faq) Add a network question to the FAQ
| * 6b53600 Add FAQ
* | 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
|/
* e058b3f Add ticket 103

git switch -c try-merge main crée la nouvelle branche à partir de main, et non de sa branche actuelle. Le message indique into try-merge (dans try-merge) parce que la cible n’est pas main. Sur main, il indique seulement Merge branch 'add-faq' (fusionner la branche add-faq).

L’historique conserve tout : les deux commits de la FAQ, leurs empreintes d’origine et le point de jonction de la branche.

Elle crée une deuxième copie de main. git merge --squash place tous les changements de add-faq dans la zone de préparation, sans créer de commit :

Terminal (Amira, ticket-notes)
git switch -c try-squash main
git merge --squash add-faq
git status --short
Sortie
Switched to a new branch 'try-squash'
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
A faq.txt

Elle enregistre les changements dans un seul commit, avec un message pour toute la demande de tirage :

Terminal (Amira, ticket-notes)
git commit -m "Add an FAQ with two questions"
git log --oneline --graph -3
Sortie
[try-squash edddd6f] Add an FAQ with two questions
1 file changed, 5 insertions(+)
create mode 100644 faq.txt
* edddd6f (HEAD -> try-squash) Add an FAQ with two questions
* 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
* e058b3f Add ticket 103

L’historique forme une ligne droite. edddd6f contient les changements des deux commits de la FAQ, mais a un seul parent, 2e9e37c. Les commits 6b53600 et 6c3828f ne font pas partie de cet historique. Leurs messages distincts n’y figurent plus.

D’abord, Amira note les empreintes des commits de la branche :

Terminal (Amira, ticket-notes)
git log --oneline main..add-faq
Sortie
6c3828f Add a network question to the FAQ
6b53600 Add FAQ

Cette fois, elle crée une copie de add-faq, car un rebase modifie la branche actuelle. Elle effectue ensuite un rebase de la copie sur main :

Terminal (Amira, ticket-notes)
git switch -c try-rebase add-faq
git rebase main
git log --oneline --graph -4
Sortie
Switched to a new branch 'try-rebase'
Successfully rebased and updated refs/heads/try-rebase.
* da418c8 (HEAD -> try-rebase) Add a network question to the FAQ
* c8c08d3 Add FAQ
* 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
* e058b3f Add ticket 103

Les deux commits ont les mêmes messages et les mêmes changements. Mais ils partent maintenant de 2e9e37c et ont de nouvelles empreintes : c8c08d3 et da418c8. Les originaux existent toujours sur add-faq :

Terminal (Amira, ticket-notes)
git log --oneline main..add-faq
git log --oneline main..try-rebase
Sortie
6c3828f Add a network question to the FAQ
6b53600 Add FAQ
da418c8 Add a network question to the FAQ
c8c08d3 Add FAQ

Pour terminer, la branche cible avance jusqu’aux copies. Amira crée une copie de main appelée try-ff et fusionne avec --ff-only. Cette option fusionne seulement si Git peut effectuer une avance rapide. Sinon, Git s’arrête sans rien changer.

Terminal (Amira, ticket-notes)
git switch -c try-ff main
git merge --ff-only try-rebase
Sortie
Switched to a new branch 'try-ff'
Updating 2e9e37c..da418c8
Fast-forward
faq.txt | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 faq.txt

L’avance rapide a fonctionné, car après le rebase, les copies partent du dernier commit de main. Aucun commit de fusion n’a été créé.

Les trois résultats contiennent les mêmes fichiers. git diff --stat entre ces résultats n’affiche rien :

Terminal (Amira, ticket-notes)
git diff --stat try-merge try-squash
git diff --stat try-merge try-ff

L’absence de sortie signifie qu’il n’y a aucune différence. Seul l’historique diffère.

Amira retourne sur main. git branch -d refuse de supprimer try-merge, car son commit de fusion n’est pas dans main :

Terminal (Amira, ticket-notes)
git switch main
git branch -d try-merge
Sortie
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
error: the branch 'try-merge' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D try-merge'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

Ces branches servent aux tests, alors elle les supprime avec -D. -D supprime une branche même si ses commits ne sont fusionnés nulle part :

Terminal (Amira, ticket-notes)
git branch -D try-merge try-squash try-rebase try-ff
Sortie
Deleted branch try-merge (was 47b3604).
Deleted branch try-squash (was edddd6f).
Deleted branch try-rebase (was da418c8).
Deleted branch try-ff (was da418c8).

main et add-faq n’ont pas changé pendant le test.

Commit de fusion Squash Rebase, puis avance rapide
Nouveaux commits sur main 1 commit de fusion 1 commit 1 copie pour chaque commit de la branche
Commits de la branche Conservés, avec les mêmes empreintes Absents de main. Leurs changements sont dans le nouveau commit. Réécrits : copies avec de nouvelles empreintes
Forme de l’historique Montre le départ et la jonction de la branche Une ligne droite Une ligne droite
Messages distincts des commits Conservés Remplacés par un seul message Conservés
Bouton sur GitHub Create a merge commit (créer un commit de fusion) Squash and merge (regrouper et fusionner) Rebase and merge (rebaser et fusionner)
Quand les équipes l’utilisent Quand chaque commit compte et que l’équipe veut voir chaque demande de tirage comme un ensemble Quand la branche contient beaucoup de petits commits ou de corrections, et qu’un commit par demande de tirage est plus clair Quand l’équipe veut un historique droit et que chaque commit est bien organisé

Aucune méthode n’est toujours la bonne. Beaucoup d’équipes choisissent une méthode pour toutes leurs demandes de tirage afin de garder un historique cohérent. Vérifiez quelle méthode votre équipe utilise avant de fusionner.

Utilisez des branches temporaires. N’exécutez pas ces commandes directement sur main ni sur add-faq.

  1. Dans ~/projects/c14-lab/ticket-notes, exécutez git status. La sortie affiche On branch main et working tree clean.

  2. Exécutez git log --oneline main..add-faq. Notez les deux empreintes : 6c3828f et 6b53600.

  3. Essayez un commit de fusion :

    Fenêtre de terminal
    git switch -c try-merge main
    git merge --no-ff --no-edit add-faq
  4. Exécutez git log --oneline --graph -5. Le dernier commit a deux parents, et 6b53600 et 6c3828f figurent dans le graphe.

  5. Essayez un squash :

    Fenêtre de terminal
    git switch -c try-squash main
    git merge --squash add-faq
    git commit -m "Add an FAQ with two questions"
  6. Exécutez git log --oneline --graph -3. L’historique forme une ligne droite. Les commits de la FAQ n’y figurent pas.

  7. Essayez un rebase sur une copie de la branche :

    Fenêtre de terminal
    git switch -c try-rebase add-faq
    git rebase main

    La sortie se termine par Successfully rebased and updated refs/heads/try-rebase. (rebasage et mise à jour de refs/heads/try-rebase réussis).

  8. Exécutez git log --oneline main..try-rebase. Comparez les empreintes avec celles de l’étape 2. Les messages sont identiques. Les empreintes diffèrent.

  9. Faites avancer une copie de main jusqu’aux commits rebasés par avance rapide :

    Fenêtre de terminal
    git switch -c try-ff main
    git merge --ff-only try-rebase

    La sortie contient Fast-forward (avance rapide).

  10. Comparez les fichiers des résultats :

    Fenêtre de terminal
    git diff --stat try-merge try-squash
    git diff --stat try-merge try-ff

    Il n’y a aucune sortie : les fichiers sont identiques.

  11. Passez à main et supprimez les branches temporaires :

    Fenêtre de terminal
    git switch main
    git branch -D try-merge try-squash try-rebase try-ff
  12. Exécutez git log --oneline --graph --all -6. main est toujours à 2e9e37c, et add-faq est toujours à 6c3828f.

La demande de tirage (pull request) de Sam dans la dernière leçon, add-ticket-104 → main, contient deux commits (enregistrements des changements) : e34484f Add ticket 104 et 5a15d07 Improve README wording.

D’abord, faites une prédiction pour chaque méthode :

  1. Combien de nouveaux commits main reçoit-elle?
  2. Les empreintes e34484f et 5a15d07 apparaissent-elles dans l’historique de main?
  3. Une personne qui consulte main voit-elle encore que la modification du README était un commit distinct?

Vérifiez ensuite vos prédictions avec des branches temporaires. Utilisez origin/add-ticket-104 comme branche à fusionner, et git log --oneline main..<throwaway branch> pour voir le résultat. Supprimez les branches temporaires à la fin.

Afficher une réponse modèle
Terminal (Amira, ticket-notes)
git switch -c try-merge main
git merge --no-ff --no-edit origin/add-ticket-104
git log --oneline main..try-merge
Sortie (fin)
5c41513 Merge remote-tracking branch 'origin/add-ticket-104' into try-merge
5a15d07 Improve README wording
e34484f Add ticket 104
Terminal (Amira, ticket-notes)
git switch -c try-squash main
git merge --squash origin/add-ticket-104
git commit -m "Add ticket 104"
git log --oneline main..try-squash
git show --stat HEAD
Sortie (fin)
37a47a9 Add ticket 104
README.md | 2 +-
scratch.txt | 2 ++
tickets.txt | 1 +
3 files changed, 4 insertions(+), 1 deletion(-)
Terminal (Amira, ticket-notes)
git switch -c try-rebase origin/add-ticket-104
git rebase main
git log --oneline main..try-rebase
Sortie (fin)
9586a0f Improve README wording
651a164 Add ticket 104

Exécutez ensuite git switch main et git branch -D try-merge try-squash try-rebase.

Nouveaux commits e34484f et 5a15d07 dans main? Modification du README visible comme commit distinct?
Commit de fusion 3 : les deux commits et le commit de fusion Oui Oui
Écrasement 1 Non Non. Elle est cachée dans « Add billet 104 » (ajouter le billet 104), avec scratch.txt.
Rebasage 2 copies Non : des copies avec de nouvelles empreintes Oui

Le résultat de l’écrasement est dangereux ici. Un écrasement ne supprime ni les modifications sans rapport ni les fichiers ajoutés par erreur. Il les cache dans un seul commit dont le message ne les mentionne pas. Une méthode de fusion ne remplace jamais une révision : corrigez d’abord la demande de tirage, comme dans la dernière leçon. Fusionnez-la ensuite.

Erreur courante

Après une fusion par écrasement, git branch -d refuse

Symptôme : Amira fusionne add-faq dans main par écrasement, pousse main et supprime la branche distante avec git push origin --delete add-faq. Elle supprime ensuite sa branche locale :

Fenêtre de terminal
git branch -d add-faq
Sortie
error: the branch 'add-faq' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D add-faq'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

Cause : -d vérifie si les commits de la branche sont dans main. Après un écrasement, ils n’y sont pas : main contient un nouveau commit avec les mêmes modifications. Cependant, 6b53600 et 6c3828f ne font pas partie de son historique. Git ne peut pas voir que les modifications sont identiques, alors il refuse. git log --oneline main..add-faq affiche encore les deux commits.

Solution :

  1. Vérifiez que le contenu est dans main. Comparez le fichier dans les deux branches :

    Fenêtre de terminal
    git diff add-faq main -- faq.txt

    Si rien ne s’affiche, faq.txt est identique dans les deux branches. Comparez seulement les fichiers que la branche a modifiés. main peut contenir d’autres nouvelles modifications absentes de la branche.

  2. Si rien ne s’affiche, supprimez la branche avec git branch -D add-faq.

  3. Si un résultat s’affiche, arrêtez. Une partie du travail de la branche n’est pas dans main. Trouvez pourquoi avant de supprimer quoi que ce soit.

Erreur courante

Après un rebasage, git push est rejeté

Symptôme : Amira rebase la branche add-faq elle-même sur main. Elle avait déjà poussé la branche. Maintenant, git status et git push affichent :

git status (first lines)
On branch add-faq
Your branch and 'origin/add-faq' have diverged,
and have 3 and 2 different commits each, respectively.
git push
To /Users/amira/projects/c14-lab/team-remote.git
! [rejected] add-faq -> add-faq (non-fast-forward)
error: failed to push some refs to '/Users/amira/projects/c14-lab/team-remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Cause : la branche distante contient encore les anciens commits, 6b53600 et 6c3828f. Sa branche contient plutôt les copies. Une poussée normale doit conserver tous les commits présents sur la branche distante, alors Git la refuse. Ne suivez pas le conseil affiché ici. Une récupération avec fusion réunirait les anciens commits et les copies, et chaque modification apparaîtrait deux fois.

Solution : déterminez d’abord si d’autres personnes utilisent la branche.

  • Si d’autres personnes utilisent la branche, ou s’il s’agit de main : ne forcez pas la poussée. Consultez l’équipe. Habituellement, vous faites une fusion plutôt qu’un rebasage.

  • Si vous êtes la seule personne à utiliser la branche : remplacez la branche distante avec --force-with-lease :

    Fenêtre de terminal
    git push --force-with-lease
    Sortie
    To /Users/amira/projects/c14-lab/team-remote.git
    + 6c3828f...68770b4 add-faq -> add-faq (forced update)

--force-with-lease remplace la branche distante seulement si elle est encore dans l’état de votre dernière récupération. Si un collègue a poussé vers la branche depuis, Git refuse, et son commit reste intact :

Sortie
To /Users/amira/projects/c14-lab/team-remote.git
! [rejected] add-faq -> add-faq (stale info)
error: failed to push some refs to '/Users/amira/projects/c14-lab/team-remote.git'

Récupérez alors les changements et examinez son commit avant de faire autre chose. N’utilisez jamais --force-with-lease ou --force sur main.

Vérifiez votre compréhension

Faites une prédiction : Sam a add-faq dans son clone, avec 6b53600 et 6c3828f. Amira rebase add-faq et force la poussée. Sam ajoute un commit à sa branche add-faq et exécute git push. Que se passe-t-il, et quelle règle Amira a-t-elle enfreinte?

Afficher la réponse

La poussée de Sam est rejetée. La branche distante add-faq contient maintenant les copies issues du rebasage. La branche de Sam contient encore les anciens commits, donc sa poussée n’est pas une avance rapide. S’il récupère les changements et les fusionne, son historique contient les anciens commits et les copies : les mêmes modifications deux fois. Amira a enfreint cette règle : ne rebasez pas des commits que d’autres personnes ont déjà.

Vérifiez votre compréhension

Vérification de fin : expliquez quel historique chaque méthode de fusion conserve ou réécrit. Utilisez add-faq comme exemple.

Afficher la réponse
  • Commit de fusion : conserve tout l’historique. 6b53600 et 6c3828f font maintenant partie de main avec les mêmes empreintes. Un nouveau commit de fusion avec deux parents indique où la branche a été réunie.
  • Écrasement : conserve les modifications, mais pas les commits de la branche. main reçoit un nouveau commit avec un parent et un message. 6b53600 et 6c3828f, ainsi que leurs messages distincts, ne sont pas dans main.
  • Rebasage : réécrit les commits de la branche. Git crée des copies avec les mêmes modifications et messages, mais de nouvelles empreintes, après la version la plus récente de main. Ensuite, main avance par une avance rapide, et aucun commit de fusion n’est créé.

Dans les trois cas, les fichiers dans main sont identiques.

Récapitulatif

  • Un commit de fusion conserve chaque commit de la branche et son empreinte, et ajoute un commit avec deux parents.
  • Un écrasement réunit toutes les modifications d’une branche dans un nouveau commit. Les commits de la branche ne font pas partie de main.
  • Un rebasage copie les commits de la branche sur un nouveau point de départ. Les copies ont de nouvelles empreintes.
  • Les trois méthodes donnent les mêmes fichiers. Elles donnent des historiques différents.
  • Ne rebasez pas des commits que d’autres personnes ont déjà. Utilisez --force-with-lease seulement sur une branche que vous seul utilisez, et jamais sur main.
  • Après une fusion par écrasement, git branch -d refuse. Vérifiez le contenu avec git diff, puis utilisez -D.

C’est la fin du module 3. Vous pouvez maintenant faire passer une modification par une demande de tirage et réviser une demande. Vous pouvez aussi choisir comment l’intégrer à main. Dans le module 4, deux personnes modifient la même ligne, et vous résolvez le conflit.

Leçon suivante : Les conflits, étape par étape