Fusion (merge)#

La fusion est le mécanisme par lequel Git intègre le travail réalisé sur des branches parallèles. Lorsque plusieurs développeurs contribuent simultanément à un projet, ou lorsqu’un même développeur travaille sur plusieurs fonctionnalités en parallèle, la fusion est l’opération qui réconcilie ces lignes de développement divergentes en un historique commun.

Git propose plusieurs stratégies de fusion, chacune adaptée à une situation particulière. Comprendre ces stratégies permet de choisir la bonne approche selon le contexte, de maintenir un historique lisible et de résoudre efficacement les conflits lorsqu’ils surviennent. Ce chapitre couvre les fusions fast-forward, à trois voies, squash, ainsi que la résolution des conflits et les stratégies avancées.

Fusion fast-forward#

Définition 21 (Fusion fast-forward)

Une fusion fast-forward se produit lorsque la branche cible n’a reçu aucun nouveau commit depuis le point de divergence. Dans ce cas, Git se contente d’avancer le pointeur de la branche cible vers le dernier commit de la branche source. Aucun commit de fusion n’est créé : l’historique reste parfaitement linéaire.

Formellement, si la branche main pointe vers le commit \(C_1\) et que la branche feature descend linéairement de \(C_1\) via \(C_2, C_3, \ldots, C_n\), alors git merge feature déplace simplement le pointeur de main vers \(C_n\).

Illustrons ce mécanisme avec un dépôt de démonstration. On crée une branche feature à partir de main, on y ajoute deux commits, puis on fusionne depuis main.

%%bash
cd /tmp && rm -rf demo-ff && mkdir demo-ff && cd demo-ff && git init
git config user.email "demo@example.com" && git config user.name "Demo"

# Commit initial sur main
echo "# Projet" > README.md
git add README.md && git commit -m "Initial commit"

# Créer la branche feature et y ajouter des commits
git checkout -b feature
echo "fonction_a()" > module.py
git add module.py && git commit -m "Ajouter fonction_a"
echo "fonction_b()" >> module.py
git add module.py && git commit -m "Ajouter fonction_b"

# Revenir sur main et fusionner
git checkout main
echo "=== Avant la fusion ==="
git log --oneline --all --graph

git merge feature
echo ""
echo "=== Après la fusion (fast-forward) ==="
git log --oneline --all --graph
Dépôt Git vide initialisé dans /tmp/demo-ff/.git/
[main (commit racine) d521453] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 RE
ADME.md
Basculement sur la nouvelle branche 'feature'
[feature 859dfca] Ajouter fonction_a
 1 file changed, 1 insertion(+)
 create mode 100644 module.py
[feature a3c21ee] Ajouter fonction_b
 1 file changed, 1 insertion(+)
Basculement sur la branche 'main'
=== Avant la fusion ===
* a3c21ee Ajouter fonction_b
* 859dfca Ajouter fonction_a
* d521453 Initial commit
Mise à jour d521453..a3c21ee
Fast-forward
 module.py | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 module.py
=== Après la fusion (fast-forward) ===
* a3c21ee Ajouter fonction_b
* 859dfca Ajouter fonction_a
* d521453 Initial commit

On observe que main a simplement avancé vers le dernier commit de feature. Il n’y a pas de commit de fusion supplémentaire et l’historique est linéaire.

Remarque 16

La fusion fast-forward préserve un historique linéaire, mais elle fait disparaitre l’information que le travail a été realise sur une branche séparée. Pour conserver cette trace dans l’historique, on peut forcer la creation d’un commit de fusion avec l’option --no-ff :

git merge --no-ff feature

Cette pratique est recommandée dans les workflows collaboratifs où l’on souhaite que chaque fonctionnalité soit clairement délimitée dans le graphe des commits.

Visualisation : avant et après un fast-forward#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import networkx as nx

fig, axes = plt.subplots(1, 2, figsize=(14, 4))

# --- Avant la fusion ---
ax = axes[0]
ax.set_title("Avant la fusion", fontsize=13, fontweight="bold")

G_before = nx.DiGraph()
G_before.add_edges_from([("C0", "C1"), ("C1", "C2"), ("C2", "C3")])

