La lliçó anterior va deixar el catàleg muntat i va resoldre els casos d'un minut. Ara toca l'ordre que apareixia a mitja dotzena de files d'aquella taula i que portem cinc mòduls prometent: git reset.

És probablement l'ordre pitjor entesa de Git. Té tres modes que fan coses diferents, dues formes d'invocar-la que es comporten de manera completament diferent, i una fama de perillosa que és merescuda només en un dels seus usos. La confusió no és casual: reset fa dues feines que en altres sistemes serien dues ordres diferents.

Aquesta lliçó el desmunta peça a peça i després el posa en context. Perquè reset no és la resposta a "vull desfer alguna cosa": és una de les respostes, i triar l'equivocada és el que converteix un canvi no desitjat en un problema per a tot l'equip. Al final tindràs una taula de decisió que respon l'única pregunta que importa: on és allò que vull desfer?

I un advertiment que val per a tota la lliçó: aquí apareix l'única ordre de Git que destrueix de debò, sense cap xarxa de seguretat possible. No és reset. És git clean.

Contingut

  1. Les tres zones, una altra vegada
  2. git reset mou la branca: la idea central
  3. Els tres modes: --soft, --mixed, --hard
  4. Casos ordenats pel que vols desfer
  5. git reset <commit> -- <ruta>: l'altra forma
  6. git restore: l'ordre moderna per a la feina diària
  7. git clean: el que sí que destrueix
  8. ORIG_HEAD: la xarxa de seguretat immediata
  9. La taula de decisió

  1. Les tres zones, una altra vegada

Tot reset s'entén tan bon punt es recuperen les tres zones de la lliçó 01-03. Val la pena tenir-les al davant:

flowchart LR
    WD["Directori de treball<br/>(els fitxers que edites)"]
    IDX["Índex / staging<br/>(.git/index)"]
    HEAD["HEAD → branca → commit<br/>(la base de dades)"]

    WD -->|"git add"| IDX
    IDX -->|"git commit"| HEAD
    HEAD -->|"git reset --hard"| WD
    HEAD -->|"git reset --mixed"| IDX

Tres fotos del projecte que poden coincidir o no:

Zona Què conté On viu
HEAD L'últim commit de la branca actual .git/refs/heads/<branca> (41 bytes)
Índex El que entraria al pròxim commit .git/index (binari)
Directori de treball Els fitxers reals al disc La teva carpeta

Quan git status diu "Changes to be committed", està comparant índex amb HEAD. Quan diu "Changes not staged for commit", compara directori de treball amb índex. Amb això clar, reset deixa de ser un misteri: cada mode decideix fins a quina d'aquestes tres zones arriba el canvi.

  1. git reset mou la branca: la idea central

Abans dels modes, l'operació bàsica. Això és el que fa git reset <commit> sempre, en els tres modes:

Mou la branca actual perquè apunti a <commit>. I com que HEAD apunta a la branca, HEAD es mou amb ella.

No esborra commits. No modifica commits. Reescriu un fitxer de 41 bytes (lliçó 03-01).

flowchart LR
    subgraph A["Abans"]
        direction LR
        a1["e91d4a8"] --> a2["4f8a2e6"] --> a3["b52c9d1"] --> a4["7d3a8f4"]
        ra(["GT-241"]) -.-> a4
        ha(["HEAD"]) -.-> ra
    end
    subgraph B["git reset HEAD~2"]
        direction LR
        b1["e91d4a8"] --> b2["4f8a2e6"] --> b3["b52c9d1<br/>(inassolible)"] --> b4["7d3a8f4<br/>(inassolible)"]
        rb(["GT-241"]) -.-> b2
        hb(["HEAD"]) -.-> rb
    end
    A ==> B

b52c9d1 i 7d3a8f4 continuen existint íntegres a .git/objects. El que ha canviat és que GT-241 ja no hi arriba, així que git log no els mostra. Recuperar-los és trivial mentre el reflog els recordi (lliçó 09-04).

D'aquí surten les dues conseqüències que cal interioritzar:

1. reset és segur sobre feina local. Encara que t'equivoquis, els commits continuen allà.

2. reset és perillós sobre feina publicada. Perquè moure la branca enrere i tornar a avançar produeix una història diferent de la que tenen els altres. El push serà rebutjat (non-fast-forward), i forçar-lo destruiria la feina dels altres. És la regla d'or de la lliçó 05-01, i el seu tractament complet és la lliçó 09-03.

Un tercer matís que gairebé ningú no coneix i que evita molts ensurts:

git reset            # sense commit: equival a git reset --mixed HEAD

Sense argument de commit, reset fa servir HEAD, és a dir, no mou res. Només actua sobre l'índex. Per això git reset a seques és completament inofensiu: buida l'àrea de preparació i deixa els teus fitxers intactes.

  1. Els tres modes: --soft, --mixed, --hard

La diferència entre els tres és fins on propaga el canvi.

