Rebase et réécriture d’historique#

Introduction#

Le rebase est l’une des commandes les plus puissantes de Git, mais aussi l’une des plus mal comprises. Là où git merge intègre deux lignes de développement en créant un commit de fusion, git rebase réécrit l’historique en rejouant une séquence de commits sur une nouvelle base. Le résultat est un historique linéaire, propre, sans bifurcation visible. Comprendre quand et comment utiliser le rebase est essentiel pour collaborer efficacement et maintenir un historique lisible.

Ce chapitre démystifie le rebase en l’abordant étape par étape. Nous commencerons par le principe fondamental, puis nous le mettrons en pratique avec des démonstrations concrètes. Nous comparerons en détail merge et rebase, avant d’explorer le rebase interactif — l’outil roi pour nettoyer un historique avant de le partager. Enfin, nous aborderons le cherry-pick et la règle d’or que tout utilisateur de Git doit connaître.

Le principe du rebase#

Définition 25 (Rebase)

Le rebase (rebaser) consiste à prendre une séquence de commits et à les rejouer au-dessus d’un autre commit de base. Concrètement, Git identifie les modifications introduites par chaque commit de la branche source, puis applique ces mêmes modifications, une par une, au sommet de la branche cible. Les commits originaux sont remplacés par de nouveaux commits : ceux-ci contiennent les mêmes changements (le même diff), mais possèdent des hashs différents car leur parent a changé.

Pour bien comprendre, prenons un scénario concret. Supposons que vous avez créé une branche feature à partir du commit B de main. Depuis, main a avancé avec un commit E, tandis que vous avez créé les commits C et D sur feature :

  • main : A — B — E

  • feature : A — B — C — D

Lorsque vous exécutez git rebase main depuis la branche feature, Git effectue les opérations suivantes :

  1. Il identifie les commits propres à feature qui ne sont pas sur main : C et D.

  2. Il « détache » temporairement ces commits.

  3. Il déplace le point de départ de feature au sommet de main (commit E).

  4. Il rejoue C au-dessus de E, créant un nouveau commit C”.

  5. Il rejoue D au-dessus de C”, créant un nouveau commit D”.

Le résultat est :

  • main : A — B — E

  • feature : A — B — E — C” — D”

Les commits C” et D” contiennent exactement les mêmes modifications que C et D, mais ils ont des hashs différents car leur ancêtre a changé. L’historique de feature est désormais linéaire au-dessus de main.

Visualisation : avant et après le rebase#

La visualisation suivante montre côte à côte l’état du graphe de commits avant et après un rebase.

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, 5))

# --- BEFORE REBASE ---
ax = axes[0]
ax.set_title("Avant rebase", fontsize=13, fontweight='bold', pad=15)

G_before = nx.DiGraph()
pos_before = {
    'A': (0, 1), 'B': (1, 1), 'E': (2, 1),
    'C': (2, 0), 'D': (3, 0)
}
edges_before = [('A', 'B'), ('B', 'E'), ('B', 'C'), ('C', 'D')]
G_before.add_edges_from(edges_before)

node_colors_before = {
    'A': '#95a5a6', 'B': '#95a5a6', 'E': '#3498db',
    'C': '#e67e22', 'D': '#e67e22'
}
colors_before = [node_colors_before[n] for n in G_before.nodes()]

nx.draw_networkx_nodes(G_before, pos_before, ax=ax, node_size=900,
                       node_color=colors_before, edgecolors='black', linewidths=1.5)
nx.draw_networkx_labels(G_before, pos_before, ax=ax,
                        font_size=12, font_weight='bold', font_color='white')
nx.draw_networkx_edges(G_before, pos_before, ax=ax, edge_color='#2c3e50',
                       width=2, arrows=True, arrowsize=20,
                       connectionstyle='arc3,rad=0.1')

# Branch labels
ax.text(2, 1.4, 'main', ha='center', fontsize=10, fontstyle='italic',
        color='#3498db', fontweight='bold')
ax.text(3, -0.4, 'feature', ha='center', fontsize=10, fontstyle='italic',
        color='#e67e22', fontweight='bold')

ax.set_xlim(-0.7, 3.7)
ax.set_ylim(-0.8, 1.8)
ax.axis('off')

