Découper un commit « fourre-tout »

Par Maxime Bréhin • Publié le 15 janvier 2025 • 4 min

Dans un souci de qualité projet, on s’attelle généralement a produire des commits atomiques, traitant des contenus cohérents. Nous restons cependant capables d’erreurs et il arrive que certains commits embarquent des sujets mêlés. On peut même occasionnellement vouloir/devoir reprendre le travail de certain·e·s de nos collègues. Comment faire alors pour être jusqu’au-boutiste et découper ce type de commits en plusieurs commits distincts ?

Cet article décrit la procédure pour les 2 cas de figures auxquels on peut être confronté selon qu’on souhaite découper :

  1. le dernier commit réalisé ;
  2. un commit plus ancien (qui a été suivi par d’autres commits).

Découper le dernier commit

Pour effectuer cette opération, nous allons procéder en 2 étapes :

  1. défaire le commit
  2. créer les nouveaux commits.

Pour notre exemple, partons de l’historique initial suivant où id5 est le dernier commit, celui à découper :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3"
    commit id: "id4"
    commit id: "(HEAD) id5" type: HIGHLIGHT

1. Défaire

Si tu as déjà lu notre article “Défaire un commit”, tu connais la marche à suivre pour la première opération. On fait un pas en arrière (ci-après la notation HEAD~1) du point de vue de l’historique, sans toucher à l’état des fichiers dans notre répertoire de travail :

git reset --mixed HEAD~1

On vérifie que l’historique a bien été mis à jour (par exemple avec l’alias git lg) :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3"
    commit id: "(HEAD) id4"

On vérifie également l’état de nos fichiers (git status) : on doit retrouver les contenus du commits comme étant “modifiés”, disponible pour l’ajout au stage.

2. Découper

Il ne nous reste plus qu’à ajouter la première partie des modifications qui nous intéressent, de commiter une première fois, puis de faire la même chose avec le ou les lots restants.

git add <premier-lot>
git commit -m 'Message du premier lot'
git add <second-lot>
git commit -m 'Message du second lot'

Évidemment, une dernière vérification de l’historique s’impose (nos commits ici ont les identifiants id6 et id7) :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3"
    commit id: "id4"
    commit id: "id6"
    commit id: "(HEAD) id7"

On peut également doubler cette vérification avec la vérification des contenus des commits :

git show id6
# Affichage du message et des modifications du commit id6
git show id7
# Affichage du message et des modifications du commit id7

Découper un commit plus ancien

La méthodologie pour cette situation demande des étapes supplémentaires qui s’appliquent autour de la première méthodologie que nous venons de voir. Voic les étapes :

  1. On demande à se replacer dans l’historique au niveau du commit à modifier ;
  2. On réalise nos opérations comme vu dans le premier cas de figure ;
  3. Une fois notre découpage terminé, on demande à Git de rejouer le reste des commits qui suivaient le commit découpé.

Je tiens à préciser ce dernier point : notre commit initial ne faisant plus partie de l’historique, on obtient 2 nouveaux commits avec des nouveaux identifiants. L’identité des commits étant construite en partie sur l’identité des commits parents, ceci implique que les commits qui suivaient le commit initial doivent être comme “copiés/collés” les uns après les autres depuis nos nouveaux commits, obtenant ainsi de nouvelles identités. C’est ici le rôle de la commande rebase.

Dans l’historique suivant, nous cherchons à découper le commit id3 :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3" type: HIGHLIGHT
    commit id: "id4"
    commit id: "(HEAD) id5"

1. Se replacer dans l’historique

Pour nous replacer sans annuler ou “perdre” les commits qui suivent (id4 et id5) nous ne pouvons pas utiliser la commande reset. Nous utilisons à la place la commande rebase et son mode intéractif en lui désignant le commit précédent celui ciblé. Ce commit servira de point de départ à nos opérations. Il s’agit ici du commit id2.

git rebase -r -i id2

Git nous ouvre alors notre éditeur ou l’interface qu’on aura configuré et nous propose d’éditer les actions à mener depuis id2 :

…
pick id3 3e commit # (C’est celui-ci qu’on veut découper)
pick id4 4e commit
pick id5 5e commit
…

On va alors changer la ligne de l’id3 l’action pick en edit sans toucher au reste :

…
edit id3 3e commit # (on a passé le premier mot de la ligne à "edit")
pick id4 4e commit
pick id5 5e commit
…

On enregistre et on ferme l’onglet ou l’éditeur.

Git exécute alors les opérations comme on lui a demandé et se stoppe directement après la première étape. Il nous donne comme souvent des indications précieuses sur la situation actuelle et la marche à suivre.

Si on regarde notre historique, on est donc positionné sur id3 :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "(HEAD - detached) id3"

2. Découper

On procède exactement comme pour la première procédure : git reset --mixed HEAD~1 + découpe/commits.

On obtient alors un historique partiellement à jour (id3-1 et id3-2 remplacent ici id3) auquel il manque la suite des commits :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3-1"
    commit id: "(HEAD - detached) id3-2"

Notre découpe terminée, il ne nous reste plus qu’à demander à Git de finaliser l’opération de rebase :

git rebase --continue

Si on est dans le terminal, différentes informations sont affichées et décrivent l’application des commits qui correspondaient à id4 et id5 :

%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%%
  gitGraph
    commit id: "id1"
    commit id: "id2"
    commit id: "id3-1"
    commit id: "id3-2"
    commit id: "id4’"
    commit id: "(HEAD) id5’"

Et voilà, le tour est joué !

Tu veux aller plus loin et maîtriser pleinement les fondamentaux de Git ou être accompagné pour garantir la qualité de tes projets grâce à une bonne mise en place de Git ? On peut t’aider ou te former, il suffit de nous décrire ton besoin !
Tu peux aussi regarder le programme de notre formation "Comprendre Git" ou nous poser tes questions sur notre forum discord.