Aller au contenu

Git reset : rien ne se perd, tout se transforme

Par Maxime Bréhin

Mini-conf (30 mn) :
Langue :
Français
CC

Liens connexes

Le sujet

Git est un outil incontournable qui apparait parfois trop complexe, surtout quand on a pas lu la documentation. Il nous promet de pouvoir triturer et tordre notre historique de projet dans tous les sens. Sur le papier c'est génial, mais c'est tout aussi effrayant car on a vite peur de perdre du travail. Heureusement la commande `reset` associée souvent au `reflog` vole à notre secours.

On va dédiaboliser une bonne fois Git et voir qu'on a ceinture, bretelles et airbag. On en profitera aussi pour arrêter cette fausse bonne pratique du `git reset --hard` que beaucoup utilisent sans avoir réellement conscience du danger inhérent.

Présenté par

Transcription

Salut les gens !

Je suis hyper content d’être à ParisWeb et surtout je suis hyper content que vous soyez là, parce que sinon, bah je parlerai devant personne, et ces derniers temps ça m'arrive un peu trop souvent… si on a le temps à la fin je vous dirai peut être ce que je veux dire par là.

Quoi qu’il en soit je suis là parce que mon boss Christophe m'a dit : "Ça serait bien que tu fasses un stolz à ParisWeb cette année…", à ce moment là j'ai pensé que j'allais devoir faire une sorte de patisserie allemande pour l’évènement, vu que je fais déjà des cookies au boulot. Mais quand il m'a vu chercher stolz sur le net il m'a dit : "Non, un talk". C’est là que j'ai compris qu’il avait dû roter en le disant la première fois. Bref, on voit ça ensemble et il me dit que ça serait bien que tu fasses un sujet sur un truc que les gens ne comprenent pas !".

J'ai alors essayé de me mettre à votre place et la première idée qui m’est venue c’était « Mais qu’est ce que je fou là ? ».

Mais je n'étais pas certain que ça matche avec l’esprit ParisWeb, alors je me suis rabattu sur un truc que tout le monde utilise sans avoir la moindre idée de comment ça marche, ni pourquoi… Git ! Sauf que là, trop de choses à dire en 25 minutes, alors j’ai trouvé le sujet qui, je l'espère, vous a fait venir, si ce n'est pas le manque de place dans la conf' voisine.

Git reset !

Je commence avec un petit sondage à mains levées.

Qui parmi vous utilise git reset ?

OK, gardez la main levée, que je vois l’évolution avec la seconde question : qui parmi vous connait git reset ? Ou plus exactement qui sait ce que fait vraiment la commande et comment elle le fait ? Ou si vous voulez, qui a lu la doc’ et ne l’utilise pas suite à un copier/coller stackoverflow ?

OK, eh bien pour les gens qui ont eu l’honnêteté d’avouer qu’ils n’y connaissent rien, je risque de vous apprendre pas mal de choses… et pour les gens qui connaissent déjà, bah, je risque de vous apprendre pas mal de choses aussi ! Ahah !

Allez, c’est parti !

On commence avec quelques rappels pour que tout le monde puisse comprendre à peu près ce que je dis…

Première chose, le vocabulaire, le truc qui est toujours mieux venu au début de la présentation qu’à la fin.

Donc dans cette présentation je vais vous parler des zones Git, qui sont au nombre total de 5, même si je ne parlerai ici que des 3 principales.

La première c’est la copie de travail, donc la partie de l’arbo dans laquelle on bosse et qu’on aurait même sans que ça soit un projet Git.

La deuxième, c’est l’index, appelé aussi stage, staging area, cache… bref, tout plein de noms pour désigner cette zone tampon qui nous sert à préparer nos commits. Je n’ai pas le temps d’en dire plus sur cette zone, mais si tu ne comprends pas l’intérêt, tu as raté un élément clé de Git, et ça veut aussi probablement dire que tu bosses comme un goret.

La dernière zone, c’est le dépôt local, donc en quelque sorte la base de données Git sur ta machine qui te sert à stocker ton historique de projet.