# --- AFTER REBASE ---
ax = axes[1]
ax.set_title("Après rebase", fontsize=13, fontweight='bold', pad=15)

G_after = nx.DiGraph()
pos_after = {
    'A': (0, 0.5), 'B': (1, 0.5), 'E': (2, 0.5),
    "C'": (3, 0.5), "D'": (4, 0.5)
}
edges_after = [('A', 'B'), ('B', 'E'), ('E', "C'"), ("C'", "D'")]
G_after.add_edges_from(edges_after)

node_colors_after = {
    'A': '#95a5a6', 'B': '#95a5a6', 'E': '#3498db',
    "C'": '#e74c3c', "D'": '#e74c3c'
}
colors_after = [node_colors_after[n] for n in G_after.nodes()]

nx.draw_networkx_nodes(G_after, pos_after, ax=ax, node_size=900,
                       node_color=colors_after, edgecolors='black', linewidths=1.5)
nx.draw_networkx_labels(G_after, pos_after, ax=ax,
                        font_size=12, font_weight='bold', font_color='white')
nx.draw_networkx_edges(G_after, pos_after, ax=ax, edge_color='#2c3e50',
                       width=2, arrows=True, arrowsize=20)

# Branch labels
ax.text(2, 0.9, 'main', ha='center', fontsize=10, fontstyle='italic',
        color='#3498db', fontweight='bold')
ax.text(4, 0.1, 'feature (rebasée)', ha='center', fontsize=10, fontstyle='italic',
        color='#e74c3c', fontweight='bold')

ax.set_xlim(-0.7, 4.7)
ax.set_ylim(-0.4, 1.3)
ax.axis('off')

# Legend
legend_elements = [
    mpatches.Patch(facecolor='#95a5a6', edgecolor='black', label='Commits partagés'),
    mpatches.Patch(facecolor='#3498db', edgecolor='black', label='main'),
    mpatches.Patch(facecolor='#e67e22', edgecolor='black', label='feature (originale)'),
    mpatches.Patch(facecolor='#e74c3c', edgecolor='black', label='feature (rebasée)'),
]
fig.legend(handles=legend_elements, loc='lower center', ncol=4,
           fontsize=9, frameon=True, fancybox=True)

plt.subplots_adjust(bottom=0.15)
plt.show()
_images/b6db7e5c110f41ae5646de11f90853ffc10ec04d71d91a3b4ce51e44b2b1c13d.png

On observe clairement la différence : avant le rebase, l’historique bifurque au commit B ; après le rebase, feature se prolonge linéairement après E. Les commits C” et D” sont de nouveaux commits (d’où la notation prime) — ils contiennent les mêmes modifications mais ont des hashs différents.

Rebase en pratique#

Mettons en oeuvre un rebase complet dans un dépôt de démonstration. Nous allons créer deux branches divergentes, puis rebaser l’une sur l’autre.

Mise en place du dépôt#

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

# Historique commun
echo "Ligne 1" > fichier.txt
git add fichier.txt
git commit -m "A: premier commit"

echo "Ligne 2" >> fichier.txt
git add fichier.txt
git commit -m "B: deuxième commit"
Dépôt Git vide initialisé dans /tmp/demo-rebase/.git/
[main (commit racine) daea5a1] A: premier commit
 1 file changed, 1 insertion(+)
 create mode 100644
 fichier.txt
[main e66bdd9] B: deuxième commit
 1 file changed, 1 insertion(+)

Création de branches divergentes#

%%bash
cd /tmp/demo-rebase

# Créer la branche feature à partir de B
git checkout -b feature

echo "Feature ligne 1" >> fichier.txt
git add fichier.txt
git commit -m "C: ajout feature ligne 1"

echo "Feature ligne 2" >> fichier.txt
git add fichier.txt
git commit -m "D: ajout feature ligne 2"

# Revenir sur main et ajouter un commit
git checkout main

echo "Main ligne 3" > autre.txt
git add autre.txt
git commit -m "E: ajout fichier sur main"
Basculement sur la nouvelle branche 'feature'
[feature 6031052] C: ajout feature ligne 1
 1 file changed, 1 insertion(+)
[feature 72ae402] D: ajout feature ligne 2
 1 file changed, 1 insertion(+)
