Confronto

git reset e git revert: come annullare un commit senza rompere la cronologia del team

Aggiornato: · Skillaria.Top

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.

ComandoCronologiaIndexCartella di lavoroSicuro su un branch condiviso
git restore filenonosìsì
git restore --staged filenosìnosì
git reset --softsìnonono
git reset --mixed (predefinito)sìsìnono
git reset --hardsìsìsìno
git revertaggiunge un commit——sì
git cleannonoelimina i file non tracciatisì, 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

Criteriogit resetgit revert
Effetto sulla cronologiala riscrive (i commit spariscono dal branch)aggiunge un nuovo commit
Commit già pushatoserve push --force, rompe il lavoro dei colleghiun normale git push
Annullare un commit a metà cronologiasolo insieme a tutti i successivisi può in modo mirato
Traccia nella cronologianon restaresta, si vede chi ha annullato e perché
Scenario tipicoripulire i commit locali prima del pushannullare 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 stash prima 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.

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.

Impara l’argomento in video

Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».

Vai al corso