Côté dépôt local, Git stocke une référence de notre emplacement courant dans un truc appelé le HEAD. D’ailleurs ce nom est plutôt bien trouvé car on se déplace rarement sans sa tête ?

Un autre truc plutôt utile est le log ou la visualisation de notre historique de projet avec le branches, tout ça tout ça.

On a ensuite le reflog, ou la journalisation des déplacements successifs de HEAD et des étiquettes de branches. Ça claque comme phrase, même si vous n'y avez rien compris ? Donc rapidement, le reflog c’est un mécanisme incontournable pour nous permettre de revenir en arrière, ou en avant, quand on a fait n’imp’ avec notre historique.

Et pour finir on a le garbage collector. En réalité j'ai mis ça surtout pour parler du fait que dans Git on ne supprime rien nous même, c’est le GC qui s’en charge de son côté après un certain temps, ce qui veut dire qu’on peut retrouver des commits qu’on pense avoir supprimé tant que le GC n’est pas passé.

Étapes d’un commit à travers les zones

Alors comment ça se passe la création d'un commit avec cette histoire de zones.

J’étais parti pour vous mettre des flammes, des feux d’artifices et toute la panoplie d’animations sensationnelles, mais on m’a dit que c’était nul et qu’avec des épileptiques dans la salle, je risquais de finir en prison. Du coup je suis reparti sur qqch de sobre, et j'en suis vraiment navré !

La rien de bien magique, pour faire mon commit je fais mes modifs ou je créé des fichiers dans ma copie de travail. J’ajoute ce que je veux à l’index. Et je valide tout ça pour enregistrer dans le dépôt local.

V’l’à l’anim de ouf !

Étapes d’un commit entre zones et log

Maintenant ça donne quoi si on regarde le log en parallèle.

On commence avec le premier commit, donc les modifs, l'ajout à l'index, puis l'ajout au dépôt local. Là on travaille sur une branche dev. J'étais parti pour utiliser master, mais ça ne rentrait pas dans la largeur.

Quoi qu’il en soit dans mon log on voit bien mon commit c1, pointé par l'étiquette de branche dev, pointée elle-même par HEAD.

On fait ensuite notre second commit. On voit dans le log que c2 référence c1, son commit parent, et que dev a bougé et pointe sur c2. Bon là mon anim’ a montré HEAD qui bougeait, mais en réalité HEAD n’a pas changé, il pointe toujours sur dev.

Allez, encore un commit pour que vous compreniez bien… Donc tout pareil, c3 pointe sur c2, dev pointe sur c3 et HEAD sur dev.

Voilà pour les rappels. Si là tu es largué, bah… bah ça craint quand même… mais l’avantage avec ParisWeb, c’est que c’est filmé, et que tu pourras revoir tout ça plus tard ! C’est beau la magie de la technologie et du partage !

Git « unstage », cas particulier de git reset

Allez, on attaque le vif du sujet avec reset et son premier cas d'application, pour défaire les fichiers ajoutés à l'index.

C’est un cas particulier et qui génère de la confusion quand on essaye d'apprendre la commande car c’est le seul mode qui permette d'agir sur des fichiers et qui fait l’équivalent d’une désindexation.

Son rôle est donc de défaire un git add… pour les fichiers ciblés.

Donc quand je ne veux pas de certains fichiers dans mon commit, je les retire de l’index.

Allez, on voit ça avec une nouvelle animation…

Git reset sur fichiers

Dans ma copie de travail j'ai plusieurs fichiers modifiés ou créés et que j'ai ajouté à l'index. Je m'aperçois que les fichiers A et C ne conviennent pas pour mon commit, du coup je fais un git reset des fichiers.

Suite à ça mon index ne contient plus de photos des modifs de A et C, et par contre ma copie de travail les contient toujours.

Git restore, depuis Git 2.23