Basculement sur la branche 'main'
[main 5dabbd6] E: ajout fichier sur main
 1 file changed, 1 insertion(+)
 create mode 100644 autre.t
xt

État avant le rebase#

%%bash
cd /tmp/demo-rebase
echo "=== Graphe AVANT rebase ==="
git log --oneline --graph --all
=== Graphe AVANT rebase ===
* 72ae402 D: ajout feature ligne 2
* 6031052 C: ajout feature ligne 1
| * 5dabbd6 E: ajout fichier sur main
|/  
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit

On voit clairement la divergence : main et feature ont évolué séparément à partir du commit B.

Exécution du rebase#

%%bash
cd /tmp/demo-rebase
git checkout feature
git rebase main
Basculement sur la branche 'feature'
Rebasage (1/2)
Rebasage (2/2)
Rebasage et mise à jour de refs/heads/feature avec succès.

État après le rebase#

%%bash
cd /tmp/demo-rebase
echo "=== Graphe APRÈS rebase ==="
git log --oneline --graph --all
=== Graphe APRÈS rebase ===
* 6bfad0c D: ajout feature ligne 2
* 67e5a21 C: ajout feature ligne 1
* 5dabbd6 E: ajout fichier sur
 main
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit

L’historique est désormais linéaire : les commits de feature ont été rejoués au-dessus du commit E de main. Si l’on souhaite maintenant intégrer feature dans main, il suffira d’un simple fast-forward :

%%bash
cd /tmp/demo-rebase
git checkout main
git merge feature
echo ""
echo "=== Graphe après merge fast-forward ==="
git log --oneline --graph --all
Basculement sur la branche 'main'
Mise à jour 5dabbd6..6bfad0c
Fast-forward
 fichier.txt | 2 ++
 1 file changed, 2 insertions(+)
=== Graphe après merge fast-forward ===
* 6bfad0c D: ajout feature ligne 2
* 67e5a21 C: ajout feature ligne 1
* 5dabbd6 E: ajout fichier sur
 main
* e66bdd9 B: deuxième commit
* daea5a1 A: premier commit

Remarque 22

Après un rebase, la branche feature a un historique linéaire au-dessus de main. Cela signifie que main peut intégrer feature par un fast-forward merge — une simple avance du pointeur, sans commit de fusion. Le résultat est un historique parfaitement linéaire et lisible.

Merge vs Rebase#

Le merge et le rebase sont deux stratégies d’intégration fondamentalement différentes. Comprendre leurs avantages et inconvénients respectifs est essentiel pour choisir la bonne approche selon le contexte.

Définition 26 (Merge (fusion))

Le merge préserve l’historique tel qu’il s’est réellement produit. Il crée un commit de fusion (merge commit) qui possède deux parents : le sommet de chaque branche. Le graphe résultant montre explicitement qu’un développement parallèle a eu lieu et à quel moment les branches ont été réunies.

Définition 27 (Rebase)

Le rebase réécrit l’historique pour produire une ligne droite. Il déplace les commits d’une branche au-dessus d’une autre, comme si le travail avait été effectué séquentiellement. Le graphe résultant ne montre aucune bifurcation, même si le développement était en réalité parallèle.

Visualisation : merge vs rebase#

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, 5))

# --- MERGE ---
ax = axes[0]
ax.set_title("Résultat d'un merge", fontsize=13, fontweight='bold', pad=15)

G_merge = nx.DiGraph()
pos_merge = {
    'A': (0, 0.5), 'B': (1, 1), 'C': (2, 1), 'D': (1, 0), 'E': (2, 0),
    'M': (3, 0.5)
}
edges_merge = [('A', 'B'), ('A', 'D'), ('B', 'C'), ('D', 'E'),
               ('C', 'M'), ('E', 'M')]
G_merge.add_edges_from(edges_merge)

node_colors_merge = {
    'A': '#95a5a6', 'B': '#3498db', 'C': '#3498db',
    'D': '#e67e22', 'E': '#e67e22', 'M': '#9b59b6'
}
colors_merge = [node_colors_merge[n] for n in G_merge.nodes()]

nx.draw_networkx_nodes(G_merge, pos_merge, ax=ax, node_size=900,
                       node_color=colors_merge, edgecolors='black', linewidths=1.5)