pos_before = {"C0": (0, 0), "C1": (1, 0), "C2": (2, 0), "C3": (3, 0)}
colors_before = ["#4a90d9", "#4a90d9", "#e07b53", "#e07b53"]

nx.draw_networkx_nodes(G_before, pos_before, ax=ax, node_size=700,
                       node_color=colors_before, edgecolors="black", linewidths=1.5)
nx.draw_networkx_labels(G_before, pos_before, ax=ax,
                        font_size=9, font_weight="bold", font_color="white")
nx.draw_networkx_edges(G_before, pos_before, ax=ax,
                       edge_color="gray", arrows=True, arrowsize=15, width=2)

ax.annotate("main", xy=(1, 0), xytext=(1, 0.5),
            fontsize=10, fontweight="bold", color="#4a90d9",
            ha="center", arrowprops=dict(arrowstyle="->", color="#4a90d9"))
ax.annotate("feature", xy=(3, 0), xytext=(3, 0.5),
            fontsize=10, fontweight="bold", color="#e07b53",
            ha="center", arrowprops=dict(arrowstyle="->", color="#e07b53"))
ax.set_xlim(-0.7, 4.2)
ax.set_ylim(-0.8, 1.0)
ax.axis("off")

# --- Après la fusion ---
ax = axes[1]
ax.set_title("Après la fusion (fast-forward)", fontsize=13, fontweight="bold")

G_after = nx.DiGraph()
G_after.add_edges_from([("C0", "C1"), ("C1", "C2"), ("C2", "C3")])

pos_after = {"C0": (0, 0), "C1": (1, 0), "C2": (2, 0), "C3": (3, 0)}
colors_after = ["#4a90d9", "#4a90d9", "#4a90d9", "#4a90d9"]

nx.draw_networkx_nodes(G_after, pos_after, ax=ax, node_size=700,
                       node_color=colors_after, edgecolors="black", linewidths=1.5)
nx.draw_networkx_labels(G_after, pos_after, ax=ax,
                        font_size=9, font_weight="bold", font_color="white")
nx.draw_networkx_edges(G_after, pos_after, ax=ax,
                       edge_color="gray", arrows=True, arrowsize=15, width=2)

ax.annotate("main / feature", xy=(3, 0), xytext=(3, 0.5),
            fontsize=10, fontweight="bold", color="#4a90d9",
            ha="center", arrowprops=dict(arrowstyle="->", color="#4a90d9"))
ax.set_xlim(-0.7, 4.2)
ax.set_ylim(-0.8, 1.0)
ax.axis("off")

plt.suptitle("Fusion fast-forward", fontsize=14, fontweight="bold", y=1.02)
plt.show()
_images/f94e72105dab73f804b54a7971c0bd009d641d3e478903d182b3e5aacd1d8b28.png

Fusion à trois voies (3-way merge)#

Définition 22 (Fusion à trois voies (3-way merge))

Lorsque les deux branches ont divergé – c’est-à-dire que chacune possède des commits que l’autre n’a pas – Git ne peut pas simplement avancer un pointeur. Il réalise alors une fusion à trois voies (3-way merge) :

  1. Git identifie l”ancêtre commun (merge base), le dernier commit partagé par les deux branches.

  2. Il compare les modifications apportées par chaque branche par rapport à cet ancêtre.

  3. Il crée un nouveau commit de fusion (merge commit) qui possède deux parents et qui combine les modifications des deux côtés.

Ce commit de fusion matérialise le point de convergence dans le graphe des commits, formant la structure en losange caractéristique.

Créons un scénario où les deux branches divergent.

%%bash
cd /tmp && rm -rf demo-3way && mkdir demo-3way && cd demo-3way && git init
git config user.email "demo@example.com" && git config user.name "Demo"

# Commit initial
echo "# Projet" > README.md
git add README.md && git commit -m "Initial commit"

# Travail sur feature
git checkout -b feature
echo "fonction_x()" > feature.py
git add feature.py && git commit -m "Ajouter feature.py"