Mode Mou HEAD/branca Reescriu l'índex Reescriu el directori de treball Destrueix feina?
--soft No No No
--mixed (per defecte) No No
--hard : el que no s'ha confirmat
--merge Parcialment (conserva canvis locals no conflictius) Rarament
--keep Parcialment (avorta si hi ha conflicte) No

Visualment, sobre les tres zones:

flowchart TD
    subgraph S["--soft"]
        s1["HEAD ⟵ es mou"]
        s2["Índex: intacte"]
        s3["Feina: intacta"]
    end
    subgraph M["--mixed (per defecte)"]
        m1["HEAD ⟵ es mou"]
        m2["Índex ⟵ es reescriu"]
        m3["Feina: intacta"]
    end
    subgraph H["--hard"]
        h1["HEAD ⟵ es mou"]
        h2["Índex ⟵ es reescriu"]
        h3["Feina ⟵ SE SOBREESCRIU"]
    end

--soft: desfer el commit, conservar-ho tot preparat

git reset --soft HEAD~1
git status --short
M  app.js
A  estils.css

El commit ha desaparegut de la branca, i tot el seu contingut és preparat, a punt per tornar-se a confirmar. Els M i A amb la lletra a la primera columna signifiquen "a l'índex".

És el mode de "vull refer aquest commit". De fet, és exactament el que fa git commit --amend per dins.

--mixed: desfer el commit i despreparar

git reset HEAD~1          # --mixed és el valor per defecte
git status --short
 M app.js
?? estils.css

Ara la lletra és a la segona columna: els canvis són al directori de treball, sense preparar. I estils.css, que era nou, ha tornat a ser un fitxer sense seguiment.

És el mode de "vull refer aquest commit i tornar a triar què hi entra". És el que vas fer servir a la lliçó 05-02 per dividir un commit en dos.

--hard: desfer el commit i el contingut

git reset --hard HEAD~1
git status
On branch GT-241
nothing to commit, working tree clean

Tot net, com si aquell commit no hagués existit mai. I aquí hi ha el perill, que convé enunciar amb precisió:

--hard no destrueix els commits (continuen a la base de dades). Destrueix els canvis sense confirmar que hi hagués en aquell moment a l'índex i al directori de treball.

