Branches et rebase

Réordonner des commits Git avec un rebase interactif

Réordonnez des commits Git avec un rebase interactif : échangez des commits indépendants, traitez un conflit, vérifiez l’arbre final.

Sommaire

Vérifier les dépendances entre commits

Git permet de réordonner des commits avec un rebase interactif, et déplacer une ligne est facile. Le faire sans rien casser dépend d’une question : un commit s’appuie-t-il sur les lignes d’un autre ? Deux commits qui touchent des fichiers différents peuvent presque toujours être échangés. Deux commits qui touchent les mêmes lignes, non.

L’exemple est un dépôt jetable, orbit-api. La branche feature/retry porte trois commits au-dessus de main. Cette commande liste les fichiers que chacun touche, du plus ancien au plus récent :

git log --reverse --oneline --decorate --name-only main..
8864acb docs: describe the retry option
README.md
6298bfd feat: add retry option
src/config.ts
ab7f160 (HEAD -> feature/retry) feat: raise retries to 5
src/config.ts

Le commit de documentation ne touche que README.md : il est indépendant. Les deux derniers modifient tous deux src/config.ts, et le troisième change la ligne ajoutée par le deuxième. Git ne vérifie que le texte, pas le sens : un commit qui appelle une fonction ajoutée par un commit précédent dans un autre fichier serait réordonné sans aucun conflit et laisserait un commit cassé entre les deux. Lisez les commits, pas seulement les noms de fichiers.

Avant de commencer, laissez un repère sur le sommet actuel pour pouvoir comparer et revenir en arrière. Vos identifiants seront différents de ceux ci-dessous.

git branch start

Modifier l’ordre dans le rebase interactif

Lancez un rebase interactif depuis main. Le guide du rebase interactif détaille l’éditeur :

git rebase -i main

Git liste les commits du plus ancien au plus récent. Dans l’exemple, la documentation décrit l’option de nouvelle tentative : elle doit venir après le commit qui l’ajoute. Déplacez la ligne docs sous la ligne feat, comme Pro Git le montre pour le réordonnancement :

pick 6298bfd feat: add retry option
pick 8864acb docs: describe the retry option
pick ab7f160 feat: raise retries to 5

Après l’enregistrement, Git rejoue les commits dans cet ordre et affiche Successfully rebased and updated refs/heads/feature/retry. Les commits ont maintenant de nouveaux identifiants :

Commit Avant Après
feat: add retry option 6298bfd bde2a75
docs: describe the retry option 8864acb cc83efc
feat: raise retries to 5 ab7f160 ef3db49

Pourquoi le dernier a-t-il changé alors qu’il n’a pas bougé ? L’identifiant d’un commit couvre l’identifiant de son parent. Le troisième commit repose maintenant sur un autre parent : il reçoit un nouvel identifiant, bien que ses changements soient les mêmes. Tout commit situé entre le premier déplacé et le sommet est réécrit.

Dans GitMini, ouvrez le panneau Interactive rebase : avec feature/retry comme branche courante, faites un clic droit sur main et choisissez Interactive rebase of “feature/retry” on “main”…. Faites glisser un commit par la poignée à gauche de sa ligne, ou placez le focus sur une ligne ou sa poignée et appuyez sur Alt+↑ ou Alt+↓ (pas dans le menu d’action ni dans un champ de texte), puis cliquez sur Launch rebase.

Panneau Interactive rebase de GitMini avec une poignée de déplacement à gauche de chaque ligne de commit
La poignée à gauche de chaque ligne déplace un commit dans la liste. Rien ne change dans le dépôt avant le clic sur Launch rebase.

Traiter un conflit ou revenir au départ

Voici maintenant le cas dépendant. Passez sur la branche repère avec git checkout start, pour laisser intacte la branche réordonnée, relancez git rebase -i main et échangez les deux derniers commits pour que feat: raise retries to 5 passe avant le commit qui ajoute la ligne retries :

pick 8864acb docs: describe the retry option
pick ab7f160 feat: raise retries to 5
pick 6298bfd feat: add retry option

Git s’arrête sur le commit qui ne s’applique plus :

Auto-merging src/config.ts
CONFLICT (content): Merge conflict in src/config.ts
error: could not apply ab7f160... feat: raise retries to 5
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply ab7f160... feat: raise retries to 5

Ces sorties viennent de Git 2.30 ; Git 2.54 commence les lignes de conseil par hint: et écrit chaque sujet de commit après un #, dans la dernière ligne et dans la liste. Le fichier explique pourquoi. La branche en cours de reconstruction n’a pas encore de ligne retries, car le commit qui l’ajoute vient désormais plus tard :

export const timeoutMs = 1000;
<<<<<<< HEAD
=======
export const retries = 5;
>>>>>>> ab7f160 (feat: raise retries to 5)

Je l’ai résolu en gardant retries = 5, j’ai indexé le fichier et lancé git rebase --continue. Git s’est arrêté de nouveau sur feat: add retry option, avec retries = 5 d’un côté et retries = 3 de l’autre. Deux commits qui entrent sans cesse en conflit sont dépendants, et un échange est le mauvais outil. Vous avez trois options : résoudre chaque arrêt et continuer, git rebase --skip pour laisser de côté le commit en cours, ou renoncer :

git rebase --abort
git rev-parse --short HEAD
ab7f160

L’abandon a restauré la branche exactement : ab7f160 est de nouveau le sommet d’origine. La documentation de git-rebase décrit --abort comme la remise de HEAD là où il était au début du rebase.

GitMini gère l’arrêt avec un bandeau. Après le clic sur Launch rebase, le panneau se ferme et le bandeau affiche « conflict by applying … » avec le nombre de fichiers en conflit. Continue reste désactivé tant que vous n’avez pas résolu et indexé les fichiers. Skip laisse de côté le commit. Abort demande « Back to status before the rebase? Ongoing resolutions will be lost. » puis restaure la branche.

Contrôler l’arbre final et le graphe

Revenons à l’échange indépendant (git checkout feature/retry) et vérifions deux choses. D’abord l’historique : avec --all, le repère montre l’ancienne ligne à côté de la nouvelle.

git log --oneline --graph --decorate --all
* ef3db49 (HEAD -> feature/retry) feat: raise retries to 5
* cc83efc docs: describe the retry option
* bde2a75 feat: add retry option
| * ab7f160 (start) feat: raise retries to 5
| * 6298bfd feat: add retry option
| * 8864acb docs: describe the retry option
|/
* 4eabd99 (main) chore: init orbit-api

Ensuite les fichiers. Un simple réordonnancement ne doit pas changer le contenu final : les empreintes d’arbre des deux sommets doivent donc être égales.

git rev-parse start^{tree} HEAD^{tree}
git diff start HEAD
5be1b584768d24c1e7f4ffc6c97dd1b56a6c03fb
5be1b584768d24c1e7f4ffc6c97dd1b56a6c03fb

Les empreintes sont identiques et git diff n’affiche rien. Si vous aviez résolu un conflit, le diff montrerait ce que la résolution a changé. Pour lire le graphe dans GitMini, voyez le guide du graphe des commits. Si la branche est déjà sur un dépôt distant, rappelez-vous que la réécriture exige un push forcé, ce que le guide du rebase interactif explique.

Sources