git reset e git revert: come annullare un commit senza rompere la cronologia del team
git reset sposta il puntatore del branch su un altro commit e riscrive la cronologia, quindi va bene solo per commit locali non ancora inviati. git revert crea un nuovo commit con le modifiche inverse senza toccare la cronologia: è il modo sicuro per annullare un commit che è già sul server.
In breve: git reset riporta indietro il branch e fa finta che i commit di troppo non siano mai esistiti. git revert lascia la cronologia com'è e aggiunge un nuovo commit che annulla le modifiche di quello vecchio. Il primo va bene per il lavoro locale, il secondo per tutto ciò che è già finito nel repository condiviso. Se ricordate solo questa regola, eviterete la maggior parte dei disastri con Git.
Di seguito vediamo come funzionano entrambi i comandi, in cosa differiscono le modalità di reset, come annullare un commit dopo il push, come recuperare i commit «persi» e cosa c'entra git clean.
I tre livelli su cui agiscono i comandi di annullamento
Per capire la differenza, ricordate le tre aree di Git: la cartella di lavoro (i file su disco), l'index (staging area, ciò che finirà nel prossimo commit) e la cronologia (i commit a cui puntano il branch e HEAD). Ogni comando di annullamento agisce su un proprio insieme di queste aree.
| Comando | Cronologia | Index | Cartella di lavoro | Sicuro su un branch condiviso |
|---|---|---|---|---|
git restore file | no | no | sì | sì |
git restore --staged file | no | sì | no | sì |
git reset --soft | sì | no | no | no |
git reset --mixed (predefinito) | sì | sì | no | no |
git reset --hard | sì | sì | sì | no |
git revert | aggiunge un commit | — | — | sì |
git clean | no | no | elimina i file non tracciati | sì, ma irreversibile |
Come funziona git reset
Il comando git reset <commit> sposta il puntatore del branch corrente (e HEAD insieme a esso) sul commit indicato. I commit «successivi» non vengono eliminati fisicamente, ma non sono più visibili nel branch. La modalità determina cosa succede all'index e ai file.
git reset --soft: annullare il commit mantenendo le modifiche nell'index
Il branch si sposta, mentre index e file restano com'erano. Tutte le modifiche dei commit annullati risultano pronte per il commit. È il modo ideale per riscrivere un messaggio o unire gli ultimi commit in uno solo.
git reset --soft HEAD~3
git commit -m "feat: form di registrazione"
Gli ultimi tre commit sono diventati uno, e il contenuto dei file non è cambiato.
git reset --mixed: annullare commit e index
È la modalità predefinita: git reset HEAD~1 equivale a git reset --mixed HEAD~1. Il branch si sposta, l'index viene azzerato e le modifiche restano nella cartella di lavoro come non preparate. Comodo quando avete committato troppo e volete decidere di nuovo cosa aggiungere e in quali porzioni.
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: tornare indietro completamente
Branch, index e cartella di lavoro vengono riportati allo stato del commit indicato. Tutte le modifiche non committate nei file tracciati vanno perse senza avviso. È l'unica modalità di reset che può davvero distruggere il vostro lavoro.
git reset --hard HEAD~1 # scartare l'ultimo commit e tutte le modifiche
git reset --hard origin/main # allineare il branch allo stato del server
Prima di --hard conviene eseguire git status e git stash, se c'è qualcosa di prezioso. Ciò che è committato si può recuperare (vedi sotto), ciò che non lo è no.
git reset su un singolo file
Con un percorso, reset non sposta il branch ma modifica solo l'index: git reset app.py toglie il file da quelli preparati. L'equivalente moderno è git restore --staged app.py, più chiaro. I flag --soft e --hard non funzionano con i percorsi.
Come funziona git revert
git revert <commit> calcola le modifiche del commit indicato, le applica al contrario e crea un nuovo commit. Le righe aggiunte vengono rimosse, quelle eliminate ripristinate. La cronologia cresce in avanti, nulla viene riscritto, e i colleghi non devono sistemare niente da parte loro.
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: caching delle risposte
# 1c02e8f fix: timeout delle richieste
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: caching delle risposte"
# 2 files changed, 3 insertions(+), 41 deletions(-)
Git aprirà l'editor con un messaggio del tipo Revert "..." e la riga This reverts commit 7be41d0.... È buona pratica aggiungere il motivo dell'annullamento.
Flag utili di revert
--no-edit: accettare il messaggio standard senza aprire l'editor.-n(--no-commit): applicare le modifiche inverse nell'index senza creare un commit. Così si possono annullare più commit con un unico commit.git revert A..B: annullare un intervallo di commit (ognuno con un commit di revert separato, dal più recente al più vecchio).-m 1: obbligatorio per un merge commit, indica quale genitore considerare come linea principale.
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: annullamento di tre commit sperimentali"
Revert di un merge commit
Un merge ha due genitori e Git non sa rispetto a quale calcolare le modifiche «inverse», quindi senza -m restituisce l'errore is a merge but no -m option was given. Di solito il branch principale è il primo genitore:
git revert -m 1 9f1e2ab
Il tranello: se in seguito vorrete unire di nuovo lo stesso branch, Git considererà i suoi commit già integrati e non porterà le modifiche. La soluzione è fare prima il revert del commit di revert stesso, e poi aggiungere le nuove correzioni.
git reset vs git revert: quale scegliere
| Criterio | git reset | git revert |
|---|---|---|
| Effetto sulla cronologia | la riscrive (i commit spariscono dal branch) | aggiunge un nuovo commit |
| Commit già pushato | serve push --force, rompe il lavoro dei colleghi | un normale git push |
| Annullare un commit a metà cronologia | solo insieme a tutti i successivi | si può in modo mirato |
| Traccia nella cronologia | non resta | resta, si vede chi ha annullato e perché |
| Scenario tipico | ripulire i commit locali prima del push | annullare in main un commit che ha rotto la produzione |
Regola semplice: se il commit l'ha visto qualcun altro oltre a voi, revert; altrimenti reset. Eccezione: il vostro feature branch personale che nessuno ha scaricato. Lì riscrivere la cronologia e fare git push --force-with-lease è accettabile.
Perché reset --hard su un branch condiviso è una cattiva idea
Immaginate: avete fatto git reset --hard HEAD~2 su main e poi git push --force. I colleghi hanno ancora quei due commit. Al successivo git pull Git cercherà di unire il loro branch con il vostro, e i commit «eliminati» torneranno indietro, per giunta con un merge commit inutile. Peggio ancora se qualcuno ha fatto in tempo a pushare i propri commit sopra: il force push li cancellerà e basta.
Per questo sui branch protetti (main, release) il force push viene di solito vietato nelle impostazioni dell'hosting. Se non si può evitare di riscrivere, usate git push --force-with-lease: rifiuterà l'operazione se sul server sono comparsi commit che non avete visto.
Scenari tipici di annullamento
Come annullare l'ultimo commit non ancora pushato
git reset --soft HEAD~1 # modifiche nell'index, si può rifare il commit
git reset HEAD~1 # modifiche nei file, index vuoto
git reset --hard HEAD~1 # modifiche scartate
Se dovete solo correggere il messaggio o aggiungere un file dimenticato, reset non serve: git add dimenticato.txt e git commit --amend.
Come annullare un commit dopo il push
git revert <hash>
git push
È la risposta alla ricerca frequente «git reverse commit»: in Git non esiste un comando git reverse, il commit «inverso» lo crea proprio git revert.
Come riportare il branch allo stato del server
git fetch origin
git reset --hard origin/main
Utile quando il branch locale si è ingarbugliato dopo un rebase non riuscito o degli esperimenti. Tutti i commit locali che non sono sul server spariranno dal branch.
Come recuperare i commit dopo un reset sbagliato
Git registra ogni spostamento di HEAD nel registro reflog. Dopo un reset i commit restano nel database degli oggetti per almeno qualche settimana (per impostazione predefinita le voci irraggiungibili del reflog si conservano 30 giorni).
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: caching delle risposte
git reset --hard a3f9c21 # oppure HEAD@{1}
Il reflog è locale: esiste solo nel vostro clone e non viene trasmesso al server.
git clean: eliminare i file non tracciati
git reset --hard non tocca i file che Git non traccia: nuovi file non aggiunti all'index, artefatti di build, log. Per questi esiste git clean. Il comando è irreversibile: i file che elimina non finiscono né nel reflog né nel cestino.
git clean -n # dry run: mostrare cosa verrà eliminato
git clean -f # eliminare i file non tracciati
git clean -fd # lo stesso più le cartelle non tracciate
git clean -fdx # più i file ignorati (.gitignore): node_modules, dist
git clean -i # selezione interattiva
Senza -f Git per impostazione predefinita rifiuta di procedere (impostazione clean.requireForce). Iniziate sempre con -n, soprattutto con -x: cancellerà anche il .env locale, se è tra gli ignorati.
La combinazione per un ripristino completo all'ultimo commit: git reset --hard && git clean -fd.
Errori tipici
- reset --hard invece di --soft. Ciò che è committato lo salva il reflog, ciò che non lo è è perso. L'abitudine di fare
git stashprima degli esperimenti risolve il problema. - revert senza capire le dipendenze. Se commit successivi hanno modificato le stesse righe, il revert produrrà un conflitto. Risolvetelo come al solito: correggete i file,
git add,git revert --continue; se ci ripensate,git revert --abort. - Confondere restore e reset. Per annullare le modifiche di un singolo file non serve il reset di tutto il branch:
git restore file. - force push su main. Meglio un revert e un push normale, anche se la cronologia diventa un po' più lunga.
Riepilogo
git reset è lo strumento per la propria cronologia non ancora pubblicata: --soft conserva le modifiche nell'index, --mixed nei file, --hard scarta tutto. git revert è l'annullamento sicuro nei branch condivisi, che lascia una traccia onesta nella cronologia. git clean ripulisce i file non tracciati, ma non ammette errori. Gli scenari di annullamento con restore, reset e revert sono affrontati in pratica nel corso su Git.
Domande frequenti
git reset elimina i commit per sempre?
No, i commit restano nel database degli oggetti e sono accessibili tramite git reflog finché non li elimina il garbage collector, per impostazione predefinita dopo 30 giorni per le voci irraggiungibili.
Si può annullare un git revert?
Sì, il commit di revert è un commit normale: si può annullare con un altro git revert oppure, se non è stato pushato, rimuoverlo con git reset.
Che differenza c'è tra git reset e git checkout?
git reset sposta il puntatore del branch corrente, mentre git checkout (o git switch) sposta HEAD su un altro branch o commit senza modificare il branch stesso.
Cosa significano HEAD~1 e HEAD^?
Entrambi indicano il genitore del commit corrente; HEAD~3 è il terzo antenato seguendo i primi genitori, mentre HEAD^2 è il secondo genitore di un merge commit.
Come annullare git clean?
Con gli strumenti di Git non si può: i file non tracciati eliminati non erano conservati nel repository, quindi lanciate sempre prima git clean -n.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
Vai al corso