nx.draw_networkx_labels(G_merge, pos_merge, ax=ax,
                        font_size=12, font_weight='bold', font_color='white')
nx.draw_networkx_edges(G_merge, pos_merge, ax=ax, edge_color='#2c3e50',
                       width=2, arrows=True, arrowsize=18,
                       connectionstyle='arc3,rad=0.05')

ax.text(2, 1.4, 'main', ha='center', fontsize=10, fontstyle='italic',
        color='#3498db', fontweight='bold')
ax.text(2, -0.4, 'feature', ha='center', fontsize=10, fontstyle='italic',
        color='#e67e22', fontweight='bold')
ax.text(3, 0.9, 'merge\ncommit', ha='center', fontsize=8, fontstyle='italic',
        color='#9b59b6')

ax.set_xlim(-0.5, 3.8)
ax.set_ylim(-0.8, 1.8)
ax.axis('off')

# --- REBASE ---
ax = axes[1]
ax.set_title("Résultat d'un rebase + fast-forward", fontsize=13, fontweight='bold', pad=15)

G_rebase = nx.DiGraph()
pos_rebase = {
    'A': (0, 0.5), 'B': (1, 0.5), 'C': (2, 0.5),
    "D'": (3, 0.5), "E'": (4, 0.5)
}
edges_rebase = [('A', 'B'), ('B', 'C'), ('C', "D'"), ("D'", "E'")]
G_rebase.add_edges_from(edges_rebase)

node_colors_rebase = {
    'A': '#95a5a6', 'B': '#3498db', 'C': '#3498db',
    "D'": '#e74c3c', "E'": '#e74c3c'
}
colors_rebase = [node_colors_rebase[n] for n in G_rebase.nodes()]

nx.draw_networkx_nodes(G_rebase, pos_rebase, ax=ax, node_size=900,
                       node_color=colors_rebase, edgecolors='black', linewidths=1.5)
nx.draw_networkx_labels(G_rebase, pos_rebase, ax=ax,
                        font_size=12, font_weight='bold', font_color='white')
nx.draw_networkx_edges(G_rebase, pos_rebase, ax=ax, edge_color='#2c3e50',
                       width=2, arrows=True, arrowsize=18)

ax.text(4, 0.1, 'main = feature', ha='center', fontsize=10, fontstyle='italic',
        color='#2c3e50', fontweight='bold')

ax.set_xlim(-0.5, 4.8)
ax.set_ylim(-0.4, 1.3)
ax.axis('off')

legend_elements = [
    mpatches.Patch(facecolor='#3498db', edgecolor='black', label='Commits main'),
    mpatches.Patch(facecolor='#e67e22', edgecolor='black', label='Commits feature'),
    mpatches.Patch(facecolor='#9b59b6', edgecolor='black', label='Commit de fusion'),
    mpatches.Patch(facecolor='#e74c3c', edgecolor='black', label='Commits rebasés'),
]
fig.legend(handles=legend_elements, loc='lower center', ncol=4,
           fontsize=9, frameon=True, fancybox=True)

plt.subplots_adjust(bottom=0.15)
plt.show()
_images/95847f9d085c4ad53eb37d21628fc7857b5d5ee1d03696aa124454655ee155f1.png

Quand utiliser le merge#

Le merge est le choix naturel dans les situations suivantes :

  • Intégration de fonctionnalités terminées : lorsque vous fusionnez une branche feature dans main, le commit de fusion documente explicitement le moment de l’intégration.

  • Branches partagées : si plusieurs développeurs travaillent sur la même branche, le merge préserve le contexte de chacun.

  • Préservation du contexte : dans un projet où l’on veut comprendre quand et pourquoi des branches ont été créées et fusionnées, le merge conserve cette information.

Quand utiliser le rebase#

Le rebase est préférable dans ces cas :

  • Nettoyage du travail local : avant de pousser une branche, rebaser sur main produit un historique linéaire et facile à relire.

  • Mise à jour d’une branche feature : au lieu de fusionner régulièrement main dans feature (ce qui crée des commits de fusion parasites), un rebase intègre proprement les dernières modifications.

  • Revue de code : un historique linéaire est plus facile à examiner commit par commit lors d’une revue de pull request.

Remarque 23