# Travail sur main (divergence)
git checkout main
echo "config = {}" > config.py
git add config.py && git commit -m "Ajouter config.py"

echo "=== Avant la fusion (branches divergentes) ==="
git log --oneline --all --graph

# Fusionner feature dans main
git merge feature -m "Merge branch 'feature' into main"

echo ""
echo "===Après la fusion (3-way merge) ==="
git log --oneline --all --graph
Dépôt Git vide initialisé dans /tmp/demo-3way/.git/
[main (commit racine) 7aab8be] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 RE
ADME.md
Basculement sur la nouvelle branche 'feature'
[feature 8f0c4cf] Ajouter feature.py
 1 file changed, 1 insertion(+)
 create mode 100644 feature.py
Basculement sur la branche 'main'
[main c2a042b] Ajouter config.py
 1 file changed, 1 insertion(+)
 create mode 100644 config.py
=== Avant la fusion (branches divergentes) ===
* c2a042b Ajouter config.py
| * 8f0c4cf Ajouter feature.py
|/  
* 7aab8be Initial commit
Merge made by the 'ort' strategy.
 feature.py | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 feature.py

===Après la fusion (3-way merge) ===
*   4b896cb Merge branch 'feature' into main
|\  
| * 8f0c4cf Ajouter feature.py
* | c2a042b Ajouter config.py
|/  
* 7aab8be Initial commit

On voit clairement le commit de fusion avec ses deux parents dans le graphe. La commande git log --graph rend cette structure visible grace aux lignes de connexion.

Remarque 17

Le commit de fusion possede deux parents, ce qui le distingue de tout commit ordinaire. On peut vérifier cela avec git cat-file -p HEAD qui affichera deux lignes parent. Cette information est essentielle pour Git : elle lui permet de savoir que les modifications des deux branches ont été intégrées et d’éviter de les réappliquer lors de fusions ultérieures.

Visualisation : avant et après une fusion à trois voies#

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- Avant la fusion ---
ax = axes[0]
ax.set_title("Avant la fusion (branches divergentes)", fontsize=13, fontweight="bold")

G_before = nx.DiGraph()
G_before.add_edges_from([("C0", "C1"), ("C0", "C2")])

pos_before = {"C0": (0, 0), "C1": (1.5, 0.6), "C2": (1.5, -0.6)}
colors_before = ["#999999", "#4a90d9", "#e07b53"]

nx.draw_networkx_nodes(G_before, pos_before, ax=ax, node_size=700,
                       node_color=colors_before, edgecolors="black", linewidths=1.5)
nx.draw_networkx_labels(G_before, pos_before, ax=ax,
                        font_size=9, font_weight="bold", font_color="white")
nx.draw_networkx_edges(G_before, pos_before, ax=ax,
                       edge_color="gray", arrows=True, arrowsize=15, width=2)

ax.annotate("main", xy=(1.5, 0.6), xytext=(2.5, 0.6),
            fontsize=10, fontweight="bold", color="#4a90d9",
            ha="center", arrowprops=dict(arrowstyle="->", color="#4a90d9"))
ax.annotate("feature", xy=(1.5, -0.6), xytext=(2.5, -0.6),
            fontsize=10, fontweight="bold", color="#e07b53",
            ha="center", arrowprops=dict(arrowstyle="->", color="#e07b53"))
ax.annotate("ancetre\ncommun", xy=(0, 0), xytext=(-0.8, 0.6),
            fontsize=9, fontstyle="italic", color="#999999",
            ha="center", arrowprops=dict(arrowstyle="->", color="#999999"))
ax.set_xlim(-1.5, 3.5)
ax.set_ylim(-1.2, 1.2)
ax.axis("off")

# --- Après la fusion ---
ax = axes[1]
ax.set_title("Après la fusion (3-way merge)", fontsize=13, fontweight="bold")

G_after = nx.DiGraph()
G_after.add_edges_from([("C0", "C1"), ("C0", "C2"), ("C1", "M"), ("C2", "M")])

pos_after = {"C0": (0, 0), "C1": (1.5, 0.6), "C2": (1.5, -0.6), "M": (3, 0)}
colors_after = ["#999999", "#4a90d9", "#e07b53", "#6ab04c"]

