Esegui git reset --hard su un commit che il tuo team ha già scaricato e la tua storia smette di corrispondere alla loro. Esegui git revert su un typo che non hai ancora pushato e aggiungi un commit inutile al log. Git ha quattro comandi per annullare le cose: restore, reset, commit --amend e revert, e non sono intercambiabili. Scegli quello sbagliato e il risultato è che non succede niente, oppure succede molto più di quanto volevi.
A decidere è dove si trova la modifica in questo momento: nella working tree, nella staging area, in un commit che hai solo tu, o in un commit che hai già pushato.
Dove si trova una modifica in Git: working tree, index, storia
Ogni comando per annullare agisce su uno o più di tre livelli:
- Working tree: i file sul disco, mentre li stai modificando.
- Index (la staging area): lo snapshot che hai costruito con
git add, in attesa di diventare il prossimo commit. - Storia: i commit già registrati, raggiungibili da
HEAD.
Una modifica parte dalla working tree, passa all’index con git add e passa alla storia con git commit. Annullarla significa farla tornare indietro attraverso gli stessi livelli. Più è andata avanti, più deliberato deve essere il comando.
| |
Come annullare le modifiche non in staging con git restore
Hai modificato config.py, l’hai rotto, e vuoi recuperare la copia di Git:
| |
Questo sovrascrive il file nella working tree con la versione nell’index, che è la stessa di HEAD a meno che tu non abbia già messo in staging qualcosa. La modifica se ne va. Non finisce in uno stash, e nessun comando Git te la farà recuperare. Per annullare tutte le modifiche non in staging in un colpo solo:
| |
git restore sostituisce il più vecchio git checkout -- <file>. Funzionano ancora entrambi, ma restore esiste perché “dammi indietro questo file” non condivide più un comando con “cambia branch”.
Come togliere un file dallo staging senza perdere le modifiche
Hai eseguito git add su tre file, poi ti sei accorto che uno di loro non c’entra con questo commit. La modifica in sé va bene:
| |
migration.sql esce dall’index e torna a “modificato, non in staging”. Il file sul disco resta intatto. È lo stesso risultato del più vecchio git reset HEAD -- migration.sql, ed è l’unico punto in cui reset e restore si sovrappongono: reset con un pathspec tocca solo l’index, che è esattamente ciò che fa restore --staged.
Come annullare un commit con git reset: soft, mixed e hard
git restore non può toccare un commit. Per quello serve git reset, e il flag controlla quanto annulla:
| Flag | Working tree | Index | Storia (HEAD) |
|---|---|---|---|
--soft | invariata | invariato | spostata |
--mixed (predefinito) | invariata | riportato al target | spostata |
--hard | riportata al target | riportato al target | spostata |
Tutti e tre spostano il puntatore del branch. La differenza è quanto del tuo lavoro lo segue all’indietro.
--soft riporta indietro i commit ma lascia le loro modifiche in staging: è il modo più rapido per fare lo squash degli ultimi tre commit in uno solo.
| |
--mixed è quello che ottieni con un semplice git reset HEAD~1. Riporta indietro il commit e rimette le sue modifiche nella working tree come modifiche non in staging, così puoi dividerle in commit più piccoli o buttarne via una parte.
--hard scarta il diff del tutto. La working tree finisce come se il commit non fosse mai esistito:
| |
Non chiede conferma. I commit che hai superato restano recuperabili con git reflog (vedi sotto), ma qualsiasi modifica non committata nella working tree in quel momento è persa per sempre, quindi controlla git status prima di eseguirlo. E non farlo mai su un commit che qualcun altro ha già scaricato: spostare all’indietro un branch condiviso lascia la tua storia e la loro in disaccordo su cosa sia successo, lo stesso problema dietro la regola d’oro del rebase.
Come correggere l’ultimo commit con git commit –amend
Se l’errore è nel commit più recente, un reset è eccessivo. --amend sostituisce il commit in cima alla storia, senza spostare HEAD all’indietro:
| |
Metti in staging altre modifiche prima e si uniscono allo stesso commit, sullo stesso genitore:
| |
Il commit modificato con amend è un nuovo oggetto con un nuovo hash, proprio come un commit rebasato. La regola quindi è la stessa del rebase: fai l’amend solo di quello che non hai ancora pushato. Fai l’amend di un commit già su un branch condiviso e chiunque l’abbia scaricato deve riconciliare due commit che pretendono di essere la stessa modifica.
Come annullare un commit già pushato con git revert
Una volta che un commit è su un branch condiviso, reset e --amend non sono più opzioni. Riscrivere una storia che altri hanno già scaricato è il modo più diretto per ottenere branch divergenti e commit duplicati. git revert prende la strada opposta: lascia il commit sbagliato dove si trova e ne aggiunge uno nuovo che fa l’opposto.
| |
Git apre un editor per il messaggio del commit di revert, poi lo registra:
| |
La storia adesso contiene sia il commit originale che il suo inverso. Non è stato riscritto nulla, quindi un normale git pull su ogni altro clone recupera la correzione. Per fare il revert di un intervallo come un singolo commit:
| |
--no-commit (-n) applica l’inverso di ogni commit alla working tree e all’index senza fare commit, così tre revert finiscono in un solo commit invece di tre. Se un revert va in conflitto con modifiche fatte nel frattempo, Git si blocca nello stesso modo in cui si blocca a metà di un merge: risolvi i marcatori di conflitto, fai git add del file, poi esegui git revert --continue.
“Annullare un push” significa quindi: fai il revert del commit, poi pusha il revert. Non fare mai push forzato di una storia riscritta su un branch da cui altri fanno pull.
Come recuperare da un reset sbagliato con git reflog
Dopo un reset --hard, i commit scompaiono da git log. Esistono ancora. git reflog elenca ogni posizione in cui HEAD ha puntato sulla tua macchina, inclusa quella subito prima del reset:
| |
| |
git reset --hard HEAD@{1} ti riporta su 7d4e1b8. Quanto tempo vivono le voci del reflog, e perché il reflog è solo locale, lo spiega la sezione sul recupero di Git Rebase vs Merge: differenze e quando usarli. Funziona nello stesso modo, qualunque comando abbia spostato HEAD.
Il reflog ti riporta a dove eri. Non ti dice quale fosse l’ultimo commit buono. Per quello, git bisect riduce centinaia di commit a quello che ha introdotto il bug.
Git reset vs revert vs restore: quale usare e quando
| La modifica è… | Comando |
|---|---|
| Modificata, non in staging, e la vuoi far sparire | git restore <file> |
| In staging, e la vuoi togliere dallo staging (modifica mantenuta) | git restore --staged <file> |
| Committata, non pushata, ed è l’ultimo commit | git commit --amend |
| Committata, non pushata, e vuoi tornare più indietro | git reset --soft / --mixed / --hard HEAD~n |
| Committata e già pushata o condivisa | git revert <commit> |
Sparita dopo un reset --hard di cui ti sei pentito | git reflog + git reset --hard HEAD@{n} |
Per qualsiasi cosa già committata, una sola domanda indica la riga giusta: qualcun altro ha già visto questo commit? Se no, reset o amend e vai avanti. Se sì, revert. Costa un commit in più nel log, e nessuno dei tuoi colleghi deve districare una storia che non corrisponde più alla loro.