Git Reset vs Revert vs Rebase

Acest articol explorează diferitele metode de a interacționa cu commit-urile în Git, oferindu-ți o înțelegere clară a modului în care poți manipula istoricul proiectului tău.

Ca dezvoltator, este posibil să te fi confruntat frecvent cu situații în care doreai să te întorci la o versiune anterioară a codului, dar nu știai exact cum să procedezi. Chiar dacă cunoști comenzile de bază Git, cum ar fi reset, revert sau rebase, diferențele dintre ele ar putea fi neclare. Prin urmare, vom analiza în detaliu aceste trei comenzi, pentru a înțelege cum și când să le utilizăm.

Git Reset – Anularea modificărilor

Comanda git reset este un instrument puternic, utilizat în principal pentru a anula anumite modificări în depozitul tău Git. O poți considera o funcție de „înapoi în timp”.

Cu git reset, poți naviga între diferitele commit-uri, similar cu derularea unui film. Există trei moduri de funcționare ale comenzii: --soft, --mixed și --hard. Modul implicit este --mixed. În cadrul procesului git reset, intervin trei elemente cheie ale Git: HEAD, zona de pregătire (index) și directorul de lucru.

Directorul de lucru reprezintă locația unde se află fișierele tale și unde lucrezi în mod curent. Poți verifica conținutul acestui director utilizând comanda git status, care îți va afișa fișierele și directoarele prezente.

Zona de pregătire, sau index, este locul unde Git urmărește și stochează modificările pe care le faci fișierelor. Aceste modificări sunt reflectate în directorul .git. Pentru a adăuga un fișier în zona de pregătire, folosește git add "nume_fisier". Din nou, git status îți va arăta ce fișiere se află în zona de pregătire.

Ramura curentă în Git este identificată prin HEAD. Aceasta indică ultimul commit realizat în ramura respectivă. Este practic un pointer către o referință. Când schimbi ramura, HEAD se mută la noul commit din ramura respectivă.

Să analizăm funcționarea git reset în cele trei moduri: hard, soft și mixed. Modul hard mută indicatorul HEAD la commit-ul specificat, actualizează directorul de lucru cu fișierele acelui commit și resetează zona de pregătire. În modul soft, se modifică doar pointerul, iar fișierele rămân intacte atât în directorul de lucru, cât și în zona de pregătire. Modul mixed (cel implicit) resetează atât indicatorul, cât și zona de pregătire.

Git Reset Hard

Scopul git reset --hard este de a deplasa HEAD la commit-ul dorit, ștergând toate commit-urile ulterioare. Această operațiune modifică istoricul commit-urilor, reîntorcând proiectul la starea specificată.

În exemplul de mai jos, voi adăuga trei fișiere noi, le voi comita și apoi voi efectua o resetare completă (hard reset).

Mai întâi, verific starea, pentru a mă asigura că nu există modificări necomise.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

Acum creez trei fișiere noi și adaug conținut în ele.

$ vi file1.txt
$ vi file2.txt
$ vi file3.txt

Adaug aceste fișiere în depozit.

$ git add file*

Verific din nou starea. Fișierele noi sunt acum în zona de pregătire.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

Changes to be committed:

(use "git restore --staged <file>..." to unstage)

new file:
file1.txt

new file:
file2.txt

new file:
file3.txt

Înainte de a comite, vă voi arăta jurnalul cu cele 3 commit-uri existente.

$ git log --oneline
0db602e (HEAD -> master) one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Acum voi comite aceste fișiere.

$ git commit -m 'added 3 files'
[master d69950b] added 3 files
3 files changed, 3 insertions(+)
create mode 100644 file1.txt
create mode 100644 file2.txt
create mode 100644 file3.txt

Cu ls-files, confirm că fișierele au fost adăugate.

$ git ls-files
demo
dummyfile
newfile
file1.txt
file2.txt
file3.txt

În jurnal, acum am 4 commit-uri, HEAD indicând ultimul.

$ git log --oneline
d69950b (HEAD -> master) added 3 files
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Dacă șterg manual file1.txt și verific starea, Git îmi spune că modificările nu sunt pregătite pentru commit.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

Changes not staged for commit:

(use "git add/rm <file>..." to update what will be committed)

(use "git restore <file>..." to discard changes in working directory)

deleted:
file1.txt