Je fais une parenthèse flash éclair car depuis Git 2.23 sortie en août de cette année on a une nouvelle commande git restore qui fait pareil et qui a le mérite de désambiguiser les opérations puisqu’elle porte un plus joli nom.

Donc si on veut faire la même chose que précédemment suite à des modifs des fichiers A et C on peut faire un git restore --staged A C et ça fera strictement pareil que reset.

Et pour les autres cas ? (Reset, mais pas sur des fichiers)

Comme je l'ai dit, ce qu’on vient de voir correspond à un cas particulier d'utilisation de reset.

Je vous montre donc à quoi sert reset le reste du temps.

Sur le principe, ça nous permet de déplacer HEAD à l'emplacement voulu. Ça fait qu'on peut modifier en quelques sorte notre historique et repositionnant HEAD sur le commit qui nous intéresse. Le cas d’utilisation typique, c’est pour défaire le dernier commit.

Ensuite, on va voir qu’il existe plusieurs modes, et que selon celui qu’on utilise, ça aura une incidence sur nos zones.

Effet des modes sur les zones

Je vous ai fait un petit tableau pour vous aider à comprendre, ou pas…

On a donc 5 modes : --soft, --mixed, --keep, --merge et --hard.

Je les ai renseigné selon leur force, ou pour être plus exact selon leur impact sur les zones.

Donc si on lit ce tableau, on constate qu’ils déplacent tous HEAD. Bon en même temps c’est ce que je disais dans la slide précédente.

Le mode --soft ne fait d'ailleurs que ça. Il ne touche pas à l'index, ni à la copie de travail.

Le mode --mixed, qui est le mode par défaut, donc qu'on a pas besoin d'écrire explicitement, déplace HEAD et défait l’index, mais sans toucher à la copie de travail.

Enfin on a un brainfuck sur la dernière ligne avec 3 modes qui agissent pareil sur les zones. C’est-à-dire que les modes --keep, --merge et --hard vont déplacer HEAD et ramener l’index et la copie de travail à l’état du commit désigné.

C’est très théorique pour l’instant, mais rassurez-vous on voit bientôt les cas d'application.

On va déjà essayer de comprendre mieux comment agissent ces modes sur les zones.

git reset --soft

Pour les démos qui suivent on part du principe qu’on a cet historique de 3 commits et qu’on veut défaire les commits c2 et c3. Ça équivaut donc à repositionner HEAD en c1 et donc d’oublier, de déréférencer c2 et c3.

On commence avec le mode --soft. Donc si je fais un git reset --soft c1, on voit à droite dans le log que HEAD référence bien c1 et c2 et c3 sont grisés car ils sont déréférencés.

Du côté des zones que s’est-il passé ?

On voit que les commits 2 et 3 sont grisés, mais que leurs photos sont préservées dans l'index et les modifs équivalentes sont préservées dans la copie de travail.

Ça veut donc dire qu’on a défait notre historique tout en conservant le travail.

git reset --mixed

Même exemple donc avec le mode --mixed.

Côté log, tous les modes feront la même chose, à savoir ramener HEAD en c1.

Par contre on voit que le mode --mixed a défait plus que les commits dans le dépôt local, il a aussi défait les photos dans l’index.

Par contre il a préservé les modifs dans la copie de travail. Donc a peu de choses près les modes --mixed et --soft font la même chose, à savoir défaire l'historique tout en conservant les modifs associées.

Différences entre mixed, merge et hard

Il nous reste donc ces 3 modes --keep, --merge et --hard. Du coup j'ai refait un tableau dédié, et là je suis certain que ça vous aide plus ! Non ?

Bon déjà on comprend que tous les 3 ils pètent la tronche à toutes les zones, donc ils défont l'historique sans préserver le travail des commits. Vous me suivez ?

Par contre ils ont quand même une différence. Ah oui, tout de suite avec des petites étoiles on voit mieux la différence… non, toujours pas ?

Bon, ces petites étoiles précisent un point important : si on a du travail en cours dans notre copie de travail ou l'index et qui n'a pas été commité, et bien, selon les mode utilisé, ce travail pourra ou non être écrasé ou préservé.