Une stratégie courante et efficace est de combiner les deux approches : utiliser git rebase pour maintenir sa branche feature à jour par rapport à main pendant le développement, puis git merge --no-ff pour intégrer la branche terminée dans main avec un commit de fusion explicite. On obtient ainsi le meilleur des deux mondes : un historique propre sur la branche, et une trace claire de l’intégration dans main.

Rebase interactif#

Le rebase interactif (git rebase -i) est l’un des outils les plus puissants de Git pour réécrire l’historique. Il permet de manipuler les commits un par un : les réordonner, les fusionner, les modifier ou les supprimer.

Définition 28 (Rebase interactif)

Le rebase interactif (git rebase -i <base>) ouvre un éditeur affichant la liste des commits entre <base> et HEAD. Devant chaque commit, une commande indique l’action à effectuer. En modifiant ces commandes avant de fermer l’éditeur, on contrôle précisément la réécriture de l’historique.

Les commandes du rebase interactif#

Voici les six commandes disponibles :

Commande

Abréviation

Effet

pick

p

Conserver le commit tel quel

reword

r

Conserver les modifications, modifier le message

edit

e

Arrêter le rebase pour modifier le commit (contenu ou message)

squash

s

Fusionner avec le commit précédent, concaténer les messages

fixup

f

Fusionner avec le commit précédent, ignorer ce message

drop

d

Supprimer le commit

Exemple 7 (Les commandes du rebase interactif en détail)

Supposons que vous ayez quatre commits à réécrire. Le rebase interactif présente la liste suivante :

pick a1b2c3d Ajout de la fonctionnalité X
pick e4f5g6h Correction typo dans X
pick i7j8k9l Ajout de tests pour X
pick m0n1o2p Fix oubli dans les tests

Voici les transformations possibles :

  • pick : garder a1b2c3d tel quel — c’est le comportement par défaut.

  • reword : remplacer pick par reword sur a1b2c3d pour changer son message de commit sans toucher aux modifications.

  • squash : remplacer pick par squash sur e4f5g6h pour le fusionner avec a1b2c3d. Les messages des deux commits sont concaténés et vous pouvez les éditer.

  • fixup : remplacer pick par fixup sur m0n1o2p pour le fusionner avec i7j8k9l, en ne conservant que le message de i7j8k9l. Idéal pour absorber une petite correction dans le commit principal.

  • drop : supprimer complètement un commit de l’historique. Attention : si d’autres commits dépendent de ses modifications, des conflits apparaîtront.

  • Réordonner : il suffit de changer l’ordre des lignes pour réordonner les commits dans l’historique.

Démonstration : squash de commits#

Dans un notebook Jupyter, il n’est pas possible d’ouvrir un éditeur interactif. On utilise la variable d’environnement GIT_SEQUENCE_EDITOR pour automatiser l’édition du fichier de rebase.

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

# Créer une série de commits
echo "Fonctionnalité X" > feature.txt
git add feature.txt
git commit -m "Ajout de la fonctionnalité X"

echo "Correction typo" >> feature.txt
git add feature.txt
git commit -m "Fix typo dans X"

echo "Tests pour X" >> feature.txt
git add feature.txt
git commit -m "Ajout des tests pour X"

echo ""
echo "=== Historique AVANT rebase interactif ==="
git log --oneline
Dépôt Git vide initialisé dans /tmp/demo-rebase-i/.git/
[main (commit racine) 0858d9a] Ajout de la fonctionnalité X
 1 file changed, 1 insertion(+)
 create
 mode 100644 feature.txt
[main f63c48a] Fix typo dans X
 1 file changed, 1 insertion(+)
[main 50101c9] Ajout des tests pour X
 1 file changed, 1 insertion(+)
=== Historique AVANT rebase interactif ===
50101c9 Ajout des tests pour X
f63c48a Fix typo dans X
0858d9a Ajout de la fonctionnalité X

Squashons les deux premiers commits en un seul. La commande sed remplace pick par squash sur la deuxième ligne (le commit « Fix typo ») :

%%bash
cd /tmp/demo-rebase-i

# Squash : fusionner le 2e commit dans le 1er
# sed remplace "pick" par "squash" uniquement sur la 2e ligne du fichier de rebase
GIT_SEQUENCE_EDITOR="sed -i '2s/pick/squash/'" git rebase -i HEAD~3