nx.draw_networkx_nodes(G_after, pos_after, ax=ax, node_size=700,
                       node_color=colors_after, edgecolors="black", linewidths=1.5)
nx.draw_networkx_labels(G_after, pos_after, ax=ax,
                        font_size=9, font_weight="bold", font_color="white")
nx.draw_networkx_edges(G_after, pos_after, ax=ax,
                       edge_color="gray", arrows=True, arrowsize=15, width=2)

ax.annotate("main", xy=(3, 0), xytext=(3.8, 0.4),
            fontsize=10, fontweight="bold", color="#6ab04c",
            ha="center", arrowprops=dict(arrowstyle="->", color="#6ab04c"))
ax.annotate("merge\ncommit", xy=(3, 0), xytext=(3.8, -0.4),
            fontsize=9, fontstyle="italic", color="#6ab04c",
            ha="center", arrowprops=dict(arrowstyle="->", color="#6ab04c"))
ax.annotate("ancetre\ncommun", xy=(0, 0), xytext=(-0.8, 0.6),
            fontsize=9, fontstyle="italic", color="#999999",
            ha="center", arrowprops=dict(arrowstyle="->", color="#999999"))
ax.set_xlim(-1.5, 4.8)
ax.set_ylim(-1.2, 1.2)
ax.axis("off")

legend_elements = [
    mpatches.Patch(facecolor="#4a90d9", edgecolor="black", label="main"),
    mpatches.Patch(facecolor="#e07b53", edgecolor="black", label="feature"),
    mpatches.Patch(facecolor="#6ab04c", edgecolor="black", label="merge commit"),
    mpatches.Patch(facecolor="#999999", edgecolor="black", label="ancetre commun"),
]
fig.legend(handles=legend_elements, loc="lower center", ncol=4, fontsize=10,
           frameon=True, fancybox=True)

plt.suptitle("Fusion à trois voies (3-way merge)", fontsize=14, fontweight="bold", y=1.02)
plt.subplots_adjust(bottom=0.15)
plt.show()
_images/336cdcf02cb552fb78b5a22d6f8d8a6c8c7ea63d3cf856f3e67222edf0a24c66.png

Résoudre les conflits#

Un conflit survient lorsque les deux branches modifient les mêmes lignes dans le même fichier. Git ne peut pas déterminer automatiquement quelle version conserver et demande l’intervention du développeur.

Créer un scenario de conflit#

%%bash
cd /tmp && rm -rf demo-conflict && mkdir demo-conflict && cd demo-conflict && git init
git config user.email "demo@example.com" && git config user.name "Demo"

# Commit initial avec un fichier partagé
echo "message = 'Bonjour'" > salut.py
git add salut.py && git commit -m "Initial commit"

# Branche feature : modifier la même ligne
git checkout -b feature
echo "message = 'Hello World'" > salut.py
git add salut.py && git commit -m "Message en anglais"

# Retour sur main : modifier la même ligne différemment
git checkout main
echo "message = 'Salut le monde'" > salut.py
git add salut.py && git commit -m "Message en francais"

echo "=== Tentative de fusion ==="
git merge feature || echo "(conflit detecté)"
Dépôt Git vide initialisé dans /tmp/demo-conflict/.git/
[main (commit racine) 165a222] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 sa
lut.py
Basculement sur la nouvelle branche 'feature'
[feature 45ba48b] Message en anglais
 1 file changed, 1 insertion(+), 1 deletion(-)
Basculement sur la branche 'main'
[main 1603ff5] Message en francais
 1 file changed, 1 insertion(+), 1 deletion(-)
=== Tentative de fusion ===
Fusion automatique de salut.py
CONFLIT (contenu) : Conflit de fusion dans salut.py
La fusion automat
ique a échoué ; réglez les conflits et validez le résultat.
(conflit detecté)

Git signale un conflit et interrompt la fusion. Examinons le contenu du fichier en conflit.