Donc si on regarde la dernière ligne, le mode --hard, c’est le mode YOLO (You Only Live Once) !

Je reprends mes super schémas animés…

git reset --keep

Pour les démos des 3 modes on considérera qu'en plus de nos 3 commits on a du boulot de préparé dans la copie de travail et dans l’index.

Donc si je fais un git reset --keep c1, mon log a toujours la même tronche, par contre du côté des zones ça commence à être velu.

J'ai les commits c2 et c3 qui ont été défaits sur toutes la ligne. Donc même la copie de travail est impactée.

Par contre ce mode --keep il nous a préservé les modifs 4 et 5 dans la copie de travail, comme ça on ne perd pas le travail en cours par erreur.

Parce que, tu te rappelles ces histoires de commits déréférencés, bah on va voir qu'on peut les récupérer. Sauf que mon boulot 4 et 5, il n'a pas été committé, et du coup si je l'efface, je le paume pour de bon, sauf coup de bol et CTRL-Z chiatique dans mon éditeur.

git reset --merge

Même démo avec le mode --merge.

Il a le même objectif général que le mode --keep, à savoir éviter de perdre du travail en cours tout en voulant supprimer des commits de mon historique.

La différence importante est qu’une fois mon git reset --merge c1 effectué j’aurais bien défait les commits c2 et c3 à travers nos zones, mais il ne m'aura conservé que le travail en cours dans la copie de travail, pas celui que j'avais indexé.

Alors il y a des raisons et des usages à ça, mais si je prends un gros raccourci je dirai que si tu dois choisir entre --keep et --merge, mieux vaut préférer --keep qui sera plus conservateur.

git reset --hard

Pour finir, le mode bourrin, que tu as copié/collé depuis stackoverflow sans avoir la moindre idée de ce que tu faisais… allez, avoue ! Le mode --hard, surnommé l’effaceur.

Donc lui dégage tout sur son passage, pas de question ni de remord, il est là pour faire un gros ménage !

C’est bien beau tout ça, mais quels sont les cas pratiques ?

Je vous ai dit qu’on allait voir les cas pratiques.

Vu qu’il ne me reste quasi plus de temps, je vais tracer ma race, j’espère que vous êtes prêt, ça va aller très très vite, et désolé d'avance pour la LSF et la vélotypie…

Reset me permet de…

Reset nous permet de faire plein de trucs cools, comme - corriger ou compléter un commit - il nous permet de défaire des commits, soit pour les supprimer, soit pour les regrouper - ou bien encore il nous permet de créer une branche là où on aurait dû en créer une plus tôt - on peut aussi l'utiliser en mode bourrin pour supprimer toutes les modifs en cours - et le boss de la fin, c’est quand tu reset un reset.

Vous ne comprenez pas comment faire tout ça ? Ça tombe bien, j'ai des schémas pour ça…

J’ai commité trop vite

Premier cas classique, j'ai commité trop vite, mon contenu n'était pas vraiment prêt. Bah il suffit de défaire le git commit en gardant les modifs et de refaire plus tard un commit de remplacement.

Modifier mon dernier commit

Si je viens de créer le commit c3 mais que je veux l’enrichir, il me suffit de revenir en c2 avec un reset --soft ou --mixed, d'ajouter mes modifs à l’index, et de committer pour avoir un autre commit en remplacement. Le commit initial étant laissé de côté.

Pourquoi pas commit --amend ?

Comme je l'ai dit on peut souvent faire à avec un git commit --amend, mais ça n'est pas le sujet ici. Je passe donc à la suite.

Je veux regrouper des commits

Deuxième cas pratique : le regroupement des commits.

Si je m'aperçois que mes commits c2 et c3 traitent du même sujet, pourquoi ne pas les regrouper. En vrai on peut faire ça de plusieurs manières, j’en montre donc une seule ici.

Je reviens donc 2 crans en arrière, mais en gardant le contenus des modifs de c2 et c3 dans l’index, donc avec un reset --soft.

