git rebase --onto ressemble à une variante compliquée de git rebase. Je l’ai longtemps évité pour cette raison. Pourtant, cette option répond à une question très précise : comment déplacer seulement une partie d’une branche vers une nouvelle base, sans embarquer les commits qui se trouvent en dessous ?

C’est le cas dès qu’une branche a été créée depuis la mauvaise branche, qu’une pile de branches doit être réorganisée, ou que quelques commits utiles sont coincés au milieu d’un historique. Un rebase classique choisit implicitement quoi reprendre. Avec --onto, on définit explicitement la nouvelle base, la limite des commits à exclure et la branche à déplacer.

Pourquoi un rebase classique ne suffit pas toujours

Prenons deux branches de fonctionnalité empilées. feature/ui a été créée au-dessus de feature/auth parce que l’interface dépendait initialement de l’authentification :

A---B                    main
     \
      C---D              feature/auth
           \
            E---F        feature/ui

Les commits E et F deviennent finalement indépendants. On veut ouvrir leur pull request sans les commits C et D de l’authentification.

Avec ce graphe précis, un git rebase main lancé depuis feature/ui ne rejoue aucun commit. main pointe toujours sur B, qui est déjà un ancêtre de feature/ui. Git répond donc que la branche est à jour et laisse son sommet sur F. La commande ne détache pas feature/ui de feature/auth.

Si main avait avancé entre-temps, le comportement serait différent :

A---B---G---H            main
     \
      C---D              feature/auth
           \
            E---F        feature/ui

Dans ce second graphe, git rebase main sélectionnerait bien C, D, E et F, car ces quatre commits sont absents de main, puis les rejouerait après H :

      C---D                      feature/auth
     /
A---B---G---H                    main
             \
              C'---D'---E'---F' feature/ui

L’historique changerait alors de place, mais la dépendance à feature/auth resterait présente. Dans les deux situations, branche principale inchangée ou avancée, le rebase classique ne sait pas sélectionner seulement E et F.

Le besoin réel est plus précis : prendre les commits après feature/auth, donc E et F, puis les rejouer sur main. C’est exactement le rôle de --onto.

git switch feature/ui
git rebase --onto main feature/auth

Le résultat devient :

      C---D              feature/auth
     /
A---B                    main
     \
      E'---F'            feature/ui

Les apostrophes sont importantes. E' et F' contiennent les mêmes changements que E et F, mais ce sont de nouveaux commits avec de nouveaux identifiants. Un rebase réécrit l’historique, il ne déplace pas physiquement les commits existants.

La formule à retenir

La forme complète est :

git rebase --onto <nouvelle-base> <frontiere> <branche>

On peut la lire ainsi :

Prends les commits accessibles depuis <branche> mais pas depuis <frontiere>, rejoue-les sur <nouvelle-base>, puis déplace <branche> sur le résultat.

Autrement dit, Git sélectionne en première approximation la plage suivante :

<frontiere>..<branche>

Puis il l’applique sur <nouvelle-base>. Cette distinction est le vrai intérêt de --onto : la frontière qui détermine quoi rejouer peut être différente de la base qui détermine où le rejouer.

Dans notre exemple :

git rebase --onto main feature/auth feature/ui
  • main est la destination ;
  • feature/auth est la frontière exclue ;
  • feature/ui est la branche déplacée.

Le troisième argument est optionnel. Sans lui, Git utilise la branche courante. Je préfère l’écrire dans un script ou une commande copiée dans une documentation, car l’intention reste visible sans dépendre du HEAD courant.

La documentation officielle de git rebase formule --onto comme le point de départ sur lequel créer les nouveaux commits. Le mot « frontière » n’est pas un terme Git officiel, mais c’est le modèle mental le plus utile pour éviter d’inverser les arguments.

Cas 1 : détacher une branche d’une pile

C’est le cas précédent, et probablement le plus fréquent dans une équipe qui utilise des branches empilées.

main:          A---B
feature/auth:       \---C---D
feature/ui:                  \---E---F

Pour détacher feature/ui de feature/auth :

git rebase --onto main feature/auth feature/ui

La plage feature/auth..feature/ui contient seulement E et F. Git les rejoue sur main, puis met à jour feature/ui.

Cette opération est utile quand deux pull requests ont été empilées pour avancer plus vite, puis que la seconde devient mergeable indépendamment. Elle évite de fermer la branche, de recréer une branche depuis main et de sélectionner les commits un par un avec cherry-pick.

Cas 2 : réparer une branche créée depuis la mauvaise base

Une branche feature/export devait partir de main, mais elle a été créée depuis release/1.x :

A---B---C                    main
     \
      R1---R2               release/1.x
             \
              X---Y         feature/export

On veut conserver X et Y, sans transporter R1 et R2 sur main :

git rebase --onto main release/1.x feature/export

La plage release/1.x..feature/export isole X et Y. Après le rebase :

A---B---C---X'---Y'         feature/export
     \
      R1---R2               release/1.x

Il faut toutefois choisir une frontière qui correspond réellement au point sous lequel on veut couper. Si release/1.x a reçu de nouveaux commits depuis la création de feature/export, son nom peut ne plus représenter cette limite. Dans ce cas, utilisez le commit de divergence explicite ou vérifiez-le avec :

git merge-base release/1.x feature/export

Une référence de branche est pratique, mais un identifiant de commit est parfois plus exact.

Cas 3 : extraire les derniers commits vers une autre base

Supposons qu’une branche contienne cinq commits, mais que les trois derniers forment une correction autonome à livrer sur release/2.x :

A---B                         release/2.x
     \
      C---D---E---F---G       feature/payments

Depuis feature/payments, la frontière HEAD~3 désigne le parent des trois commits à reprendre :

git switch feature/payments
git rebase --onto release/2.x HEAD~3

Git rejoue E, F et G sur release/2.x. La branche feature/payments pointe ensuite sur cette nouvelle série.

Ce cas est proche d’un cherry-pick de plusieurs commits, avec une différence d’intention. cherry-pick copie des commits sur la branche courante sans déplacer la branche source. rebase --onto transplante une plage et met à jour la branche rebased. Si l’objectif est de conserver la branche d’origine intacte, créez d’abord une nouvelle branche ou utilisez cherry-pick.

Cas 4 : retirer une plage de commits au milieu

--onto peut aussi supprimer plusieurs commits contigus d’une branche sans ouvrir un rebase interactif.

A---B---C---D---E            topic

Imaginons que C et D soient des essais à retirer, tandis que E doit être conservé. Il faut rejouer tout ce qui vient après D sur B :

git rebase --onto B D topic

Le résultat est :

A---B---E'                   topic

La commande ne dit pas directement « supprime C et D ». Elle dit « prends D..topic et rejoue cette plage sur B ». Comme seul E appartient à cette plage, C et D ne font plus partie de la nouvelle histoire de topic.

Pour retirer des commits dispersés, réordonner des commits ou modifier leurs messages, git rebase -i reste plus lisible. --onto est particulièrement adapté quand la coupure forme une plage continue et que ses deux bornes sont claires.

Vérifier la sélection avant de réécrire

La difficulté de --onto n’est pas la destination. C’est la sélection. Avant d’exécuter la commande, j’affiche toujours la plage que Git doit rejouer :

git log --oneline --reverse feature/auth..feature/ui

Si la sortie contient E et F, et rien d’autre, la frontière est correcte. On peut ensuite visualiser l’ensemble du graphe :

git log --oneline --graph --decorate --all

Je crée aussi une référence de secours avant une réécriture importante :

git branch backup/feature-ui feature/ui
git rebase --onto main feature/auth feature/ui

Cette branche ne copie pas les fichiers. Elle conserve simplement un pointeur vers l’ancien sommet, ce qui suffit pour comparer ou revenir en arrière.

Après l’opération, git range-diff permet de comparer les deux séries de patches plutôt que leurs identifiants :

git range-diff feature/auth..backup/feature-ui main..feature/ui

On s’attend à retrouver les mêmes changements, avec de nouveaux commits et éventuellement des résolutions de conflits.

Gérer les conflits et revenir en arrière

Comme tout rebase, --onto rejoue les commits un par un. Un conflit peut donc apparaître si la nouvelle base a modifié les mêmes lignes.

Après avoir résolu les fichiers :

git add <fichiers-resolus>
git rebase --continue

Pour abandonner l’opération en cours et retrouver l’état initial :

git rebase --abort

Si le rebase est déjà terminé, la branche de secours reste la récupération la plus simple. Sans elle, git reflog permet généralement de retrouver l’ancien sommet de la branche :

git reflog feature/ui

Il faut aussi partir d’un répertoire de travail propre. Un rebase et des modifications non committées forment un mélange inutilement risqué. --autostash existe, mais je préfère un commit temporaire ou un stash explicite dont la présence ne dépend pas d’une option oubliée.

Le coût réel : les commits changent d’identité

Rejouer un commit sur une autre base produit un nouveau commit, même si son diff reste identique. Les identifiants changent, et tous les descendants changent avec eux.

Sur une branche personnelle, c’est généralement ce que l’on veut. Sur une branche déjà partagée, il faut prévenir les autres contributeurs. Leur historique local pointe encore vers les anciens commits.

Si la branche distante doit être mise à jour, utilisez :

git push --force-with-lease origin feature/ui

--force-with-lease refuse l’écrasement si la branche distante a avancé d’une manière que votre dépôt local ne connaît pas. Ce garde-fou ne transforme pas une réécriture publique en opération anodine, mais il évite d’effacer silencieusement le travail poussé par quelqu’un d’autre.

Autre limite, un rebase classique ne conserve pas automatiquement la topologie des commits de merge. Si la branche contient des merges qui portent une information utile, étudiez --rebase-merges et vérifiez soigneusement le graphe obtenu.

Quand utiliser –onto, et quand choisir autre chose

BesoinCommande adaptée
Mettre tous les commits d’une branche à jour sur maingit rebase main
Déplacer seulement les commits après une frontièregit rebase --onto
Copier quelques commits sans déplacer leur branche sourcegit cherry-pick
Réordonner, fusionner ou modifier des commitsgit rebase -i
Annuler un changement sans réécrire un historique partagégit revert

--onto est donc moins une version avancée de rebase qu’un outil de sélection. Il devient pertinent dès que la base de destination n’est pas la même référence que la frontière de sélection.

Cette précision compte particulièrement quand Git sert de source de vérité pour le déploiement, comme je l’explique dans GitOps sans Kubernetes : Django avec doco-cd, SOPS et age. Un historique lisible permet de savoir quels changements ont réellement atteint une branche de livraison, sans traîner des commits issus d’une dépendance temporaire.

Conclusion

Le moyen le plus fiable de comprendre git rebase --onto tient en une phrase : rejouer <frontiere>..<branche> sur <nouvelle-base>.

À partir de ce modèle, les principaux cas deviennent mécaniques : détacher une branche empilée, corriger une mauvaise base, transplanter les derniers commits ou retirer une plage intermédiaire. La commande reste puissante parce qu’elle réécrit l’historique. Affichez la plage avant de lancer le rebase, gardez une branche de secours, comparez les patches après l’opération et réservez la réécriture aux branches dont vous maîtrisez le partage.