echo ""
echo "=== Historique APRÈS squash ==="
git log --oneline
fatal : amont invalide 'HEAD~3'
=== Historique APRÈS squash ===
50101c9 Ajout des tests pour X
f63c48a Fix typo dans X
0858d9a Ajout de la fonctionnalité X

Le commit « Fix typo dans X » a été absorbé dans le premier commit. L’historique ne contient plus que deux commits au lieu de trois. C’est exactement ce que l’on souhaite avant de partager son travail : un historique propre, où chaque commit représente un changement logique cohérent.

Remarque 24

Le rebase interactif est l’outil par excellence pour nettoyer une branche feature avant de la fusionner dans main. Pendant le développement, il est normal de créer de nombreux petits commits exploratoires, des corrections de typo, des ajustements. Avant de soumettre une pull request, on regroupe ces commits en unités logiques avec git rebase -i. Il est parfaitement sain — et même encouragé — de réécrire l’historique local (non poussé) aussi souvent que nécessaire.

Cherry-pick#

Parfois, on ne veut pas rebaser une branche entière, mais simplement copier un commit spécifique d’une branche vers une autre. C’est exactement ce que fait git cherry-pick.

Définition 29 (Cherry-pick)

La commande git cherry-pick <hash> copie un commit spécifique vers la branche courante. Git applique les modifications (diff) introduites par ce commit et crée un nouveau commit avec le même contenu mais un hash différent. Le commit original reste intact sur sa branche d’origine.

Le cas d’usage le plus courant est le portage de correctif (backport) : un bug est corrigé sur la branche de développement, et on veut appliquer la même correction sur une branche de maintenance ou de production.

Démonstration#

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

# Commit initial sur main
echo "Application v1" > app.txt
git add app.txt
git commit -m "Version initiale"

# Créer une branche de développement
git checkout -b develop

echo "Nouvelle fonctionnalité" >> app.txt
git add app.txt
git commit -m "Ajout fonctionnalité"

echo "Correction du bug #42" >> app.txt
git add app.txt
git commit -m "Fix bug #42"

echo "Autre fonctionnalité" >> app.txt
git add app.txt
git commit -m "Autre fonctionnalité"

# Récupérer le hash du commit de correction
FIX_HASH=$(git log --oneline | grep "Fix bug" | awk '{print $1}')
echo "Hash du commit à cherry-picker : $FIX_HASH"

# Revenir sur main et cherry-picker uniquement le fix
git checkout main
git cherry-pick $FIX_HASH

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

echo ""
echo "=== Historique de develop ==="
git log --oneline develop
Dépôt Git vide initialisé dans /tmp/demo-cherry/.git/
[main (commit racine) b9d15e4] Version initiale
 1 file changed, 1 insertion(+)
 create mode 100644 
app.txt
Basculement sur la nouvelle branche 'develop'
[develop 460caf3] Ajout fonctionnalité
 1 file changed, 1 insertion(+)
[develop 90ac833] Fix bug #42
 1 file changed, 1 insertion(+)
[develop a735b63] Autre fonctionnalité
 1 file changed, 1 insertion(+)
Hash du commit à cherry-picker : 90ac833
Basculement sur la branche 'main'
Fusion automatique de app.txt
CONFLIT (contenu) : Conflit de fusion dans app.txt
erreur : impossible d'appliquer 90ac833... Fix bug #42
astuce : Après résolution des conflits, m
arquez-les avec
astuce : "git add/rm <spéc-de-réf>", puis lancez
astuce : "git cherry-pick --con
tinue".
astuce : Vous pouvez aussi sauter ce commit avec "git cherry-pick --skip".
astuce : Pour a
rrêter et revenir à l'état antérieur à "git cherry-pick",,
astuce : lancez "git cherry-pick --
abort".
astuce : Disable this message with "git config advice.mergeConflict false"
=== Historique de main ===
b9d15e4 Version initiale

=== Historique de develop ===
a735b63 Autre fonctionnalité
90ac833 Fix bug #42
460caf3 Ajout fonctionnalité
b9d15e4 Version initiale

On observe que le correctif apparaît sur main avec un nouveau hash, tandis que le commit original reste sur develop. Seule la correction a été portée, sans les fonctionnalités avant et après.

Remarque 25