Et du coup mon index contient tout c2 et c3, je n'ai qu'à committer pour obtenir un nouveau commit qui contient la totale. Trop dur !

J’aurais DÛ faire une branche plus tôt

Là encore on est dans une approche méthodique. Si je m'aperçois que mes commits c2 et c3 auraient dû être sur une nouvelle branche et que master ne devrait pas les intégrer, il me suffit de créer une étiquette de branche à mon emplacement actuel, puis de ramener master en c1, et de me remettre sur ma branche de dev pour continuer mon travail.

Purger toutes les modifs en cours

On l'a vu avec reset --hard qui purge mes zones. Donc sur ce principe si je fais un reset --hard à l'emplacement de HEAD, bah je ne fais pas bouger HEAD dans mon historique, mais par contre je purge mon index et ma copie de travail.

Je vous refais une petite démo avec toujours mes commits c1, c2 et c3 et des modifs indéxées (modifs 4) et des modifs non indéxées (modifs 5).

Si je fais un git reset --hard à ma position actuel, donc HEAD, et bien je fais un tour sur moi-même, mais en écrasant au passage l’index et la copie de travail pour les remettre à l’état de HEAD.

Défaire c’est bien, mais peut-on refaire si on s’est trompé ?

Tout ça c’est bien, mais je fais quoi si j'ai fait un reset en passant une mauvaise référence, ou si je m'aperçois que finalement c’était mieux avant… Eh bah je vais resetter mon reset !

Le soucis quand j'ai défait des choses, c’est qu'en général ces choses ne sont plus référencées, ce qui veut dire que je ne peux plus y avoir accès via le log.

Heureusement pour ça on a un truc magique, le reflog. Bon en fait rien de magique là-dedans, le reflog c’est juste comme mon parcours GPS réel, il sait par où est passé ma tête, donc HEAD, mais aussi mes étiquettes de branches.

Du coup je peux utiliser les refs du reflog pour annuler mes annulation. C’est un comme retour vers le futur si tu vois ce que je veux dire.

log + reflog (depuis HEAD)

Un schéma à fond les ballons pour que tu comprennes la manière dont il se construit.

J'ai mon historique à gauche avec le log et mon reflog à droite.

Quand je fais des commits, on a une retranscription plutôt linéaire, parallèle.

Par contre dès que je fais d'autres actions qui touchent au HEAD, mon reflog prend une autre dimension.

Par exemple si je créé une branche et que je positionne HEAD dessus, le reflog enregistre ce déplacement.

Et ainsi de suite si je fais d'autres commits et déplacements entre mes branches.

log + reflog (depuis les têtes de branches)

Si on prend cette fois le même exemple mais qu'on regarde le reflog des branches, donc le déplacement des étiquettes de branches uniquement et non plus de HEAD, on voit que le résultat est différent.

On constate que le changement de branche, donc les checkouts, ne créent pas d’entrée dans le reflog, ce qui est normal car ces opérations représentent des déplacements de HEAD seulement.

Donc à vous de choisir selon le contexte quel affichage du reflog est le plus pertinent…

log + reflog : retour vers le futur

Pour finir, dans le cas d’un reset qui nous ramène en arrière dans notre historique, le reflog enregistre ce déplacement mais surtout il conserve la référence précédente. Et ici c'est c3, et pour y revenir je peux faire un git reset --keep master@{1}.

Ça marche avec tout !

Cette combinaison entre log et reflog fonctionne avec tout, ce qui veut dire que je peux défaire des commits, des resets, des merges, des rebases et tout ce qui touche à mon historique de commits.

Donc si on résume tout ça, ça veut dire qu’on ne perd jamais rien de ce qui a été commité.

Pour aller plus loin…

Si vous voulez en savoir plus, on a tout un tas d'articles sur notre site, et sinon on a lancé des cours vidéos sur Git parmis d’autres sujet, et dont certains cours sont gratuits.

Merci

Peut-être a-ton encore un peu de temps pour des questions…