Això és el que no té marxa enrere, perquè mai no va ser a .git/objects (lliçó 09-01, taula de l'apartat 1).

Una excepció important i tranquil·litzadora: --hard no toca els fitxers sense seguiment. Si tenies un notes.txt que no vas afegir mai, continua allà. El que s'emporta per davant són les modificacions de fitxers seguits.

La comparació en un sol experiment

# Punt de partida: un commit, i a més un canvi sense confirmar
git log --oneline -2
git status --short
7d3a8f4 (HEAD -> GT-241) GT-241 filtre de pendents
b52c9d1 GT-241 base del filtre
 M app.js
Ordre git log -1 app.js (el meu canvi sense confirmar) Contingut del commit desfet
git reset --soft HEAD~1 b52c9d1 Conservat A l'índex
git reset --mixed HEAD~1 b52c9d1 Conservat (barrejat) Al directori de treball
git reset --hard HEAD~1 b52c9d1 PERDUT Descartat

La fila del --hard és la que cal memoritzar: s'emporta dues coses alhora, el commit i la teva feina en curs. I només una de les dues es pot recuperar.

--merge i --keep: els modes que gairebé ningú no fa servir

Existeixen i en dues situacions són exactament el que vols:

# Mou la branca però intenta CONSERVAR els meus canvis locals
git reset --keep <commit>

--keep és un --hard educat: mou la branca i actualitza els fitxers que difereixen, però avorta si això trepitjaria un canvi local. És l'opció sensata quan vols retrocedir i no estàs segur de tenir-ho tot confirmat:

error: Entry 'app.js' not uptodate. Cannot merge.

Aquest error, aquí, és una bona notícia: t'ha salvat.

--merge és semblant però intenta fusionar els canvis locals a la destinació. Es fa servir sobretot per avortar una fusió a mà.

Regla pràctica: quan tinguis la temptació d'escriure --hard i no n'estiguis segur, escriu --keep. Si funciona, no hi havia res a perdre; si falla, acabes d'evitar el problema.

  1. Casos ordenats pel que vols desfer

Aquest és el receptari. Està ordenat de menor a major abast.

4.1. Descartar canvis d'un fitxer al directori de treball

L'Ana ha estat provant coses a estils.css i vol tornar a com estava.

git restore estils.css               # forma moderna (Git 2.23+)
git checkout -- estils.css           # forma antiga, equivalent

Això sí que destrueix, i sense xarxa: les modificacions no confirmades no són enlloc. Git ni tan sols pregunta.

# Abans de destruir, mira què perdràs
git diff estils.css

# Tot el directori de treball
git restore .

4.2. Treure alguna cosa de l'índex sense perdre el canvi

El Bruno ha fet git add . i hi ha ficat sense voler notes-personals.md.

git restore --staged notes-personals.md      # forma moderna
git reset notes-personals.md                 # forma antiga, equivalent

No has perdut res: el fitxer continua modificat al directori de treball, només deixa d'estar preparat.

git status --short
M  app.js
?? notes-personals.md

Fixa't en l'asimetria, que és la font de confusió número u:

Treu de l'índex Destrueix el canvi
git restore --staged <f> No
git restore <f> No
git restore --staged --worktree <f>

git restore sense opcions actua sobre el directori de treball. Amb --staged, sobre l'índex. És el contrari del que molta gent assumeix, i és la raó que git restore mereixi el seu propi apartat (el 6).

4.3. Desfer l'últim commit conservant els canvis

El cas més freqüent de tots. La Carla ha confirmat i s'ha adonat que el commit barreja dues coses.

git reset --soft HEAD~1

No has perdut res. El commit ja no és a la branca, però tot el seu contingut és preparat. Ara el pot refer:

git status --short
M  app.js
M  estils.css

Si a més vol tornar a triar què entra a cada commit, --mixed i git add -p (lliçó 02-04):

git reset HEAD~1
git add -p app.js            # triar tros a tros
git commit -m "GT-241 calcula el comptador sobre la llista completa"
git add .
git commit -m "GT-241 estils de l'indicador de pendents"

Aquest és el flux complet de "he barrejat dues coses en un commit", i és el que es fa servir a diari.

4.4. Només vull canviar el missatge

git commit --amend

No cal reset per a això. --amend substitueix l'últim commit (lliçó 02-04), i amb -m ni tan sols obre l'editor:

git commit --amend -m "GT-241 calcula el comptador sobre la llista completa"

Recorda que el hash canvia: si estava publicat, això reescriu historial.

4.5. Llençar els últims N commits

git reset --hard HEAD~3

Tres commits fora de la branca. Abans d'executar-ho, dos costums que costen cinc segons:

# 1. Anota on ets
git log --oneline -1
git rev-parse HEAD > /tmp/on-era.txt

# 2. O millor: posa un marcador. És gratis i no s'oblida
git branch copia-GT-241

Amb git branch copia-GT-241, el --hard deixa de ser irreversible en cap sentit: els commits continuen sent assolibles des de la còpia. És el mateix patró que a la lliçó 09-01, apartat 5.2, i el que es generalitza a la 09-04.

I si ja ho vas fer sense còpia: tampoc no passa res, mentre no fossin canvis sense confirmar.

git reflog -5                       # busca el hash anterior al reset
git reset --hard b52c9d1            # o millor: git branch rescat b52c9d1

4.6. Tornar la branca exactament a com està al remot

git fetch origin
git reset --hard origin/main

És el "descarta tot el meu i deixa'm com el servidor". Útil quan la teva còpia local s'ha embolicat sense remei i saps que no hi ha res teu a salvar.

Comprova abans què llençaràs:

git log --oneline origin/main..HEAD      # commits meus que no són al remot
git status --short                       # canvis sense confirmar

Si la primera llista no és buida, no executis el --hard sense crear abans una branca de còpia.

4.7. Desfer alguna cosa que ja està publicada

Aquí reset no és la resposta. Reescriure una branca publicada obliga a forçar el push i destrueix la feina de qui tingui la versió anterior.

La resposta és git revert (lliçó 05-06): crea un commit nou amb el canvi invers, no reescriu res, no cal forçar, i en queda constància.

git revert 4f8a2e6
git push

L'única excepció: una branca publicada que és exclusivament teva (la teva branca de treball GT-241, que ningú més no fa servir). Aquí sí que pots reescriure i forçar amb --force-with-lease, i això és la lliçó 09-03.

  1. git reset <commit> -- <ruta>: l'altra forma

Aquí hi ha la font principal de confusió amb reset, i mereix un apartat propi.

Quan hi afegeixes una ruta, git reset fa una cosa completament diferent:

git reset <commit> -- <ruta> NO mou la branca. Copia l'estat de <ruta> a <commit> a l'índex, i deixa el directori de treball intacte.

Forma Mou la branca? Toca l'índex? Toca el directori de treball?
git reset <commit> Segons el mode Segons el mode
git reset <commit> -- <ruta> No, mai Sí, només aquesta ruta No, mai

Per què no pot moure la branca: moure HEAD a un altre commit és una operació sobre el repositori sencer. No existeix "moure la branca només per a un fitxer". Així que quan dones una ruta, Git entén que vols l'altra operació: omplir l'índex des d'un commit.

Conseqüència directa: git reset <commit> -- <ruta> no accepta --hard.

git reset --hard HEAD~1 -- app.js
fatal: Cannot do hard reset with paths.

Precisament perquè el mode --hard implica tocar el directori de treball, i aquesta forma no ho fa mai.

Per a què serveix realment

És la manera de preparar el contingut antic d'un fitxer, típicament per desfer només una part d'un commit:

# El commit 4f8a2e6 va tocar tres fitxers; vull desfer només el d'estils.css
git reset 4f8a2e6~1 -- estils.css
git status --short
M  estils.css

L'índex té ara la versió anterior d'estils.css, preparada. Un git commit crea un commit que desfà només aquesta part.

I si a més vols que el fitxer al disc canviï, la forma moderna i molt més clara és git restore:

git restore --source=4f8a2e6~1 --staged --worktree estils.css

El cas "aquest fitxer no hauria de ser al repositori"

Reprenent la lliçó 08-03:

git rm --cached configuracio-local.json

git rm --cached treu el fitxer de l'índex (deixa de tenir seguiment) però el conserva al disc. És l'operació que cal fer quan alguna cosa va entrar al repositori i després la vas afegir al .gitignore. És una cosina germana de git reset -- <ruta>, però per deixar de seguir el fitxer en lloc de restaurar-ne el contingut.

  1. git restore: l'ordre moderna per a la feina diària

Git 2.23 va partir el vell git checkout en dues ordres amb noms honestos: git switch per a les branques (lliçó 03-02) i git restore per als fitxers. És l'eina que hauries de fer servir a diari, deixant reset per al que de debò implica moure la branca.

# Descartar canvis del directori de treball
git restore app.js
git restore .

# Treure de l'índex (conservant el canvi al disc)
git restore --staged app.js

# Totes dues coses: deixar el fitxer com a HEAD, sense rastre del canvi
git restore --staged --worktree app.js

# Portar un fitxer des d'un altre commit o branca
git restore --source=HEAD~3 app.js
git restore --source=origin/main estils.css

# Interactiu: triar tros a tros què descartar
git restore -p app.js

La taula d'equivalències amb el que és antic, perquè veuràs les dues formes a la documentació i a les respostes de fòrum:

Objectiu Modern Antic
Descartar canvis al disc git restore <f> git checkout -- <f>
Treure de l'índex git restore --staged <f> git reset HEAD <f>
Portar un fitxer d'un altre commit git restore --source=<c> <f> git checkout <c> -- <f>
Canviar de branca git switch <branca> git checkout <branca>
Crear i canviar git switch -c <branca> git checkout -b <branca>

git restore -p mereix una menció a part. És el germà de git add -p: t'ensenya cada fragment del diff i et pregunta si el vols descartar. És la manera quirúrgica de desfer part d'un canvi sense perdre la resta, i evita el "descartaré tot i ho refaré".

git restore -p app.js
@@ -12,6 +12,9 @@ function renderitzaTasques() {
   llista.innerHTML = '';
+  console.log('DEBUG tasques', tasques);
   tasques.forEach(tasca => {
Discard this hunk from worktree [y,n,q,a,d,e,?]?

Perfecte per treure els console.log de depuració conservant la feina real.

  1. git clean: el que sí que destrueix

Tot l'anterior té xarxa de seguretat en algun grau. Aquesta ordre no en té cap, i per això mereix el to més seriós de la lliçó.

git clean esborra fitxers sense seguiment. I un fitxer sense seguiment no ha passat mai per .git/objects: no hi ha commit, no hi ha blob, no hi ha reflog, no hi ha fsck que valgui. Quan git clean l'esborra, està esborrat, igual que amb rm.

La regla absoluta: -n primer, sempre

git clean -n
Would remove bolcat.sql
Would remove captures/
Would remove notes-personals.md

-n (o --dry-run) no esborra res: només llista. Llegeix-ho sencer. Si en aquesta llista apareix alguna cosa que t'importa, no executis l'ordre real.

Aquest costum no és opcional. És la diferència entre netejar el projecte i perdre el fitxer de configuració local que portes sis mesos mantenint a mà.

Les opcions

Opció Què fa Perill
-n / --dry-run Només llista el que esborraria Cap
-f / --force Esborra de debò (obligatòria: sense ella no fa res) Alt
-d Inclou directoris sense seguiment Alt
-x Inclou també el que està ignorat pel .gitignore Molt alt
-X Esborra només el que està ignorat, conservant la resta Mitjà
-i / --interactive Pregunta fitxer a fitxer Baix
-e <patró> Exclou el que coincideixi

Els usos habituals:

# El que gairebé sempre vols: veure, i després esborrar fitxers i directoris nous
git clean -nd
git clean -fd

# Deixar el projecte com acabat de clonar (també esborra node_modules, .env, builds!)
git clean -ndx
git clean -fdx

# Esborrar només el que està ignorat: netejar artefactes de compilació conservant la resta
git clean -fdX

# El mode segur quan dubtes
git clean -id

-x és la que cal mirar dues vegades. Esborra el que el .gitignore protegeix, i allà sol haver-hi precisament el que no és al repositori però sí que t'importa: .env amb la configuració local, credencials de desenvolupament, bases de dades de prova.

El mode interactiu

git clean -id
Would remove the following items:
  captures/  notes-personals.md  bolcat.sql

*** Commands ***
    1: clean                2: filter by pattern    3: select by numbers
    4: ask each             5: quit                 6: help
What now> 4

L'opció 4 (ask each) pregunta un per un. Quan hi ha molts fitxers i només en sobren alguns, és la manera correcta.

El "reset total" i el seu cost

La combinació que circula per internet com a "deixar-ho tot net":

git reset --hard HEAD && git clean -fdx

Això deixa el directori exactament com un clon acabat de fer d'aquell commit. I s'emporta per davant, sense preguntar i sense recuperació possible:

  • Tots els canvis sense confirmar de fitxers seguits.
  • Tots els fitxers nous que no havies afegit.
  • Tot el que està ignorat: .env, node_modules/, configuracions locals, bases de dades de prova.

És una ordre legítima —en CI, en un contenidor, en un repositori d'usar i llençar— i una catàstrofe a la màquina d'algú que portava dues hores treballant. No l'enganxis mai sense haver executat abans git clean -ndx i llegit la llista.

L'única prevenció que funciona

# Abans de qualsevol neteja dubtosa
git stash push -u -m "per si de cas"

git stash -u (lliçó 05-04) desa també els fitxers sense seguiment, i els desa com a objectes de Git. Amb això, el que anaves a esborrar passa a ser a la base de dades i, per tant, deixa de ser irrecuperable. Costa un segon.

  1. ORIG_HEAD: la xarxa de seguretat immediata

Abans d'una operació que mou HEAD de manera substancial —reset, merge, rebase, pull—, Git desa la posició anterior en una referència especial:

cat .git/ORIG_HEAD
git rev-parse ORIG_HEAD

Així que el "desfer" immediat d'un reset és:

git reset --hard ORIG_HEAD

O, seguint el patró segur:

git branch rescat ORIG_HEAD

Dos advertiments sobre ORIG_HEAD:

  • Només desa una posició, la de l'última operació. Si fas dos reset seguits, la primera es perd. Per a això hi ha el reflog, que desa tot l'historial de moviments (lliçó 09-04).
  • No recupera canvis sense confirmar. ORIG_HEAD és un hash de commit: torna la branca al seu lloc, no la teva feina en curs.

És la xarxa de seguretat dels trenta segons següents. Per a tota la resta, la 09-04.

  1. La taula de decisió

La pregunta correcta no és "quina ordre faig servir per desfer?", sinó "on és allò que vull desfer?". Amb aquesta resposta, l'ordre és immediata.

flowchart TD
    Q1{"Està confirmat?"}
    Q1 -->|No| Q2{"Està preparat<br/>(git add)?"}
    Q1 -->|Sí| Q3{"Està publicat<br/>al remot?"}

    Q2 -->|No| R1["git restore &lt;f&gt;<br/>(o git clean si no té seguiment)"]
    Q2 -->|Sí| R2["git restore --staged &lt;f&gt;"]

    Q3 -->|No| Q4{"Només l'últim<br/>commit?"}
    Q3 -->|Sí| R5["git revert (05-06)<br/>o --force-with-lease si la branca és teva (09-03)"]

    Q4 -->|Sí| R3["git commit --amend<br/>o git reset --soft HEAD~1"]
    Q4 -->|No| R4["git reset HEAD~N<br/>o git rebase -i (05-02)"]

I en forma de taula, que és com es consulta en el moment:

Vull desfer... Estat Ordre Recuperable?
Canvis en un fitxer editat Sense confirmar git restore <f> No
Part dels canvis d'un fitxer Sense confirmar git restore -p <f> No
Un git add Preparat git restore --staged <f> Sí (no es perd res)
Un fitxer nou que sobra Sense seguiment git clean -n i després -f No
L'últim commit, conservant la feina Sense publicar git reset --soft HEAD~1 Sí (reflog)
El missatge de l'últim commit Sense publicar git commit --amend Sí (reflog)
Un fitxer oblidat a l'últim commit Sense publicar git add <f> && git commit --amend --no-edit Sí (reflog)
Els últims N commits i el seu contingut Sense publicar git reset --hard HEAD~N Sí els commits; no el que no s'ha confirmat
Un commit del mig de la branca Sense publicar git rebase -i amb drop (05-02) Sí (reflog)
Només un fitxer d'un commit Sense publicar git reset <c>~1 -- <f> i confirmar
Un commit ja publicat Publicat git revert <sha> (05-06) Sí (és additiu)
Una fusió ja publicada Publicat git revert -m 1 <sha> (05-06)
Un commit a la meva branca personal publicada Publicat, branca pròpia reset + push --force-with-lease (09-03) Sí, avisant
Tot: tornar a l'estat del remot Qualsevol git fetch && git reset --hard origin/<branca> Els commits sí; el que no s'ha confirmat no
Tot, inclosos fitxers nous Qualsevol git reset --hard && git clean -fdx No el que no s'ha confirmat

La fila que governa totes les altres és la de "està publicat?". Si la resposta és sí i la branca és compartida, reset surt de la llista i l'única eina correcta és revert.

Errors Habituals i Consells

Error 1: creure que reset --hard esborra commits. No els esborra: mou la branca. Els commits continuen a .git/objects i el reflog els troba (09-04). El que sí que destrueix són els canvis sense confirmar.

Error 2: fer servir --hard quan n'hi havia prou amb --soft. Si només volies refer el commit, --soft ho conserva tot preparat. --hard s'emporta a més la teva feina en curs.

Error 3: confondre git restore <f> amb git restore --staged <f>. El primer destrueix el canvi al disc; el segon només el treu de l'índex i no perd res. Són gairebé oposats.

Error 4: esperar que git reset <commit> -- <ruta> mogui la branca. No ho fa mai, i per això --hard amb rutes dona error. És una altra operació amb el mateix nom.

Error 5: executar git clean -fdx copiat d'internet. Esborra .env, configuracions locals i tot el que està ignorat, sense recuperació possible. -n primer, sempre.

Error 6: fer reset en una branca compartida i forçar el push. Destrueix la feina de qui tingués els commits. En el que està publicat, revert (05-06).

Error 7: fer servir git reset --hard origin/main sense comprovar què hi ha de teu al davant. git log --oneline origin/main..HEAD costa un segon i et diu exactament què llençaràs.

Error 8: oblidar que --hard respecta els fitxers sense seguiment. No és un error greu, però explica el "he fet reset --hard i això continua aquí": no tenia seguiment.

Consell 1: quan dubtis entre --hard i no fer res, fes servir --keep. Fa el mateix, però avorta en lloc de trepitjar canvis locals.

Consell 2: git branch copia-<alguna-cosa> abans de qualsevol reset --hard. Costa dos segons i converteix l'operació en trivialment reversible.

Consell 3: fes servir git restore i git switch en lloc de git checkout. Els noms diuen el que fan i eviten errors per confusió.

Consell 4: git restore -p per treure els console.log. Quirúrgic, i conserva la resta de la feina.

Consell 5: git stash push -u abans de qualsevol neteja. Converteix l'irrecuperable en recuperable per un segon d'esforç.

Consell 6: recorda ORIG_HEAD. El desfer immediat d'un reset, merge, rebase o pull que acabes de lamentar.

Exercicis

Exercici 1: els tres modes, al mateix punt de partida

  1. Crea un repositori amb app.js i tres commits. Al tercer, afegeix-hi també estils.css.
  2. Modifica app.js sense confirmar i crea un fitxer nou notes.txt sense afegir-lo.
  3. Executa git reset --soft HEAD~1 i anota la sortida de git status --short i git log --oneline -1.
  4. Desfés-ho amb git reset --hard ORIG_HEAD... i observa què ha passat amb la teva modificació d'app.js. Explica-ho.
  5. Repeteix l'experiment complet amb --mixed i amb --hard, i omple una taula amb tres columnes: log -1, canvi sense confirmar a app.js, i notes.txt.
  6. Explica per què notes.txt sobreviu fins i tot a --hard.

Exercici 2: reset amb ruta contra reset sense ruta

  1. Sobre un repositori amb quatre commits, on el tercer va modificar app.js, estils.css i index.html, prova git reset --hard HEAD~1 -- app.js i explica l'error.
  2. Fes servir git reset HEAD~2 -- estils.css i comprova amb git status, git diff i git diff --cached en quina zona ha quedat el canvi.
  3. Confirma. Comprova que has desfet només la part d'estils.css del tercer commit i que la resta continua al projecte.
  4. Aconsegueix el mateix resultat amb git restore --source=... --staged --worktree i compara l'estat del directori de treball en tots dos casos.

Exercici 3: el límit real de la recuperació

  1. En un repositori de proves, crea tres situacions alhora: un commit sense publicar, un canvi preparat amb git add, i un canvi només al disc.
  2. Executa git reset --hard HEAD~1.
  3. Recupera el commit fent servir el reflog.
  4. Intenta recuperar el canvi preparat amb git fsck --lost-found i git cat-file -p.
  5. Intenta recuperar el canvi que era només al disc. Documenta per què no és possible.
  6. Repeteix tot l'exercici executant abans git stash push -u i explica què canvia.

Solucions

Solució 1:

mkdir /tmp/p9-02 && cd /tmp/p9-02 && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"

echo "// gestor-tasques" > app.js && git add . && git commit -q -m "chore: inici"
echo "// llistat" >> app.js && git commit -q -am "feat: llistat"
echo "// filtre" >> app.js && echo "body{margin:0}" > estils.css
git add . && git commit -q -m "feat: filtre i estils"

# 2. Estat brut abans del reset
echo "// FEINA SENSE CONFIRMAR" >> app.js
echo "notes privades" > notes.txt
git status --short
 M app.js
?? notes.txt
# 3. --soft
git reset --soft HEAD~1
git log --oneline -1
git status --short
b52c9d1 feat: llistat
M  app.js
A  estils.css
?? notes.txt

El commit ha sortit de la branca i el seu contingut és preparat (M i A a la primera columna). I aquí hi ha un detall fi: app.js apareix una sola vegada perquè l'índex conté tant el del commit desfet com la teva modificació sense confirmar encara fora... en realitat la modificació d'app.js continua al directori de treball, i l'índex té la versió del commit desfet. Comprova-ho:

git diff --cached --stat     # el del commit desfet, preparat
git diff --stat              # la teva modificació sense confirmar, encara pendent
 app.js     | 1 +
 estils.css | 1 +
 app.js | 1 +

Res no s'ha perdut: les dues capes són intactes i separades.

# 4. Desfer amb ORIG_HEAD
git reset --hard ORIG_HEAD
git log --oneline -1
git status --short
grep -c "FEINA SENSE CONFIRMAR" app.js
7d3a8f4 feat: filtre i estils
?? notes.txt
0

La branca ha tornat al seu lloc, però la modificació d'app.js ha desaparegut: --hard l'ha sobreescrita. ORIG_HEAD torna la posició de la branca, no la teva feina en curs. És exactament l'advertiment de l'apartat 8.

# 5. La taula completa (refent l'estat brut cada vegada)
Ordre git log -1 Modificació d'app.js notes.txt
git reset --soft HEAD~1 b52c9d1 Conservada (al disc) Intacte
git reset --mixed HEAD~1 b52c9d1 Conservada (barrejada amb la del commit) Intacte
git reset --hard HEAD~1 b52c9d1 PERDUDA Intacte
# 6. Per què notes.txt sobreviu
git ls-files | grep notes
(sense sortida)

notes.txt no és a l'índex: Git no el segueix. reset opera sobre HEAD, l'índex i els fitxers que Git coneix; un fitxer sense seguiment li és invisible. Per esborrar-lo cal git clean (apartat 7).

Solució 2:

mkdir /tmp/p9-02-b && cd /tmp/p9-02-b && git init -q -b main
echo "v1" > app.js; echo "v1" > estils.css; echo "v1" > index.html
git add . && git commit -q -m "c1"
echo "v2" > app.js && git commit -q -am "c2"
echo "v3" > app.js; echo "v3" > estils.css; echo "v3" > index.html
git add . && git commit -q -m "c3: toca els tres fitxers"
echo "v4" > app.js && git commit -q -am "c4"
# 1. L'error
git reset --hard HEAD~1 -- app.js
fatal: Cannot do hard reset with paths.

Amb una ruta, reset mai no toca el directori de treball ni mou la branca; --hard implica totes dues coses, així que és contradictori. Git ho rebutja en lloc de fer alguna cosa a mitges.

# 2. La forma correcta
git reset HEAD~2 -- estils.css       # HEAD~2 = c2, és a dir, abans de c3
git status --short
M  estils.css
git diff --cached estils.css         # índex vs HEAD: sí que hi ha canvi
git diff estils.css                  # treball vs índex: també, en sentit invers
cat estils.css
-v3
+v1
v3

Clau: el fitxer al disc continua sent v3. Només l'índex té v1. Per això git diff (treball vs índex) mostra el canvi invers. I la branca no s'ha mogut:

git log --oneline -1
9c4e7b2 c4
# 3. Confirmar només aquesta part
git commit -q -m "revert parcial: torna estils.css a l'estat previ a c3"
cat app.js estils.css index.html
v4
v1
v3

app.js conserva v4, index.html conserva v3 (el que va aportar c3), i només estils.css ha tornat enrere. S'ha desfet una tercera part d'un commit.

Però atenció: el fitxer al disc continuava sent v3 en confirmar. El commit diu v1 i el disc diu v3, així que ara hi ha una modificació pendent. Aquesta asimetria és justament el que evita restore:

# 4. La forma moderna, que deixa disc i índex coherents
git restore --source=HEAD~3 --staged --worktree estils.css
git status --short
cat estils.css
M  estils.css
v1

Índex i directori de treball coincideixen. Aquesta és la diferència pràctica: reset -- <ruta> només prepara; restore --staged --worktree prepara i escriu al disc. Per a l'ús diari, restore és més predictible.

Solució 3:

mkdir /tmp/p9-02-c && cd /tmp/p9-02-c && git init -q -b main
echo "base" > app.js && git add . && git commit -q -m "c1"

# 1. Les tres situacions
echo "COMMIT SENSE PUBLICAR" >> app.js && git commit -q -am "c2: feina confirmada"
echo "CANVI PREPARAT" >> estils.css && git add estils.css
echo "CANVI NOMES AL DISC" >> app.js
git status --short
A  estils.css
 M app.js
# 2. El desastre
git reset --hard HEAD~1
git status --short           # (sense sortida)
cat app.js
ls
base
app.js

Tot fora: el commit, el fitxer preparat i la modificació al disc.

# 3. El commit torna
git reflog -3
3f8a1d6 HEAD@{0}: reset: moving to HEAD~1
8f4c2a9 HEAD@{1}: commit: c2: feina confirmada
3f8a1d6 HEAD@{2}: commit (initial): c1
git branch rescat 8f4c2a9
git show rescat --stat
 app.js | 1 +

Recuperat íntegre.

# 4. El canvi preparat també
git fsck --lost-found
dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
git cat-file -p 6f2b9d4
CANVI PREPARAT
git cat-file -p 6f2b9d4 > estils.css      # recuperat

El git add el va salvar. En preparar el fitxer, Git va crear el blob i el va escriure a .git/objects. El reset --hard va treure la referència des de l'índex, però l'objecte s'hi va quedar.

# 5. El canvi que era només al disc
git fsck --lost-found | wc -l
grep -c "NOMES AL DISC" $(git rev-list --objects --all | awk '{print $1}' > /dev/null; echo app.js)
1
0

Només hi ha un objecte penjant, el de l'add. La línia CANVI NOMES AL DISC no existeix enlloc de .git. Mai no se'n va calcular el hash, mai no es va comprimir, mai no es va escriure. Git no pot recuperar una cosa de la qual no va tenir mai notícia. Les úniques vies reals serien l'historial local del teu editor (VS Code, IntelliJ i Vim amb undofile el desen) o una còpia de seguretat del sistema de fitxers.

# 6. Amb stash previ, tot canvia
cd /tmp && rm -rf p9-02-d && mkdir p9-02-d && cd p9-02-d && git init -q -b main
echo "base" > app.js && git add . && git commit -q -m "c1"
echo "COMMIT SENSE PUBLICAR" >> app.js && git commit -q -am "c2"
echo "CANVI PREPARAT" >> estils.css && git add estils.css
echo "CANVI NOMES AL DISC" >> app.js
echo "fitxer nou sense afegir" > esborrany.txt

git stash push -u -m "per si de cas"
git reset --hard HEAD~1
git stash list
stash@{0}: On main: per si de cas
git stash pop
git status --short
cat app.js | tail -1
ls
A  estils.css
 M app.js
?? esborrany.txt
CANVI NOMES AL DISC
app.js  esborrany.txt  estils.css

Tot intacte, inclòs el fitxer sense seguiment gràcies a -u. El desament temporal va convertir les tres capes en commits ocults dins de la base de dades i, per tant, en una cosa recuperable fins i tot si s'hagués esborrat el desament (lliçó 09-04).

Un segon de git stash push -u compra la diferència entre "he perdut dues hores" i "no ha passat res".

Conclusió

reset deixa de fer por tan bon punt s'entén que fa dues coses amb el mateix nom.

  • git reset <commit> mou la branca. No esborra commits: reescriu un fitxer de 41 bytes. Els commits que deixen de ser assolibles continuen íntegres a la base de dades.
  • Els tres modes es distingeixen per fins on arriben: --soft només mou HEAD; --mixed (per defecte) a més reescriu l'índex; --hard a més sobreescriu el directori de treball. Només --hard destrueix alguna cosa, i el que destrueix són els canvis sense confirmar, no els commits. Els fitxers sense seguiment no els toca.
  • --keep és el --hard prudent: avorta en lloc de trepitjar la teva feina local.
  • git reset <commit> -- <ruta> és una altra operació: mai no mou la branca, mai no toca el disc, només omple l'índex. Per això --hard amb rutes és un error.
  • Per a la feina diària, git restore: --staged per despreparar (no perd res), sense opcions per descartar al disc (destrueix), --source per portar d'un altre commit i -p per fer-ho tros a tros.
  • git clean és l'única ordre d'aquest mòdul sense xarxa de seguretat, perquè el que mai no es va confirmar no és a .git/objects. -n abans que -f, sempre; -x mereix llegir-se dues vegades; git stash push -u ho converteix tot en recuperable.
  • ORIG_HEAD desfà l'últim reset, merge, rebase o pull. Només desa una posició.
  • I la decisió, en una pregunta: està publicat? Si no, reset, --amend o rebase -i. Si sí i la branca és compartida, revert (lliçó 05-06), sense excepcions.

Queda pendent el cas que la taula de decisió despatxa amb un "veure 09-03": què fer quan allò que s'ha embolicat no és dins del teu repositori, sinó entre el teu repositori i el servidor. El push rebutjat, el missatge de branques divergents, el company que va reescriure l'historial publicat i et va deixar amb feina penjant d'una base que ja no existeix.

Tot això, i el tancament de la promesa que vam deixar oberta a la lliçó 04-05, a la lliçó 09-03: Resolent Divergències amb el Remot.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats