En tant que développeur, vous travaillez tout le temps avec Git.
Êtes-vous déjà arrivé à un point où vous avez dit: "Uh-oh, qu'est-ce que je viens de faire?"
Cet article vous donnera les outils pour réécrire l'histoire en toute confiance.
J'ai également donné une conférence en direct couvrant le contenu de ce post. Si vous préférez une vidéo (ou souhaitez la regarder en même temps que la lecture) — vous pouvez la trouver
Je travaille sur un livre sur Git ! Êtes-vous intéressé à lire les versions initiales et à fournir des commentaires ? Envoyez-moi un email: [email protected]
Avant de comprendre comment annuler des choses dans Git, vous devez d'abord comprendre comment nous enregistrons les modifications dans Git. Si vous connaissez déjà tous les termes, n'hésitez pas à sauter cette partie.
Il est très utile de considérer Git comme un système permettant d'enregistrer des instantanés d'un système de fichiers dans le temps. Considérant un référentiel Git, il a trois « états » ou « arborescences » :
Habituellement, lorsque nous travaillons sur notre code source, nous travaillons à partir d'un répertoire de travail . Un répertoire de travail (ectrory) (ou arbre de travail ) est n'importe quel répertoire de notre système de fichiers auquel est associé un référentiel .
Il contient les dossiers et fichiers de notre projet ainsi qu'un répertoire appelé .git
. J'ai décrit le contenu du dossier .git
plus en détail dans
Après avoir apporté certaines modifications, vous souhaiterez peut-être les enregistrer dans votre référentiel . Un dépôt (en bref : repo ) est une collection de commits , dont chacun est une archive de ce à quoi ressemblait l'arborescence de travail du projet à une date passée, que ce soit sur votre machine ou sur celle de quelqu'un d'autre.
Un référentiel comprend également des éléments autres que nos fichiers de code, tels que HEAD
, des branches, etc.
Entre les deux, nous avons l' index ou la staging area ; ces deux termes sont interchangeables. Lorsque nous checkout
une branche, Git remplit l' index avec tout le contenu du fichier qui a été extrait pour la dernière fois dans notre répertoire de travail et à quoi il ressemblait lors de son extraction initiale.
Lorsque nous utilisons git commit
, le commit est créé en fonction de l'état de l' index .
Ainsi, l' index, ou la zone de staging, est votre terrain de jeu pour le prochain commit. Vous pouvez travailler et faire ce que vous voulez avec l' index , y ajouter des fichiers, en supprimer des éléments, puis seulement lorsque vous êtes prêt, vous allez de l'avant et vous engagez dans le référentiel.
Il est temps de mettre la main à la pâte 🙌🏻
Utilisez git init
pour initialiser un nouveau référentiel. Écrivez du texte dans un fichier appelé 1.txt
:
Parmi les trois états d'arborescence décrits ci-dessus, où se trouve 1.txt
maintenant ?
Dans l'arbre de travail, car il n'a pas encore été introduit dans l'index.
Pour le mettre en scène , pour l'ajouter à l'index, utilisez git add 1.txt
.
Maintenant, nous pouvons utiliser git commit
pour valider nos modifications dans le référentiel.
Vous avez créé un nouvel objet de validation, qui inclut un pointeur vers un arbre décrivant l'intégralité de l'arbre de travail. Dans ce cas, ce ne sera que 1.txt
dans le dossier racine. En plus d'un pointeur vers l'arborescence, l'objet de validation inclut des métadonnées, telles que des horodatages et des informations sur l'auteur.
Pour plus d'informations sur les objets dans Git (tels que les commits et les arbres),
(Oui, "check out", jeu de mots voulu 😇)
Git nous indique également la valeur SHA-1 de cet objet commit. Dans mon cas, c'était c49f4ba
(qui ne sont que les 7 premiers caractères de la valeur SHA-1, pour économiser de l'espace).
Si vous exécutez cette commande sur votre ordinateur, vous obtiendrez une valeur SHA-1 différente, car vous êtes un auteur différent ; également, vous créeriez le commit sur un horodatage différent.
Lorsque nous initialisons le référentiel, Git crée une nouvelle branche (nommée main
par défaut). Etmain
. Que se passe-t-il si vous avez plusieurs succursales ? Comment Git sait-il quelle branche est la branche active ?
Git a un autre pointeur appelé HEAD
, qui pointe (généralement) vers une branche, qui pointe ensuite vers un commit. D'ailleurs,HEAD
Il est temps d'introduire plus de changements dans le référentiel !
Maintenant, je veux en créer un autre. Créons donc un nouveau fichier et ajoutons-le à l'index, comme précédemment :
Maintenant, il est temps d'utiliser git commit
. Il est important de noter que git commit
fait deux choses :
Tout d'abord, il crée un objet de validation, il existe donc un objet dans la base de données d'objets interne de Git avec une valeur SHA-1 correspondante. Ce nouvel objet commit pointe également vers le commit parent. C'est le commit vers lequel HEAD
pointait lorsque vous avez écrit la commande git commit
.
Deuxièmement, git commit
déplace le pointeur de la branche active — dans notre cas, ce serait main
, pour pointer vers l'objet commit nouvellement créé.
Pour réécrire l'historique, commençons par annuler le processus d'introduction d'un commit. Pour cela, nous allons faire connaissance avec la commande git reset
, un outil super puissant.
git reset --soft
Donc, la toute dernière étape que vous avez faite auparavant était de git commit
, ce qui signifie en fait deux choses : Git a créé un objet commit et a déplacé main
, la branche active. Pour annuler cette étape, utilisez la commande git reset --soft HEAD~1
.
La syntaxe HEAD~1
fait référence au premier parent de HEAD
. Si j'avais plus d'un commit dans le commit-graph, dites "Commit 3" pointant vers "Commit 2", qui à son tour pointe vers "Commit 1".
Et disons que HEAD
pointait vers "Commit 3". Vous pouvez utiliser HEAD~1
pour faire référence à "Commit 2", et HEAD~2
ferait référence à "Commit 1".
Donc, revenons à la commande : git reset --soft HEAD~1
Cette commande demande à Git de modifier tout ce vers quoi HEAD
pointe. (Remarque : dans les diagrammes ci-dessous, j'utilise *HEAD
pour « tout ce vers quoi HEAD
pointe »). Dans notre exemple, HEAD
pointe vers main
. Ainsi, Git ne changera que le pointeur de main
pour pointer vers HEAD~1
. C'est-à-dire que main
pointera vers "Commit 1".
Cependant, cette commande n'affectait pas l'état de l'index ou de l'arbre de travail. Donc, si vous utilisez git status
, vous verrez que 2.txt
est mis en scène, comme avant d'exécuter git commit
.
Qu'en est-il de git log?
Il commencera par HEAD
, ira à main
, puis à "Commit 1". Notez que cela signifie que "Commit 2" n'est plus accessible depuis notre historique.
Cela signifie-t-il que l'objet commit de "Commit 2" est supprimé ? 🤔
Non, il n'est pas supprimé. Il réside toujours dans la base de données d'objets interne de Git.
Si vous poussez l'historique actuel maintenant, en utilisant git push
, Git ne poussera pas "Commit 2" sur le serveur distant, mais l'objet commit existe toujours sur votre copie locale du référentiel.
Maintenant, validez à nouveau - et utilisez le message de validation de "Commit 2.1" pour différencier ce nouvel objet du "Commit 2" d'origine :
Pourquoi "Commit 2" et "Commit 2.1" sont-ils différents ? Même si nous avons utilisé le même message de validation, et même s'ils pointent vers1.txt
et 2.txt
), ils ont toujours des horodatages différents, car ils ont été créés à des moments différents.
Dans le dessin ci-dessus, j'ai conservé "Commit 2" pour vous rappeler qu'il existe toujours dans la base de données d'objets interne de Git. "Commit 2" et "Commit 2.1" pointent maintenant vers "Commit 1", mais seul "Commit 2.1" est accessible depuis HEAD
.
Il est temps de revenir en arrière et de défaire plus loin. Cette fois, utilisez git reset --mixed HEAD~1
(remarque : --mixed
est le commutateur par défaut pour git reset
).
Cette commande démarre de la même manière que git reset --soft HEAD~1
. Cela signifie qu'il prend le pointeur de tout ce que HEAD
pointe maintenant, qui est la branche main
, et le définit sur HEAD~1
, dans notre exemple - "Commit 1".
Ensuite, Git va plus loin, annulant efficacement les modifications que nous avons apportées à l'index. C'est-à-dire, changer l'index pour qu'il corresponde au HEAD
actuel, le nouveau HEAD
après l'avoir défini à la première étape.
Si nous lancions git reset --mixed HEAD~1
, cela signifie que HEAD
serait défini sur HEAD~1
("Commit 1"), puis Git ferait correspondre l'index à l'état de "Commit 1" - dans ce cas, il signifie que 2.txt
ne fera plus partie de l'index.
Il est temps de créer un nouveau commit avec l'état du "Commit 2" d'origine. Cette fois, nous devons à nouveau mettre en scène 2.txt
avant de le créer :
Allez-y, annulez encore plus !
Allez-y et lancez git reset --hard HEAD~1
Encore une fois, Git commence par l'étape --soft
, en définissant tout ce que HEAD
pointe vers ( main
), sur HEAD~1
("Commit 1").
Jusqu'ici, tout va bien.
Ensuite, passez à l'étape --mixed
, en faisant correspondre l'index avec HEAD
. Autrement dit, Git annule la mise en scène de 2.txt
.
Il est temps pour l'étape --hard
où Git va encore plus loin et fait correspondre le répertoire de travail avec l'étape de l'index. Dans ce cas, cela signifie supprimer également 2.txt
du répertoire de travail.
(**Remarque : dans ce cas précis, le fichier n'est pas suivi, il ne sera donc pas supprimé du système de fichiers ; ce n'est cependant pas vraiment important pour comprendre git reset
).
Donc, pour introduire un changement dans Git, vous avez trois étapes. Vous modifiez le répertoire de travail, l'index ou la zone de staging, puis vous validez un nouvel instantané avec ces modifications. Pour annuler ces modifications :
git reset --soft
, nous annulons l'étape de validation.
git reset --mixed
, nous annulons également l'étape de mise en scène.
git reset --hard
, nous annulons les modifications apportées au répertoire de travail. Donc, dans un scénario réel, écrivez "J'aime Git" dans un fichier ( love.txt
), car nous aimons tous Git 😍. Allez-y, mettez en scène et validez ceci également :
Oh, oups !
En fait, je ne voulais pas que tu le commettes.
Ce que je voulais vraiment que vous fassiez, c'est d'écrire quelques mots d'amour supplémentaires dans ce fichier avant de le valider.
Que pouvez-vous faire?
Eh bien, une façon de surmonter cela serait d'utiliser git reset --mixed HEAD~1
, en annulant efficacement les actions de validation et de mise en scène que vous avez prises :
Donc, les points main
sont à nouveau "Commit 1", et love.txt
ne fait plus partie de l'index. Cependant, le fichier reste dans le répertoire de travail. Vous pouvez maintenant continuer et ajouter plus de contenu :
Allez-y, mettez en scène et commitez votre fichier :
Bravo 👏🏻
Vous avez cet historique clair et agréable de "Commit 2.4" pointant vers "Commit 1".
Nous avons maintenant un nouvel outil dans notre boîte à outils, git reset
💪🏻
Cet outil est super, super utile, et vous pouvez accomplir presque n'importe quoi avec lui. Ce n'est pas toujours l'outil le plus pratique à utiliser, mais il est capable de résoudre presque tous les scénarios d'historique de réécriture si vous l'utilisez avec précaution.
Pour les débutants, je recommande d'utiliser uniquement git reset
presque chaque fois que vous souhaitez annuler dans Git. Une fois que vous vous sentez à l'aise avec, il est temps de passer à d'autres outils.
Considérons un autre cas.
Créez un nouveau fichier nommé new.txt
; mettre en scène et valider :
Oops. En fait, c'est une erreur. Vous étiez sur main
, et je voulais que vous créiez ce commit sur une branche de fonctionnalité. Mon mauvais 😇
Il y a deux outils les plus importants que je veux que vous retiriez de cet article. Le second est git reset
. La première et de loin la plus importante consiste à dresser un tableau blanc de l'état actuel par rapport à l'état dans lequel vous voulez être.
Pour ce scénario, l'état actuel et l'état souhaité ressemblent à ceci :
Vous remarquerez trois changements :
points main
à "Commit 3" (le bleu) dans l'état actuel, mais à "Commit 2.4" dans l'état souhaité.
La branche feature
n'existe pas dans l'état actuel, mais elle existe et pointe vers "Commit 3" dans l'état souhaité.
HEAD
pointe sur main
dans l'état actuel et sur feature
dans l'état souhaité.
Si vous pouvez dessiner ceci et que vous savez comment utiliser git reset
, vous pouvez certainement vous sortir de cette situation.
Encore une fois, la chose la plus importante est de respirer et de tirer cela.
En observant le dessin ci-dessus, comment passe-t-on de l'état actuel à celui souhaité ?
Il existe bien sûr plusieurs façons, mais je ne présenterai qu'une seule option pour chaque scénario. N'hésitez pas à jouer avec d'autres options également.
Vous pouvez commencer par utiliser git reset --soft HEAD~1
. Cela définirait main
pour pointer vers le commit précédent, "Commit 2.4":
En jetant un coup d'œil au diagramme actuel vs souhaité, vous pouvez voir que vous avez besoin d'une nouvelle branche, n'est-ce pas ? Vous pouvez utiliser git switch -c feature
pour cela ou git checkout -b feature
(qui fait la même chose):
Cette commande met également à jour HEAD
pour pointer vers la nouvelle branche.
Depuis que vous avez utilisé git reset --soft
, vous n'avez pas modifié l'index, il a donc exactement l'état que vous souhaitez valider - comme c'est pratique ! Vous pouvez simplement vous engager sur la branche feature
:
Et vous êtes arrivé à l'état souhaité 🎉
Prêt à appliquer vos connaissances à d'autres cas ?
Ajoutez quelques modifications à love.txt
et créez également un nouveau fichier appelé cool.txt
. Mettez-les en scène et engagez :
Oh, oups, en fait je voulais que vous créiez deux commits séparés, un avec chaque changement 🤦🏻
Vous voulez essayer celui-ci vous-même ?
Vous pouvez annuler les étapes de validation et de préproduction :
Suite à cette commande, l'index n'inclut plus ces deux modifications, mais elles sont toujours dans votre système de fichiers. Alors maintenant, si vous ne faites que mettre en scène love.txt
, vous pouvez le valider séparément, puis faire de même pour cool.txt
:
sympa 😎
Créez un nouveau fichier ( new_file.txt
) avec du texte et ajoutez du texte à love.txt
. Mettez en scène les deux modifications et validez-les :
Oups 🙈🙈
Donc cette fois, je voulais que ce soit sur une autre branche, mais pas une nouvelle branche, plutôt une branche déjà existante.
Alors que peux-tu faire?
Je vais vous donner un indice. La réponse est vraiment courte et vraiment facile. Que fait-on en premier ?
Non, pas reset
. Nous dessinons. C'est la première chose à faire, car cela rendrait tout le reste beaucoup plus facile. Voici donc l'état actuel :
Et l'état souhaité ?
Comment passer de l'état actuel à l'état souhaité, qu'est-ce qui serait le plus simple ?
Donc, une façon serait d'utiliser git reset
comme vous l'avez fait auparavant, mais il y a une autre façon que j'aimerais que vous essayiez.
Tout d'abord, déplacez HEAD
pour pointer vers la branche existing
:
Intuitivement, ce que vous voulez faire est de prendre les modifications introduites dans le commit bleu et d'appliquer ces modifications ("copier-coller") au-dessus de la branche existing
. Et Git a un outil juste pour ça.
Pour demander à Git de prendre les changements introduits entre ce commit et son commit parent et d'appliquer simplement ces changements sur la branche active, vous pouvez utiliser git cherry-pick
. Cette commande prend les changements introduits dans la révision spécifiée et les applique au commit actif.
Il crée également un nouvel objet commit et met à jour la branche active pour pointer vers ce nouvel objet.
Dans l'exemple ci-dessus, j'ai spécifié l'identifiant SHA-1 du commit créé, mais vous pouvez également utiliser git cherry-pick main
, car le commit dont nous appliquons les modifications est celui vers lequel pointe main
.
Mais nous ne voulons pas que ces changements existent sur la branche main
. git cherry-pick
n'a appliqué les modifications qu'à la branche existing
. Comment pouvez-vous les supprimer de main
?
Une façon serait de switch
à main
, puis d'utiliser git reset --hard HEAD~1
:
Tu l'as fait! 💪🏻
Notez que git cherry-pick
calcule en fait la différence entre le commit spécifié et son parent, puis les applique au commit actif. Cela signifie que parfois, Git ne pourra pas appliquer ces modifications car vous pourriez avoir un conflit, mais c'est un sujet pour un autre article.
Notez également que vous pouvez demander à Git de cherry-pick
les modifications introduites dans n'importe quel commit, pas seulement les commits référencés par une branche.
Nous avons acquis un nouvel outil, nous avons donc git reset
ainsi que git cherry-pick
à notre actif.
Bon, alors un autre jour, un autre repo, un autre problème.
Créez un commit :
Et push
-le sur le serveur distant :
Euh, oups 😓…
Je viens de remarquer quelque chose. Il y a une faute de frappe. J'ai écrit This is more tezt
au lieu de This is more text
. Oups. Alors quel est le gros problème maintenant ? J'ai push
ed, ce qui signifie que quelqu'un d'autre a peut-être déjà pull
ces modifications.
Si je remplace ces modifications en utilisant git reset
, comme nous l'avons fait jusqu'à présent, nous aurons des historiques différents, et l'enfer pourrait se déchaîner. Vous pouvez réécrire votre propre copie du référentiel autant que vous le souhaitez jusqu'à ce que vous le push
.
Une fois que vous push
le changement, vous devez être très certain que personne d'autre n'a récupéré ces changements si vous allez réécrire l'histoire.
Alternativement, vous pouvez utiliser un autre outil appelé git revert
. Cette commande prend le commit que vous lui fournissez et calcule le Diff à partir de son commit parent, tout comme git cherry-pick
, mais cette fois, il calcule les changements inverses.
Donc, si dans le commit spécifié vous avez ajouté une ligne, l'inverse supprimerait la ligne, et vice versa.
git revert
a créé un nouvel objet commit, ce qui signifie qu'il s'agit d'un ajout à l'historique. En utilisant git revert
, vous n'avez pas réécrit l'historique. Vous avez admis votre erreur passée, et ce commit est une reconnaissance que vous avez fait une erreur et maintenant vous l'avez corrigée.
Certains diraient que c'est la voie la plus mature. Certains diront que ce n'est pas un historique aussi propre que si vous utilisiez git reset
pour réécrire le commit précédent. Mais c'est une façon d'éviter de réécrire l'histoire.
Vous pouvez maintenant corriger la faute de frappe et valider à nouveau :
Votre boîte à outils est maintenant chargée avec un nouvel outil brillant, revert
:
Travaillez, écrivez du code et ajoutez-le à love.txt
. Mettez en scène ce changement et validez-le :
J'ai fait la même chose sur ma machine et j'ai utilisé la touche fléchée vers le Up
de mon clavier pour revenir aux commandes précédentes, puis j'ai appuyé sur Enter
et… Wow.
Oups.
Ai-je simplement utilisé git reset --hard
? 😨
Que s'est-il réellement passé ? Git a déplacé le pointeur vers HEAD~1
, donc le dernier commit, avec tout mon précieux travail, n'est pas accessible depuis l'historique actuel. Git a également désorganisé toutes les modifications de la zone de staging, puis a fait correspondre le répertoire de travail à l'état de la zone de staging.
C'est-à-dire que tout correspond à cet état où mon travail est… parti.
Temps de panique. Paniquer.
Mais, vraiment, y a-t-il une raison de paniquer ? Pas vraiment… Nous sommes des gens détendus. Qu'est-ce qu'on fait? Eh bien, intuitivement, le commit a-t-il vraiment, vraiment disparu ? Non pourquoi pas? Il existe toujours dans la base de données interne de Git.
Si je savais seulement où c'est, je connaîtrais la valeur SHA-1 qui identifie ce commit, nous pourrions le restaurer. Je pourrais même annuler l'annulation et reset
à ce commit.
Donc, la seule chose dont j'ai vraiment besoin ici est le SHA-1 du commit "supprimé".
Donc la question est, comment puis-je le trouver? git log
serait-il utile ?
Eh bien pas vraiment. git log
irait à HEAD
, qui pointe vers main
, qui pointe vers le commit parent du commit que nous recherchons. Ensuite, git log
remonterait à travers la chaîne parente, qui n'inclut pas le commit avec mon précieux travail.
Heureusement, les personnes très intelligentes qui ont créé Git ont également créé un plan de sauvegarde pour nous, et cela s'appelle le reflog
.
Pendant que vous travaillez avec Git, chaque fois que vous modifiez HEAD
, ce que vous pouvez faire en utilisant git reset
, mais aussi d'autres commandes comme git switch
ou git checkout
, Git ajoute une entrée au reflog
.
Nous avons trouvé notre engagement ! C'est celui qui commence par 0fb929e
.
Nous pouvons également l'identifier par son "surnom" - HEAD@{1}
. Ainsi, comme Git utilise HEAD~1
pour accéder au premier parent de HEAD
, et HEAD~2
pour faire référence au deuxième parent de HEAD
et ainsi de suite, Git utilise HEAD@{1}
pour faire référence au premier parent reflog de HEAD
, où HEAD
pointait à l'étape précédente.
On peut aussi demander à git rev-parse
de nous montrer sa valeur :
Une autre façon de voir le reflog
est d'utiliser git log -g
, qui demande à git log
de considérer réellement le reflog
:
Nous voyons ci-dessus que le reflog
, tout comme HEAD
, pointe vers main
, qui pointe vers « Commit 2 ». Mais le parent de cette entrée dans le reflog
pointe vers "Commit 3".
Donc, pour revenir à "Commit 3", vous pouvez simplement utiliser git reset --hard HEAD@{1}
(ou la valeur SHA-1 de "Commit 3") :
Et maintenant, si nous git log
:
Nous avons sauvé la journée ! 🎉👏🏻
Que se passerait-il si j'utilisais à nouveau cette commande ? Et exécuté git commit --reset HEAD@{1}
? Git définirait HEAD
sur l'endroit où HEAD
pointait avant la dernière reset
, c'est-à-dire sur "Commit 2". On peut continuer toute la journée :
En regardant maintenant notre boîte à outils, elle est remplie d'outils qui peuvent vous aider à résoudre de nombreux cas où les choses tournent mal dans Git :
Avec ces outils, vous comprenez mieux maintenant le fonctionnement de Git. Il existe d'autres outils qui vous permettraient de réécrire spécifiquement l'historique, git rebase
), mais vous avez déjà beaucoup appris dans cet article. Dans les prochains articles, je plongerai également dans git rebase
.
L'outil le plus important, encore plus important que les cinq outils répertoriés dans cette boîte à outils, est de dresser un tableau blanc de la situation actuelle par rapport à celle souhaitée. Faites-moi confiance, cela rendra chaque situation moins intimidante et la solution plus claire.
J'ai également donné une conférence en direct couvrant le contenu de ce message. Si vous préférez une vidéo (ou souhaitez la regarder en même temps que la lecture) — vous pouvez la trouver
En général,
Omer est titulaire d'une maîtrise en linguistique de l'Université de Tel-Aviv et est le créateur du
Publié pour la première fois ici