Abans de passar a eines més especialitzades, parlem dels comandaments reset i checkout de Git. Aquests comandaments són dues de les parts més confuses de Git quan els trobes per primera vegada. Fan tantes coses que sembla desesperant entendre’ls i utilitzar-los correctament. Per a això, recomanem una metàfora senzilla.
Una manera més fàcil de pensar en reset i checkout és a través del marc mental de Git com a gestor de continguts de tres arbres diferents. Per "arbre" aquí, ens referim realment a "col·lecció de fitxers", no específicament a l’estructura de dades. Hi ha alguns casos en què l’índex no actua exactament com un arbre, però per als nostres propòsits és més fàcil pensar-hi d’aquesta manera per ara.
Git com a sistema gestiona i manipula tres arbres en la seva operació normal:
| Arbre | Rol | |-------|------| | HEAD | Últim instantània de commit, següent pare | | Index | Proposta de següent instantània de commit | | Directori de Treball | Sandbox |
HEAD és el punter a la referència de la branca actual, que al seu torn és un punter a l’últim commit fet en aquesta branca. Això significa que HEAD serà el pare del següent commit que es creï. Generalment és més senzill pensar en HEAD com l’instantània del teu últim commit en aquesta branca.
De fet, és bastant fàcil veure com és aquesta instantània. Aquí tens un exemple d’obtenció de la llista de directoris real i les sumes de verificació SHA-1 per a cada fitxer a l’instantània de HEAD:
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... libEls comandaments cat-file i ls-tree de Git són comandaments de "fontaneria" que s’utilitzen per a coses de nivell inferior i no realment utilitzats en el treball diari, però ens ajuden a veure què està passant aquí.
L’índex és la teva proposta de següent commit. També ens hem referit a aquest concepte com l'"Àrea d’Staging" de Git, ja que això és el que Git mira quan executes git commit.
Git omple aquest índex amb una llista de tots els continguts dels fitxers que van ser revisats per última vegada al teu directori de treball i com es veien quan van ser revisats originalment. Després reemplaces alguns d’aquests fitxers amb noves versions d’ells, i git commit converteix això en l’arbre per a un nou commit.
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rbUn altre cop, aquí utilitzem git ls-files, que és més un comandament darrere de les escenes que et mostra com és actualment el teu índex.
L’índex no és tècnicament una estructura d’arbre, de fet està implementat com un manifest aplanat, però per als nostres propòsits és prou a prop.
Finalment, tens el teu directori de treball (també comunament referit com l'"arbre de treball"). Els altres dos arbres emmagatzemen el seu contingut d’una manera eficient però poc convenient, dins de la carpeta .git. El directori de treball els desempaqueta en fitxers reals, cosa que facilita molt l’edició. Pensa en el directori de treball com un sandbox, on pots provar canvis abans de confirmar-los a la teva àrea d’staging (índex) i després a l’historial.
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directori, 3 fitxersEl flux de treball típic de Git és registrar instantànies del teu projecte en estats successivament millors, manipulant aquests tres arbres.
Visualitzem aquest procés: suposem que entres en un nou directori amb un únic fitxer. Anomenarem això v1 del fitxer, i l’indicarem en blau. Ara executem git init, que crearà un repositori Git amb una referència HEAD que apunta a la branca master no nascuda.
En aquest punt, només l’arbre del directori de treball té algun contingut.
Ara volem confirmar aquest fitxer, així que utilitzem git add per agafar el contingut al directori de treball i copiar-lo a l’índex.
Llavors executem git commit, que agafa el contingut de l’índex i el guarda com una instantània permanent, crea un objecte commit que apunta a aquesta instantània, i actualitza master per apuntar a aquest commit.
Si executem git status, no veurem canvis, perquè els tres arbres són iguals.
Ara volem fer un canvi a aquest fitxer i confirmar-lo. Passarem pel mateix procés; primer, canviem el fitxer al nostre directori de treball. Anomenem això v2 del fitxer, i l’indiquem en vermell.
Si executem git status ara mateix, veurem el fitxer en vermell com a "Canvis no preparats per al commit", perquè aquesta entrada difereix entre l’índex i el directori de treball. A continuació executem git add per preparar-lo al nostre índex.
En aquest punt, si executem git status, veurem el fitxer en verd sota "Canvis a confirmar" perquè l’índex i HEAD difereixen, és a dir, la nostra proposta de següent commit és ara diferent del nostre últim commit. Finalment, executem git commit per finalitzar el commit.
Ara git status no ens donarà cap sortida, perquè els tres arbres són iguals de nou.
Canviar de branques o clonar passa per un procés similar. Quan revises una branca, canvia el HEAD per apuntar a la nova referència de branca, omple el teu índex amb l’instantània d’aquest commit, i després copia el contingut de l'índex al teu directori de treball.
El comandament reset té més sentit quan es veu en aquest context.
Per als propòsits d’aquests exemples, suposem que hem modificat file.txt una altra vegada i l’hem confirmat una tercera vegada. Així que ara el nostre historial sembla així:
Ara passem per tot el que fa reset quan el crides. Manipula directament aquests tres arbres d’una manera senzilla i previsible. Fa fins a tres operacions bàsiques.
La primera cosa que farà reset és moure a què apunta HEAD. Això no és el mateix que canviar HEAD en si mateix (que és el que fa checkout); reset mou la branca a la qual apunta HEAD. Això significa que si HEAD està establert a la branca master (és a dir, ets actualment a la branca master), executar git reset 9e5e6a4 començarà fent que master apunti a 9e5e6a4.
No importa quina forma de reset amb un commit invoquis, aquesta és la primera cosa que sempre intentarà fer. Amb reset --soft, simplement s’aturarà aquí.
Ara pren-te un segon per mirar aquest diagrama i adonar-te del que ha passat: bàsicament ha desfet l’últim comandament git commit. Quan executes git commit, Git crea un nou commit i mou la branca a la qual apunta HEAD cap amunt. Quan fas reset fins a HEAD~ (el pare de HEAD), estàs movent la branca cap enrere a on estava, sense canviar l’índex o el directori de treball. Ara podries actualitzar l’índex i executar git commit una altra vegada per aconseguir el que hauria fet git commit --amend (vegeu [_git_amend]).
Nota que si executes git status ara veuràs en verd la diferència entre l’índex i el que és el nou HEAD.
La següent cosa que farà reset és actualitzar l’índex amb el contingut de qualsevol instantània a la qual apunti ara HEAD.
Si especifiques l’opció --mixed, reset s’aturarà en aquest punt. Aquesta també és l’opció per defecte, així que si no especifiques cap opció (només git reset HEAD~ en aquest cas), aquí és on s’aturarà el comandament.
Ara pren-te un altre segon per mirar aquest diagrama i adonar-te del que ha passat: encara ha desfet el teu últim commit, però també ha despreparat tot. Has tornat enrere abans d’executar tots els teus comandaments git add i git commit.
La tercera cosa que farà reset és fer que el directori de treball sembli l’índex. Si utilitzes l’opció --hard, continuarà fins a aquesta etapa.
Així que pensem en el que acaba de passar. Has desfet el teu últim commit, els comandaments git add i git commit, i tot el treball que vas fer al teu directori de treball.
És important notar que aquesta bandera (--hard) és l’única manera de fer que el comandament reset sigui perillós, i un dels pocs casos en què Git destruirà realment dades. Qualsevol altra invocació de reset es pot desfer bastant fàcilment, però l’opció --hard no, ja que sobreescriu forçosament fitxers al directori de treball. En aquest cas particular, encara tenim la versió v3 del nostre fitxer en un commit a la nostra base de dades de Git, i podríem recuperar-la mirant el nostre reflog, però si no l’haguéssim confirmat, Git encara hauria sobreescrit el fitxer i seria irrecuperable.
El comandament reset sobreescriu aquests tres arbres en un ordre específic, aturant-se quan li dius que ho faci:
-
Mou la branca a la qual apunta HEAD (atura’t aquí si és `--soft).
-
Fes que l’índex sembli HEAD (atura’t aquí a menys que sigui `--hard).
-
Fes que el directori de treball sembli l’índex.
Això cobreix el comportament de reset en la seva forma bàsica, però també pots proporcionar-li una ruta per actuar. Si especifiques una ruta, reset saltarà el pas 1, i limitarà la resta de les seves accions a un fitxer o conjunt de fitxers específic. Això en realitat té una mica de sentit: HEAD és només un punter, i no pots apuntar a part d’un commit i part d’un altre. Però l’índex i el directori de treball poden ser actualitzats parcialment, així que reset continua amb els passos 2 i 3.
Així que, suposem que executem git reset file.txt. Aquesta forma (ja que no has especificat un SHA-1 de commit o branca, i no has especificat --soft o --hard) és una abreviatura de git reset --mixed HEAD file.txt, que:
-
Mou la branca a la qual apunta HEAD (saltat).
-
Fes que l’índex sembli HEAD (atura’t aquí).
Així que bàsicament només copia file.txt de HEAD a l’índex.
Això té l’efecte pràctic de despreparar el fitxer. Si mirem el diagrama d’aquest comandament i pensem en el que fa git add, són oposats exactes.
Això és per què la sortida del comandament git status suggereix que executis això per despreparar un fitxer (vegeu ch02-git-basics-chapter.asc per a més informació sobre això).
Podríem simplement no deixar que Git suposi que volíem dir "treure les dades de HEAD" especificant un commit específic per treure aquesta versió del fitxer. Simplement executaríem alguna cosa com git reset eb43bf file.txt.
Això efectivament fa el mateix que si haguéssim revertit el contingut del fitxer a v1 al directori de treball, haguéssim executat git add sobre ell, i després l’haguéssim revertit de nou a v3 (sense passar realment per tots aquests passos). Si executem git commit ara, registrarà un canvi que reverteix aquest fitxer de nou a v1, encara que mai no l’haguéssim tingut de nou al nostre directori de treball.
També és interessant notar que, com git add, el comandament reset acceptarà una opció --patch per despreparar contingut per trossos. Així que pots despreparar o revertir contingut de manera selectiva.
Mirem com fer alguna cosa interessant amb aquest nou poder trobat: aplanar commits.
Digues que tens una sèrie de commits amb missatges com "oops.", "WIP" i "m’he oblidat d’aquest fitxer". Pots utilitzar reset per aplanar-los ràpidament i fàcilment en un únic commit que et faci semblar molt intel·ligent. [_squashing] mostra una altra manera de fer això, però en aquest exemple és més senzill utilitzar reset.
Digues que tens un projecte on el primer commit té un fitxer, el segon commit afegeix un nou fitxer i canvia el primer, i el tercer commit canvia el primer fitxer una altra vegada. El segon commit era un treball en procés i vols aplanar-lo.
Pots executar git reset --soft HEAD~2 per moure la branca HEAD enrere a un commit més antic (el commit més recent que vols mantenir):
I llavors simplement executar git commit una altra vegada:
Ara pots veure que el teu historial accessible, l’historial que enviaries, ara sembla que vas tenir un commit amb file-a.txt v1, i després un segon que va modificar file-a.txt a v3 i va afegir file-b.txt. El commit amb la versió v2 del fitxer ja no està a l’historial.
Finalment, potser et preguntes quina és la diferència entre checkout i reset. Com reset, checkout manipula els tres arbres, i és una mica diferent segons si li donis al comandament una ruta de fitxer o no.
Executar git checkout [branca] és bastant similar a executar git reset --hard [branca] en el sentit que actualitza els tres arbres perquè semblin [branca], però hi ha dues diferències importants.
En primer lloc, a diferència de reset --hard, checkout és segur per al directori de treball; comprovarà que no està eliminant fitxers que han estat modificats. De fet, és una mica més intel·ligent: intenta fer una fusió trivial al directori de treball, de manera que tots els fitxers que no has modificat es actualitzaran. reset --hard, d’altra banda, simplement reemplaçarà tot sense comprovar.
La segona diferència important és com checkout actualitza HEAD. Mentre que reset mourà la branca a la qual apunta HEAD, checkout mourà HEAD mateix per apuntar a una altra branca.
Per exemple, suposem que tenim branques master i develop que apunten a diferents commits, i estem actualment a develop (és a dir, HEAD apunta a ella). Si executem git reset master, develop mateix es mourà per apuntar al mateix commit que master. Si en canvi executem git checkout master, develop no es mou, HEAD ho fa. HEAD apuntarà ara a master.
Així que, en tots dos casos estem movent HEAD per apuntar al commit A, però com ho fem és molt diferent. reset mourà la branca a la qual apunta HEAD, checkout mou HEAD mateix.
L’altra manera d’executar checkout és amb una ruta de fitxer, que, com reset, no mou HEAD. És exactament com git reset [branca] fitxer en el sentit que actualitza l’índex amb aquest fitxer en aquest commit, però també sobreescriu el fitxer al directori de treball. Seria exactament com git reset --hard [branca] fitxer (si reset et deixés executar això) — no és segur per al directori de treball, i no mou HEAD.
A més, com git reset i git add, checkout acceptarà una opció --patch per permetre’t revertir selectivament continguts de fitxers per trossos.
Espera que ara entenguis i et sentis més còmode amb el comandament reset, però probablement encara estiguis una mica confós sobre com difereix exactament de checkout i no podries recordar totes les regles de les diferents invocacions.
Aquí tens una fulla de trucs per a quins comandaments afecten quins arbres. La columna "HEAD" diu "REF" si aquest comandament mou la referència (branca) a la qual apunta HEAD, i "HEAD" si mou HEAD mateix. Presta especial atenció a la columna 'WD Safe?': si diu NO, pren-te un segon per pensar abans d’executar aquest comandament.
| | HEAD | Index | Workdir | WD Safe? |
|---|------|-------|---------|----------|
| Nivell de Commit | | | | |
| reset --soft [commit] | REF | NO | NO | YES |
| reset [commit] | REF | YES | NO | YES |
| reset --hard [commit] | REF | YES | YES | NO |
| checkout <commit> | HEAD | YES | YES | YES |
| Nivell de Fitxer | | | | |
| reset [commit] <paths> | NO | YES | NO | YES |
| checkout [commit] <paths> | NO | YES | YES | NO |

