no changes added to commit (use "git add" and/or "git commit -a")

Acum execut o resetare completă (hard reset).

$ git reset --hard
HEAD is now at d69950b added 3 files

Dacă verific starea, nu mai am nimic de comitat, iar file1.txt a revenit. Aceasta se întâmplă deoarece modificarea (ștergerea fișierului) nu a fost comitată. Resetarea completă readuce proiectul la starea din commit-ul indicat.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

În jurnal, vedem că am revenit la starea commit-ului d69950b.

$ git log
commit d69950b7ea406a97499e07f9b28082db9db0b387 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 19:53:31 2020 +0530

added 3 files

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Scopul resetării hard este de a indica un anumit commit și de a actualiza directorul de lucru și zona de staging. Un alt exemplu: vizualizarea curentă a commit-urilor este:

Voi rula comanda cu HEAD^, ceea ce înseamnă că vreau să resetez la commit-ul anterior (un commit înapoi).

$ git reset --hard HEAD^
HEAD is now at 0db602e one more commit

Observăm că indicatorul HEAD s-a mutat de la d69950b la 0db602e.

$ git log --oneline
0db602e (HEAD -> master) one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Commit-ul d69950b a dispărut din jurnal, iar HEAD indică acum SHA-ul 0db602e.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

Test

Cu ls-files, observăm că fișierele file1.txt, file2.txt și file3.txt nu mai există, deoarece commit-ul care le includea a fost eliminat în urma resetării hard.

$ git ls-files
demo
dummyfile
newfile

Git Soft Reset

În continuare, voi prezenta un exemplu de resetare soft. Presupunem că am adăugat din nou cele 3 fișiere și le-am comitat. Jurnalul arată astfel, cu „soft reset” ca ultimul commit și HEAD indicând la el.

$ git log --oneline
aa40085 (HEAD -> master) soft reset
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Detaliile commit-ului din jurnal sunt afișate cu comanda de mai jos.

$ git log
commit aa400858aab3927e79116941c715749780a59fc9 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 21:01:36 2020 +0530

soft reset

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Acum, folosind resetarea soft, mă voi întoarce la commit-ul cu SHA 0db602e085a4d59cfa9393abac41ff5fd7afcb14.

Pentru aceasta, voi rula următoarea comandă. Este suficient să specificați primele 6 caractere din SHA.

$ git reset --soft 0db602e085a4

În jurnal, HEAD a fost mutat la commit-ul indicat.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Important, fișierele din commit-ul aa400858aab3927e79116941c715749780a59fc9 (unde am adăugat 3 fișiere) se află încă în directorul de lucru. Ele nu au fost șterse. Acesta este motivul pentru care se recomandă utilizarea resetării soft, și nu a celei hard, când nu vrei să pierzi fișierele.

$ git ls-files
demo
dummyfile
file1.txt
file2.txt
file3.txt
newfile

Git Revert – Anularea modificărilor prin commit

Comanda git revert este utilizată pentru a anula modificările prin crearea unui nou commit, care inversează modificările aduse de commit-ul specificat. Spre deosebire de reset, revert nu elimină date. Putem spune că o comandă git revert este de fapt un nou commit.

Git revert menține istoricul commit-urilor, neștergând informații despre commit-urile anterioare.

Presupunem că adaug din nou 3 fișiere și execut un commit, ca în exemplul de mai jos:

$ git commit -m 'add 3 files again'
[master 812335d] add 3 files again
3 files changed, 3 insertions(+)
create mode 100644 file1.txt
create mode 100644 file2.txt
create mode 100644 file3.txt

Jurnalul arată noul commit.

$ git log --oneline
812335d (HEAD -> master) add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Vreau să revin la commit-ul 59c86c9 new commit. Voi folosi următoarea comandă:

$ git revert 59c86c9

Această comandă va deschide un editor, unde vei găsi detaliile commit-ului la care încerci să revii. Aici poți defini numele noului commit, apoi salvezi și închizi fișierul.

Revert "new commit"

This reverts commit 59c86c96a82589bad5ecba7668ad38aa684ab323.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is ahead of 'origin/master' by 4 commits.
# (use "git push" to publish your local commits)
#
# Changes to be committed:
# modified: dummyfile

După ce salvezi și închizi fișierul, vei obține următorul rezultat:

$ git revert 59c86c9
[master af72b7a] Revert "new commit"
1 file changed, 1 insertion(+), 1 deletion(-)

Revenirea a creat un nou commit, spre deosebire de resetare. În jurnal, se observă noul commit generat de operațiunea de revenire.

$ git log --oneline
af72b7a (HEAD -> master) Revert "new commit"
812335d add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Jurnalul Git păstrează tot istoricul commit-urilor. Dacă vrei să elimini commit-uri din istoric, revert nu este o soluție potrivită. Dar, dacă vrei să păstrezi istoricul commit-urilor, cu modificările corespunzătoare, revert este alternativa corectă la reset.

Git Rebase – Mutarea și combinarea commit-urilor

În Git, rebase este utilizat pentru a muta sau a combina commit-urile de pe o ramură pe alta. În general, un dezvoltator nu ar crea funcționalități noi pe ramura principală. Acesta ar lucra pe propria ramură („feature branch”), și odată ce are câteva commit-uri cu funcționalitatea adăugată, le-ar muta pe ramura principală.

Rebase poate fi uneori dificil de înțeles, deoarece este similar cu o operație de merge. Scopul ambelor este de a aduce commit-urile din ramura de funcționalitate pe ramura principală (sau orice altă ramură). Să considerăm un grafic ca cel de mai jos:

Într-o echipă cu mai mulți dezvoltatori, acest grafic ar putea deveni complex, cu diferite ramuri de funcționalități și multiple modificări. Rebase intervine aici pentru a simplifica lucrurile. În loc de git merge, se utilizează rebase, pentru a muta commit-urile de pe ramura de funcționalitate pe ramura principală. Practic, rebase mută commit-urile ramurii de funcționalitate în vârful commit-urilor ramurii principale.

Această abordare oferă un grafic liniar, cu toate commit-urile în ordine.

Astfel, este mai ușor de urmărit ce commit-uri au ajuns unde. Chiar și într-o echipă mare, toate commit-urile sunt în ordine, iar istoricul devine mai simplu de gestionat.

Să vedem cum funcționează practic.

Ramura principală are 4 commit-uri:

$ git log --oneline
812335d (HEAD -> master) add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Voi crea o ramură nouă, numită feature, bazată pe commit-ul 59c86c9:

(master)
$ git checkout -b feature 59c86c9
Switched to a new branch 'feature'

În ramura feature, jurnalul arată doar 2 commit-uri preluate de pe ramura principală:

(feature)
$ git log --oneline
59c86c9 (HEAD -> feature) new commit
e2f44fc (origin/master, origin/HEAD) test

Creez un fișier feature1.txt și îl adaug în ramura feature.

(feature)
$ vi feature1.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'feature 1'
[feature c639e1b] feature 1
1 file changed, 1 insertion(+)
create mode 100644 feature1.txt

Creez al doilea fișier, feature2.txt și îl commit în ramura feature.

(feature)
$ vi feature2.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'feature 2'
[feature 0f4db49] feature 2
1 file changed, 1 insertion(+)
create mode 100644 feature2.txt

Acum, în jurnalul ramurii feature avem două noi commit-uri:

(feature)
$ git log --oneline
0f4db49 (HEAD -> feature) feature 2
c639e1b feature 1
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Vreau să adaug aceste două funcționalități pe ramura principală. Pentru aceasta, folosesc comanda rebase. Din ramura feature, fac rebase față de ramura master, ceea ce ancorează ramura feature la modificările din ramura master.

(feature)
$ git rebase master
Successfully rebased and updated refs/heads/feature.

Apoi verific ramura master.

(feature)
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

În final, fac rebase pe ramura principală față de ramura feature. Aceasta va aduce cele două commit-uri din ramura feature în vârful ramurii master.

(master)
$ git rebase feature
Successfully rebased and updated refs/heads/master.

Dacă verific jurnalul ramurii principale, observ că cele două commit-uri de pe ramura feature au fost adăugate cu succes.

(master)
$ git log --oneline
766c996 (HEAD -> master, feature) feature 2
c036a11 feature 1
812335d add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Acesta a fost totul despre comenzile reset, revert și rebase în Git.

Concluzie

Am analizat comenzile reset, revert și rebase în Git. Sperăm că acest ghid a fost util. Acum ar trebui să poți interacționa cu commit-urile în funcție de nevoile tale, utilizând comenzile descrise în acest articol.