%%bash
cd /tmp/demo-conflict
echo "=== Contenu du fichier en conflit ==="
cat salut.py
echo ""
echo "=== Statut du dépôt ==="
git status
=== Contenu du fichier en conflit ===
<<<<<<< HEAD
message = 'Salut le monde'
=======
message = 'Hello World'
>>>>>>> feature
=== Statut du dépôt ===
Sur la branche main
Vous avez des chemins non fusionnés.
  (réglez les conflits puis lancez "git c
ommit")
  (utilisez "git merge --abort" pour annuler la fusion)

Chemins non fusionnés :
  (utilise
z "git add <fichier>..." pour marquer comme résolu)
	modifié des deux côtés :  salut.py

aucune 
modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")

Les marqueurs de conflit suivent toujours le même schema :

  • <<<<<<< HEAD : début de la version de la branche courante (ici main)

  • ======= : séparateur entre les deux versions

  • >>>>>>> feature : fin de la version de la branche entrante

Résoudre le conflit#

Pour résoudre le conflit, il faut éditer le fichier manuellement, supprimer les marqueurs et choisir le contenu final. Ensuite, on ajoute le fichier et on valide.

%%bash
cd /tmp/demo-conflict

# Résolution : on choisit de combiner les deux versions
cat > salut.py << 'EOF'
message_fr = 'Salut le monde'
message_en = 'Hello World'
EOF

git add salut.py
git commit -m "Résoudre le conflit : conserver les deux langues"

echo "=== Historique après résolution ==="
git log --oneline --graph
[main 
1a1091b] Résoudre le conflit : conserver les deux langues
=== Historique après résolution ===
*   1a1091b Résoudre le conflit : conserver les deux langues
|\  
| * 45ba48b Message en anglais
* 
| 1603ff5 Message en francais
|/  
* 165a222 Initial commit

Abandonner une fusion#

Si le conflit est trop complexe ou que l’on souhaite repartir à zero, on peut abandonner la fusion en cours.

%%bash
cd /tmp && rm -rf demo-abort && mkdir demo-abort && cd demo-abort && git init
git config user.email "demo@example.com" && git config user.name "Demo"

echo "x = 1" > data.py
git add data.py && git commit -m "Initial"

git checkout -b feature
echo "x = 2" > data.py
git add data.py && git commit -m "x = 2 sur feature"

git checkout main
echo "x = 3" > data.py
git add data.py && git commit -m "x = 3 sur main"

git merge feature || true
echo "=== Abandon de la fusion ==="
git merge --abort
echo ""
git status
echo "=== Le fichier est revenu à son état avant la fusion ==="
cat data.py
Dépôt Git vide initialisé dans /tmp/demo-abort/.git/
[main (commit racine) 27ffd2d] Initial
 1 file changed, 1 insertion(+)
 create mode 100644 data.py
Basculement sur la nouvelle branche 'feature'
[feature 2c1f8f9] x = 2 sur feature
 1 file changed, 1 insertion(+), 1 deletion(-)
Basculement sur la branche 'main'
[main cd00e6f] x = 3 sur main
 1 file changed, 1 insertion(+), 1 deletion(-)
Fusion automatique de data.py
CONFLIT (contenu) : Conflit de fusion dans data.py
La fusion automatique a échoué ; réglez les conflits et validez le résultat.
=== Abandon de la f
usion ===

Sur la branche main
rien à valider, la copie de travail est propre
=== Le fichier est revenu à son état avant la fusion ===
x = 3

Remarque 18

Pour minimiser les conflits dans un projet collaboratif :

  • Garder les branches courtes. Plus une branche vit longtemps, plus elle diverge de main et plus les conflits sont probables et complexes.

  • Fusionner main dans la branche feature regulièrement. Cela permet de résoudre les petits conflits au fil de l’eau plutot qu’un gros conflit a la fin.

  • Communiquer avec l’équipe. Si deux personnes travaillent sur le même fichier, il est préférable de se coordonner pour éviter des modifications concurrentes sur les mêmes lignes.

  • Découper le code en petits fichiers. Des fichiers courts et bien délimités réduisent la probabilité que deux branches touchent les mêmes lignes.

Fusion squash#

Définition 23 (Fusion squash)

Une fusion squash (git merge --squash feature) applique toutes les modifications de la branche source en une seule fois dans l’index de la branche cible, sans créer de commit de fusion ni de lien avec la branche source. Le développeur doit ensuite créer manuellement un commit qui regroupe l’ensemble des changements.

Le résultat est un unique commit sur la branche cible, comme si tout le travail de la branche avait été réalisé en une seule étape. L’historique détaillé de la branche source n’apparait pas dans main.

%%bash
cd /tmp && rm -rf demo-squash && mkdir demo-squash && cd demo-squash && git init
git config user.email "demo@example.com" && git config user.name "Demo"

echo "# Projet" > README.md
git add README.md && git commit -m "Initial commit"

# Branche feature avec plusieurs petits commits
git checkout -b feature
echo "etape 1" > feature.py
git add feature.py && git commit -m "WIP: début de la feature"
echo "etape 2" >> feature.py
git add feature.py && git commit -m "WIP: suite"
echo "etape 3" >> feature.py
git add feature.py && git commit -m "WIP: fin de la feature"

echo "=== Historique de feature ==="
git log --oneline feature

# Retour sur main et fusion squash
git checkout main
git merge --squash feature
git commit -m "Ajouter la feature complète"

echo ""
echo "=== Historique de main (propre, un seul commit) ==="
git log --oneline main

echo ""
echo "=== La branche feature apparait-elle comme fusionné ? ==="
git branch --merged
Dépôt Git vide initialisé dans /tmp/demo-squash/.git/
[main (commit racine) e3913ec] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 RE
ADME.md
Basculement sur la nouvelle branche 'feature'
[feature db1a4d1] WIP: début de la feature
 1 file changed, 1 insertion(+)
 create mode 100644 feat
ure.py
[feature d3b9337] WIP: suite
 1 file changed, 1 insertion(+)
[feature 642b4cc] WIP: fin de la feature
 1 file changed, 1 insertion(+)
=== Historique de feature ===
642b4cc WIP: fin de la feature
d3b9337 WIP: suite
db1a4d1 WIP: début de la feature
e3913ec Initial commit
Basculement sur la branche 'main'
Mise à jour e3913ec..642b4cc
Fast-forward
Validation compressée -- HEAD non mise à jour
 feature.
py | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 feature.py
[main 411981c] Ajouter la feature complète
 1 file changed, 3 insertions(+)
 create mode 100644 fea
ture.py
=== Historique de main (propre, un seul commit) ===
411981c Ajouter la feature complète
e3913ec Initial commit
=== La branche feature apparait-elle comme fusionné ? ===
* main

On constate que feature n’apparait pas dans la liste des branches fusionnées. C’est le comportement attendu : la fusion squash ne crée aucun lien de parenté avec la branche source.

Remarque 19

Apres une fusion squash, la branche source semble non fusionnée du point de vue de Git, car aucun commit de fusion ne relie les deux historiques. Il faut donc la supprimer explicitement avec git branch -D feature (notez le -D majuscule, car -d refuserait de supprimer une branche « non fusionnée »).

La fusion squash est particulièrement utile pour les branches de travail avec un historique désordonné (commits WIP, corrections de typos, etc.) que l’on souhaite présenter comme un changement atomique et propre dans main.

Stratégies de fusion#

Git dispose de plusieurs stratégies de fusion internes. Dans la grande majorité des cas, la stratégie par défaut suffit, mais il est utile de connaitre les alternatives.

Définition 24 (Stratégies de fusion Git)

Les principales stratégies de fusion sont :

  • récursive (défaut pour la fusion de deux branches) : compare les deux branches par rapport à l’ancêtre commun. S’il existe plusieurs ancêtres communs possibles, Git les fusionne récursivement pour créer un ancêtre virtuel. C’est la stratégie la plus robuste pour la plupart des cas.

  • ort (Ostensibly Recursive’s Twin) : remplacement moderne de recursive depuis Git 2.34, plus rapide et plus correct dans les cas limites. Activée par défaut dans les versions récentes.

  • octopus : permet de fusionner plus de deux branches en une seule opération. Utilisée automatiquement lorsque l’on passe plusieurs branches à git merge. Elle refuse toute fusion qui générerait un conflit.

  • ours : conserve intégralement le contenu de la branche courante en ignorant les modifications de la branche fusionnée. Le commit de fusion est tout de même crée, ce qui marque la branche comme fusionnée dans l’historique.

Résolution automatique des conflits avec -X#

Lorsqu’un conflit survient, on peut demander à Git de privilégier automatiquement une version avec les options -X ours ou -X theirs.

%%bash
cd /tmp && rm -rf demo-strategy && mkdir demo-strategy && cd demo-strategy && git init
git config user.email "demo@example.com" && git config user.name "Demo"

echo "valeur = 0" > param.py
git add param.py && git commit -m "Initial"

git checkout -b feature
echo "valeur = 42" > param.py
git add param.py && git commit -m "valeur = 42 sur feature"

git checkout main
echo "valeur = 99" > param.py
git add param.py && git commit -m "valeur = 99 sur main"

echo "=== Fusion avec -X theirs (privilégier la branche entrante) ==="
git merge feature -X theirs -m "Merge avec préférence theirs"
echo ""
cat param.py
echo ""
git log --oneline --graph
Dépôt Git vide initialisé dans /tmp/demo-strategy/.git/
[main (commit racine) 77232bb] Initial
 1 file changed, 1 insertion(+)
 create mode 100644 param.py
Basculement sur la nouvelle branche 'feature'
[feature 3743cbc] valeur = 42 sur feature
 1 file changed, 1 insertion(+), 1 deletion(-)
Basculement sur la branche 'main'
[main 9363e36] valeur = 99 sur main
 1 file changed, 1 insertion(+), 1 deletion(-)
=== Fusion avec -X theirs (privilégier la branche entrante) ===
Fusion automatique de param.py
Merge made by the 'ort' strategy.
 param.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

valeur = 42

*   0b282ca Merge avec préférence theirs
|\  
| * 3743cbc valeur = 42 sur feature
* | 9363e36 vale
ur = 99 sur main
|/  
* 77232bb Initial

La différence entre -X ours et -X theirs est cruciale :

  • -X ours : en cas de conflit, conserve la version de la branche courante (celle sur laquelle on se trouve).

  • -X theirs : en cas de conflit, conserve la version de la branche entrante (celle que l’on fusionne).

Ces options ne s’appliquent qu’aux lignes en conflit. Les modifications non conflictuelles des deux branches sont toujours intégrées normalement.

Remarque 20

En pratique, la stratégie par défaut (ort ou recursive selon la version de Git) gère correctement la vaste majorité des fusions. Les cas d’usage des autres stratégies sont rares :

  • octopus est parfois utilisée dans les projets qui maintiennent de nombreuses branches thématiques et les intègrent toutes en une seule opération.

  • ours est utile pour marquer une branche comme fusionnée sans en intégrer le contenu, par exemple pour déprecier une branche obsolète.

  • Les options -X ours et -X theirs sont pratiques dans les scripts automatisés ou lorsqu’on sait à l’avance quelle version doit prévaloir en cas de conflit.

Résumé#

Le tableau suivant récapitule les principales commandes de fusion et les situations où les utiliser.

Commande

Type de fusion

Quand l’utiliser

git merge feature

Fast-forward (si possible) ou 3-way

Cas général : intégrer une branche dans une autre

git merge --no-ff feature

3-way (force)

Conserver la trace de la branche dans l’historique

git merge --squash feature

Squash

Condenser une branche désordonnée en un seul commit propre

git merge -X ours feature

3-way avec préférence

Privilégier la branche courante en cas de conflit

git merge -X theirs feature

3-way avec préférence

Privilégier la branche entrante en cas de conflit

git merge --abort

Abandonner une fusion en cours (en cas de conflit)

Remarque 21

Le choix de la stratégie de fusion depend du contexte. Pour un historique lisible, on préfère --no-ff qui matérialise chaque branche dans le graphe. Pour un historique épuré, --squash condense le travail en un seul commit. Dans tous les cas, la clé pour éviter les conflits est de garder les branches courtes et de les synchroniser régulièrement avec la branche principale.