Le cherry-pick crée un commit dupliqué : l’original et la copie contiennent les mêmes modifications mais ont des hashs différents. Si les deux branches (main et develop) sont un jour fusionnées, Git devra traiter les deux versions du même changement. Dans la plupart des cas, Git gère cela intelligemment, mais cela peut parfois provoquer des conflits. Il est donc préférable d’utiliser le cherry-pick avec parcimonie, pour des cas ponctuels comme le portage de correctifs critiques.

La règle d’or du rebase#

Remarque 26

Ne jamais rebaser des commits déjà partagés (poussés sur un dépôt distant). C’est la règle la plus importante concernant le rebase. Le rebase réécrit l’historique : les commits originaux sont remplacés par de nouveaux commits avec des hashs différents. Si d’autres développeurs ont déjà basé leur travail sur les commits originaux, la réécriture provoque une divergence entre votre historique et le leur. La règle est simple : rebasez librement vos commits locaux (non poussés), ne rebasez jamais l’historique partagé.

Que se passe-t-il si on enfreint la règle ?#

Imaginons que vous avez poussé les commits C et D sur origin/feature, et qu’un collègue a tiré (pull) ces commits et travaille dessus. Si vous rebasez feature sur main, les commits C et D deviennent C” et D”. Votre historique local et l’historique distant divergent maintenant :

  • Votre historique : A — B — E — C” — D”

  • Historique distant : A — B — C — D

  • Historique de votre collègue : A — B — C — D — F

Pour pousser votre version, vous devez forcer avec git push --force, ce qui écrase l’historique distant. Votre collègue se retrouve alors avec un historique incohérent et doit résoudre manuellement la divergence. C’est une source majeure de confusion et de perte de travail dans les équipes.

git push --force-with-lease : l’alternative plus sûre#

Si vous devez absolument pousser un historique rebasé (par exemple, après avoir nettoyé une branche feature personnelle), utilisez --force-with-lease plutôt que --force :

git push --force-with-lease

Cette variante vérifie que la branche distante n’a pas été modifiée par quelqu’un d’autre depuis votre dernier fetch. Si elle a changé, le push est refusé, ce qui vous protège contre l’écrasement du travail d’un collègue. C’est un filet de sécurité essentiel.

Exemple 8 (Résumé de la règle d’or)

Voici comment appliquer la règle en pratique :

  • Commits locaux, non poussés : rebasez librement. Nettoyez, réorganisez, squashez autant que vous le souhaitez.

  • Commits poussés sur une branche personnelle (personne d’autre ne travaille dessus) : rebasez avec prudence, utilisez git push --force-with-lease.

  • Commits poussés sur une branche partagée (main, develop, branche d’équipe) : ne rebasez jamais. Utilisez git merge pour intégrer les modifications.

Résumé#

Merge vs Rebase : tableau comparatif#

Critère

git merge

git rebase

Historique

Préserve les bifurcations

Crée un historique linéaire

Commits

Crée un commit de fusion

Réécrit les commits existants

Hashs

Les commits originaux sont conservés

Les commits obtiennent de nouveaux hashs

Conflits

Résolus une seule fois

Résolus commit par commit

Utilisation typique

Intégrer une branche terminée

Nettoyer l’historique local

Branches partagées

Sûr

Dangereux (réécriture d’historique)

Lisibilité du graphe

Peut devenir complexe

Toujours linéaire

Commandes essentielles#

Commande

Description

git rebase <branche>

Rebaser la branche courante sur <branche>

git rebase -i HEAD~N

Rebase interactif sur les N derniers commits

git rebase --abort

Annuler un rebase en cours (revenir à l’état initial)

git rebase --continue

Continuer un rebase après résolution de conflits

git cherry-pick <hash>

Copier un commit spécifique sur la branche courante

git push --force-with-lease

Pousser en forçant (avec vérification de sécurité)

Remarque 27

Le rebase est un outil puissant qui, bien utilisé, produit un historique propre et lisible. La clé est de respecter la règle d’or : ne jamais réécrire l’historique partagé. Pour le travail local, le rebase (et particulièrement le rebase interactif) est un allié indispensable. Maîtriser la distinction entre merge et rebase, et savoir quand utiliser l’un ou l’autre, est l’une des compétences les plus précieuses pour tout utilisateur de Git.