Revenir en arrière avec Git
Si vous avez lu le tuto précédent, Git ne vous fait plus penser à une version en ligne de commande d’un écarteleur. Vous savez comment sauvegarder votre code dans Git, et comprenez comment ça marche, mais la partie la plus intéressante reste à venir.
Nous allons voir maintenant comment récupérer une ancienne version du code et comment se déplacer dans l’historique.
Pour faire ce tuto, vous devrez :
- Avoir fait le tuto précédent.
- Travailler sa souplesse d’esprit, car ce que l’on va voir n’a rien à voir avec ce que vous aviez probablement appris avec des outils type SVN.
Temps de lecture : 1h
Découvrez les branches
Hein ? Les branches ? Mais on ne devait pas parler de restauration de document avant de faire des branches ?
Rassurez vous, on ne vas pas créer ni changer de branche, on va juste expliquer ce que c’est.
De manière étonnante, on ne peut pas faire du travail sérieux dans Git sans comprendre comment fonctionnent les branches car :
- On travaille dès le début sur une branche.
- Les opérations de déplacement utilisent la notion de branche.
- Les branches ne sont probablement pas du tout ce que vous croyez.
En vérité, les branches ne sont pas des branches d’un arbre virtuel comme par exemple sur SVN. Elles sont de simples marqueurs, au même titre que HEAD mais avec une fonction différente. Ce sont des panneaux qui indiquent « cette partie du travail avance à partir d’ici ».
Par exemple, si vous travaillez sur une version Linux et une version Mac de votre programme, vous pouvez avoir avez deux branches, « linux-version » et « mac-version », ainsi disposées :
Attention cependant, une branche n’est pas une succession de changesets.
Ainsi, la divergence de l’historique qui inclut « Nouveau changeset » et « Finalement le TODO est bien comme cela » n’est pas la branche « linux-version ». C’est juste une succession de changesets. La branche « linux-version », c’est uniquement la partie verte sur le schéma, c’est un panneau qui pointe sur un changeset pour dire « la branche est ici ».
Ainsi, on peut très bien avoir deux branches sur un historique linéaire :
La branche n’a donc rien à voir avec les directions que prend l’historique, c’est uniquement un petit point dans l’historique. Du coup la métaphore de l’arbre en prend un coup avec Git
Autre chose : HEAD et les branches sont des concepts différents. HEAD, c’est le pointeur pour vous dire où VOUS êtes. Les branches, ce sont des pointeurs pour dire « ici se trouve une certaine version de mon code ».
Vous allez me dire que cela ressemble beaucoup au tags, et c’est vrai, mais la grosse différence est que les branches bougent. En particulier, si vous êtes sur une branche et que vous faites un commit, la branche se déplace sur le nouveau changeset avec vous.
Quand vous créez un dépôt Git, une branche est créée automatiquement et placée sur le premier changeset. C’est branche est appelée « master ». La branche « master » n’a rien de spéciale, c’est une branche comme une autre, mais elle existe depuis le début et porte toujours ce nom.
Ainsi, si vous avez gardé le dépôt que vous avons créé lors de notre dernier tuto, on se trouve dans la situation suivante :
Quand on est sur une branche et que l’on fait un commit, la branche est déplacée sur le nouveau changeset. Du coup, la branche « master » vous a suivi jusqu’ici. Cependant, vous devez comprendre que HEAD et la branche actuelle sont indépendants, et donc on peut bouger HEAD sans déplacer la branche.
Annuler un commit avec « git commit –amend »
Parfois vous ferez une sauvegarde dont vous ne voulez pas.
Dans ce cas, faites vos modifications telles que vous voudriez qu’elles soient, puis au lieu de faire « git commit », effectuez « git commit –amend » : le précédent commit est alors supprimé et remplacé par le précédent commit.
Par exemple, on ajoute « Super ! » dans text.txt, et on créé un fichier nommé proto.txt. Il reste aussi des modifications sur TODO.txt que nous n’avions pas enregistrées la dernière fois :
$ git status # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: TODO.txt # modified: test.txt # # Untracked files: # (use "git add <file>..." to include in what will be committed) # # proto.txt no changes added to commit (use "git add" and/or "git commit -a")
Ajoutons nos deux modifications, mais pas la création, puis commitons :
$ git add TODO.txt $ git add test.txt $ git commit -m "Je le sens pas ce commit" [master b415af6] Je le sens pas ce commit 2 files changed, 2 insertions(+), 0 deletions(-) $ git log commit b415af6c2e48a8f77fc12caea380f8f7e7345b81 Author: ksamuel <mail@e-vidence.net> Date: Mon Mar 29 16:55:10 2010 +0000 Je le sens pas ce commit commit 1a0633ce778044aadea23eb39cdc21fc6c949989 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 21:08:04 2010 +0000 On sauvegarde l'index dans l'historique […]
On s’apperçoit qu’on a oublié le fichier « proto.txt ». Si on fait un second commit, il sera pris en compte. Mais si quelqu’un récupère le code du précédent commit, il lui manquera ce fichier !
Annulons plutôt le précédent commit après avoir ajouté « proto.txt » :
$ git add proto.txt $ git commit --amend -m "Je savais bien que j'avais oublié quelque chose" [master a9c07b1] Je savais bien que j'avais oublié quelque chose 2 files changed, 2 insertions(+), 0 deletions(-) create mode 100644 proto.txt $ git log commit a9c07b154a6d2e3b2435fa51bbc3a920fb1652b2 Author: ksamuel <mail@e-vidence.net> Date: Mon Mar 29 16:55:10 2010 +0000 Je savais bien que j'avais oublié quelque chose commit 1a0633ce778044aadea23eb39cdc21fc6c949989 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 21:08:04 2010 +0000 On sauvegarde l'index dans l'historique […]
Nous avons donc fait :
Retourner à une version antérieur avec « git reset »
« git reset » permet de déplacer la branche actuelle dans l’historique. Par exemple, si vous êtes au niveau de « master », vous pouvez faire pointer « master » sur un autre changeset. Par défaut, « git reset » va aussi prendre tous les changements du changesets et le mettres dans l’index, comme ça seule votre copie de travail est en décalage avec le changeset et « git status » vous donne un résultat cohérent.
Le HEAD est déplacé en même tant que la branche actuelle. Après un « reset », les deux pointent sur le nouveau changeset.
Par exemple, supposons qu’on veuille replacer la branche master à l’époque de l’ajout du .gitignore :
$git log [...] commit 9f2f748ecb853a1cb616b6d387f7169dac32f243 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 19:12:18 2010 +0000 Ajout d'un fichier et suppression d'un autre commit e3bfc5d3457756d695bbed735ea018559d8b00cd Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:52:12 2010 +0000 Ajout du .gitignore commit bbfc3cf5ef3f93c4aab17b39193907a047b5617b Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:34:49 2010 +0000 Ceci est un commentaire
On voit que le hash de ce changeset est e3bfc5d3457756d695bbed735ea018559d8b00cd, donc on fait un reset avec ce hash :
$ git reset e3bfc5d3457756d695bbed735ea018559d8b00cd Unstaged changes after reset: M README.txt M TODO.txt M tuto.txt
Notre HEAD s’est bien déplacé sur ce changeset car maintenant le log ne nous affiche plus les autres changesets :
$ git log commit e3bfc5d3457756d695bbed735ea018559d8b00cd Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:52:12 2010 +0000 Ajout du .gitignore commit bbfc3cf5ef3f93c4aab17b39193907a047b5617b Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:34:49 2010 +0000 Ceci est un commentaire
Et on peut voir que l’index a été mis à jour mais que votre copie de travail est restée intacte car « git status » nous indique des changements :
$ git status # On branch master # Changed but not updated: # (use "git add/rm <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # deleted: README.txt # modified: TODO.txt # deleted: tuto.txt # # Untracked files: # (use "git add <file>..." to include in what will be committed) # # proto.txt # test.txt
A ce stade vous avez peut-être envie de revenir tout en haut de l’historique, mais « git log » ne vous donne plus la référence pour le faire !
C’est normal, « git log » vous indique le log telle que vu par la branche courrante. Il existe heureusement une commande qui vous permet de voir toutes les valeurs qu’on a donné à HEAD depuis le début, c’est « git reflog » :
$git reflog e3bfc5d HEAD@{0}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{1}: commit (amend): Je savais bien que j'avais oublié quelque chose b415af6 HEAD@{2}: commit: Je le sens pas ce commit 1a0633c HEAD@{3}: commit: On sauvegarde l'index dans l'historique edfb9d2 HEAD@{4}: commit: Finalement le TODO est bien comme ça 9f2f748 HEAD@{5}: commit: Ajout d'un fichier et suppression d'un autre e3bfc5d HEAD@{6}: commit: Ajout du .gitignore bbfc3cf HEAD@{7}: commit (initial): Ceci est un commentaire
On voit que HEAD s’est pas mal baladé ! Le changeset qui nous intéresse est l’avant dernier (son hash abrégé est le numéro tout à gauche), et on peut y retourner avec « git reset » bien entendu :
$ git reset a9c07b1 $ git log commit a9c07b154a6d2e3b2435fa51bbc3a920fb1652b2 Author: ksamuel <mail@e-vidence.net> Date: Mon Mar 29 16:55:10 2010 +0000 Je savais bien que j'avais oublié quelque chose commit 1a0633ce778044aadea23eb39cdc21fc6c949989 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 21:08:04 2010 +0000 On sauvegarde l'index dans l'historique […]
Les options de git reset
« git reset » possède deux options « –soft » et « — hard », qui permettent de changer son comportement.
« –soft » fait la même chose qu’avant, mais ne met pas à jour l’index. Vous gardez votre index d’origine, ainsi vous pouvez faire un commit directement et sauvegarder le contenu de l’index. Refaisons la même opération que précédemment avec « –soft » :
$ git reset --soft e3bfc5d3457756d695bbed735ea018559d8b00cd $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # deleted: README.txt # modified: TODO.txt # new file: proto.txt # new file: test.txt # deleted: tuto.txt #
Le status nous dit que tous les changements sont près à être sauvegardés car l’index n’a pas été modifié.
On retourne à notre commit final :
$ git reset a9c07b1
Voyons maintenant l’option « –hard », qui elle met en plus à jour la copie de travail. Tous vos fichiers sont donc écrasés et remplacés par ceux issu du changeset vers lequel vous vous déplacez :
$ ls divers proto.txt README.txt test.txt test.txt~ TODO.txt TODO.txt~ tuto.txt $ git reset --hard e3bfc5d3457756d695bbed735ea018559d8b00cd HEAD is now at e3bfc5d Ajout du .gitignore $ ls divers README.txt test.txt~ TODO.txt TODO.txt~ tuto.txt
Vous voyez que les fichiers de la copie de travail ont aussi changé. Du coup pour revenir à notre commit final et avoir les fichiers du commit final, il faut utiliser « –hard » également :
$ git reset --hard a9c07b1 HEAD is now at a9c07b1 Je savais bien que j'avais oublié quelque chose
Vous voyez que le log des références contient maintenant la liste de nos nombreux allers-et-retours :
$ git reflog a9c07b1 HEAD@{0}: a9c07b1: updating HEAD e3bfc5d HEAD@{1}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{2}: a9c07b1: updating HEAD e3bfc5d HEAD@{3}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{4}: a9c07b1: updating HEAD e3bfc5d HEAD@{5}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{6}: a9c07b1: updating HEAD e3bfc5d HEAD@{7}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{8}: commit (amend): Je savais bien que j'avais oublié quelque chose b415af6 HEAD@{9}: commit: Je le sens pas ce commit 1a0633c HEAD@{10}: commit: On sauvegarde l'index dans l'historique edfb9d2 HEAD@{11}: commit: Finalement le TODO est bien comme ça 9f2f748 HEAD@{12}: commit: Ajout d'un fichier et suppression d'un autre e3bfc5d HEAD@{13}: commit: Ajout du .gitignore bbfc3cf HEAD@{14}: commit (initial): Ceci est un commentaire
Le mien est sans doute un peu différent du votre car j’ai fais une ou deux coquilles durant la rédaction du tuto
Pour récapituler :
- « git reset » permet de déplacer la branche courante ainsi que le HEAD. L’index est mis à jour pour refléter le nouveau changeset.
- « –soft » permet d’éviter que l’index soit mis à jour.
- « –hard » permet de mettre à jour aussi la copie de travail.
- « git reflog » permet de voir tout l’historique des références de HEAD (c’est à dire partout où vous êtes allé).
Deux usages de « reset » courant
On utilisera souvent pour deux opérations très courantes.
La première, c’est annuler tous les ajouts dans dans l’index. Par exemple, on s’aperçoit qu’on a fait « git add . » et que plein de choses sont prêtes à être sauvegardées par erreur. Il suffit de faire :
$ git reset HEAD
En effet, on se déplace jusqu’à HEAD (mais on y est déjà puisque HEAD c’est le commit actuel, donc ça ne change rien), et on met tout le contenu du dernier changeset dans l’index. Donc l’index est remis à zéro.
La seconde utilisation est de remettre à plat sa copie de travail. Par exemple on a fait des tas de modifications inutiles sur les fichiers et on veut recommencer à zéro avec les fichiers propres du dernier commit :
$ git reset HEAD --hardEn effet, on se déplace jusqu’à HEAD (mais on y est déjà puisque HEAD c’est le commit actuel, donc ça ne change rien), et on met tout le contenu du dernier changeset dans l’index, puis met à jour la copie de travail pour qu’elle ressemble exactement à son état lors du dernier commit.
« git reset » sur des fichiers
« reset » peut aussi s’appliquer directement à des fichiers, mais dans ce cas on ne peut faire ni « –hard » ni « –soft ». La seule chose que cela produit est la mise à jour de l’index. On l’utilise surtout pour annuler une modification de l’index, par exemple, supposons que je supprime TODO.txt :
$ rm TODO.txt $ git status # On branch master # Changed but not updated: # (use "git add/rm <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # deleted: TODO.txt # no changes added to commit (use "git add" and/or "git commit -a")
La suppression de mon fichier est notée comme “détectée ma pas à sauvegarder”. Je dis à Git que je veux qu’il enregistre cette surpression.
$ git rm TODO.txt rm 'TODO.txt' $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # deleted: TODO.txt #
Maintenant, si je fais un commit, cette suppression est enregistrée. Mais si je change d’avis ? « reset » me permet de récupérer le TODO.txt qui est dans HEAD pour le mettre dans l’index.
$ git reset -- TODO.txt Unstaged changes after reset: M TODO.txt $ git status # On branch master # Changed but not updated: # (use "git add/rm <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # deleted: TODO.txt # no changes added to commit (use "git add" and/or "git commit -a")
Et voilà, la supression ne sera pas enregistrée si je fais un commit.
Mais vous devez vous demander, qu’est-ce que signifie « — ». Ceci n’a rien à voir avec Git, c’est un marqueur utilisé dans n’importe quelle ligne de commande pour dire qu’il n’y a plus d’option après lui. Il n’est pas utile d’entrer dans les détails, tout ce que vous avez besoin de savoir c’est qu’il faut le mettre juste avant le nom du fichier que l’on veut remettre à zéro dans l’index.
Enfin, pour les besoins de l’exercice, nous alons faire un hard reset pour mettre notre copie de travail au propre :
$ git reset –hard HEAD
Et voilà, retour à la case départ
« git checkout »
La commande « checkout » toute seule ressemble beaucoup à un « reset –hard » : elle permet de se déplacer dans l’historique à un point donné et l’index ainsi que la copie de travail sont mis à jour :
$ ls divers proto.txt test.txt test.txt~ TODO.txt TODO.txt~ $ git log [...] commit bbfc3cf5ef3f93c4aab17b39193907a047b5617b Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:34:49 2010 +0000 Ceci est un commentaire $ git checkout bbfc3cf5ef3f93c4aab17b39193907a047b5617b Note: moving to 'bbfc3cf5ef3f93c4aab17b39193907a047b5617b' which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new_branch_name> HEAD is now at bbfc3cf... Ceci est un commentaire $ git log commit bbfc3cf5ef3f93c4aab17b39193907a047b5617b Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 18:34:49 2010 +0000 Ceci est un commentaire $ ls divers README.txt test.txt~ TODO.txt TODO.txt~ tuto.txt
Vous voyez ici la copie de travail telle qu’elle était à notre premier commit. Vous voyez de plus text.txt~ car il est dans le .gitignore et n’a donc pas été touché. Attention cependant :
$ git status # Not currently on any branch. # Untracked files: # (use "git add <file>..." to include in what will be committed) # # TODO.txt~ # test.txt~
Le .gitignore n’avait pas été créé à cette époque, du coup à ce niveau de l’historique, Git voit ces fichiers
Revenons à la case départ grâce au reflog :
$ git reflog bbfc3cf HEAD@{0}: checkout: moving from master to bbfc3cf5ef3f93c4aab17b39193907a047b5617b a9c07b1 HEAD@{1}: a9c07b1: updating HEAD e3bfc5d HEAD@{2}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{3}: a9c07b1: updating HEAD e3bfc5d HEAD@{4}: e3bfc5d3457756d695bbed735ea018559d8b00cd: updating HEAD a9c07b1 HEAD@{5}: a9c07b1: updating HEAD […] $ git checkout a9c07b1 Previous HEAD position was bbfc3cf... Ceci est un commentaire HEAD is now at a9c07b1... Je savais bien que j'avais oublié quelque chose $ ls divers proto.txt test.txt test.txt~ TODO.txt TODO.txt~
Jusqu’ici, pas très utile, on savait déjà faire ça avec « reset ».
Il y a pourtant une différence primordiale : contrairement à « reset », « checkout » ne déplace pas la branche courante, ce qui fait que si vous vous déplacez dans un endroit sans branche, vous travaillez en dehors de toute branche ! Un problème important, que nous verrons dans un autre article.
De plus, « checkout » permet de faire quelque chose que « reset » ne fait pas : récupérer un fichier de l’historique et l’importer dans la copie de travail. Pour cela, il suffit de lui passer en paramètre le nom du fichier.
Supposons qu’on veuille récupérer TODO.txt tel qu’il était 2 commits avant :
$ git log commit a9c07b154a6d2e3b2435fa51bbc3a920fb1652b2 Author: ksamuel <mail@e-vidence.net> Date: Mon Mar 29 16:55:10 2010 +0000 Je savais bien que j'avais oublié quelque chose commit 1a0633ce778044aadea23eb39cdc21fc6c949989 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 21:08:04 2010 +0000 On sauvegarde l'index dans l'historique commit edfb9d255b7a77b7babeaeda3ea6630b38ca1231 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 19:19:33 2010 +0000 Finalement le TODO est bien comme ça commit 9f2f748ecb853a1cb616b6d387f7169dac32f243 Author: ksamuel <mail@e-vidence.net> Date: Sat Mar 27 19:12:18 2010 +0000 Ajout d'un fichier et suppression d'un autre […] $ git checkout 9f2f748ecb853a1cb616b6d387f7169dac32f243 TODO.txt
Et voilà TODO.txt revenu à son état d’origine :
- Devenir un expert en Python.
- Créer mon premier site avec Django.
- Penser "ergonomie" dès le départ pour mon prochain dév.
La copie de travail et l’index sont mis à jour, du coup, la modification est prêt à être sauvegardée :
$ git status # Not currently on any branch. # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: TODO.txt #
Retour à l’état départ :
$ git reset --hard HEADEn résumé :
- « git checkout hash » permet de se placer dans un endroit dans l’historique, mais ne déplace pas la branche courante. On préfère souvent utiliser « reset » pour ce genre de chose, sauf si on sait très bien ce qu’on fait.
- « git checkout hash nom_de_fichier » permet de récupérer un fichier issu d’un changeset et de le mettre dans l’index et la copie de travail courante. Très très utile !
Il est aussi possible de récupérer un fichier issu de l’index en effectuant :
$ git checkout -- nom_de_fichierEn pratique, on l’utilise rarement.
Finis !
La prochaine fois nous verrons plus en détails le principe des tags, des branches et finalement de comment fusionner des modifications entre elles avec « merge ».




30/03/2010 à 12:55
[...] « Nom d’un crotal, pourquoi “self” en Python ? Revenir en arrière avec Git [...]
31/03/2010 à 18:19
je trouve qu’après la lecture de ce tuto, il y a des facette de git k j’assimile mieux. Bonne continuation a Kévin.
19/06/2010 à 12:11
J’ai hâte de voir la suite, notamment la partie collaboration !
Il est prévu le tuto sur la partie serveur ? “git push” et “git pull” ?
J’ai envie de déployer cette solution dans mon entreprise mais je manque encore de recul sur ces points..
Merci en tout cas, excellent tutos
19/06/2010 à 12:43
La partie serveur est prévue.
31/08/2010 à 12:27
Sans le push et le pull, je ne peux pas partager mon travail