Il controllo di versione viene di solito presentato come «una serie di salvataggi a cui tornare». È vero e insufficiente: la parte utile comincia quando si capisce come Git rappresenta quei salvataggi, perché da lì discendono comportamenti che altrimenti sembrano arbitrari.

Il modello a oggetti#

come è fatto un commit
Git non salva "le differenze": salva ALBERI DI CONTENUTO, indirizzatidall'hash del contenuto stesso. Capire questo spiega quasi tutti icomportamenti che altrimenti sembrano arbitrari.  commit  9f3a2c1  ──▶ tree  4b2e8d1  ──▶ blob  a1c0f77   src/app.js     │                   │           └──▶ tree  77bd012   src/utils/     │                   └──────────▶ blob  3e9a0b2   README.md     └─ parent ▶ commit 8d10cc4 ──▶ tree ...  blob   = il CONTENUTO di un file (non il nome, non il percorso)  tree   = una cartella: nomi → blob/tree  commit = un tree + i genitori + autore + data + messaggioConseguenze immediate:  - due file identici in punti diversi occupano UN solo blob;  - rinominare un file non è un'operazione registrata: Git la DEDUCE    confrontando i contenuti (per questo a volte "sbaglia");  - l'hash di un commit dipende dai genitori: riscrivere la storia    cambia TUTTI gli hash successivi, sempre.

Le tre conseguenze elencate spiegano tre confusioni ricorrenti. Le rinomine non sono registrate: Git le deduce confrontando i contenuti, ed è per questo che a volte «perde» la storia di un file spostato e modificato pesantemente nello stesso commit — il consiglio operativo è separare lo spostamento dalla modifica in due commit.

E poiché l'hash di un commit include i genitori, riscrivere la storia cambia tutti gli hash successivi, sempre. Non è un effetto collaterale: è la proprietà che rende la storia verificabile.

Commit atomici#

staging selettivo
# Un commit dovrebbe rispondere a UNA domanda: "cosa fa questa modifica?"# Se la risposta contiene una "e", sono due commit.# ✗ Il commit che nessuno sa piu' leggere fra sei mesigit commit -am "fix vari e aggiornamenti"# ✓ Si seleziona per PEZZI, non per file: -p propone ogni blocco#   e si decide uno per uno. Serve proprio quando si e' lavorato#   a due cose insieme senza accorgersene.git add -pdiff --git a/src/carrello.js b/src/carrello.js@@ -12,7 +12,7 @@ function totale(righe) {-  return righe.reduce((s, r) => s + r.prezzo, 0);+  return righe.reduce((s, r) => s + r.prezzo * r.qta, 0);(1/3) Stage this hunk [y,n,q,a,d,s,e,?]? ygit commit -m "Correggi il totale ignorando la quantita' di riga"
-p propone ogni blocco separatamente. Serve proprio nel caso comune: si stava correggendo un bug e per strada si è sistemata anche un'altra cosa.

Il criterio: se la descrizione del commit contiene una «e», sono due commit. Non è pignoleria — è ciò che rende utilizzabili git revert, git bisect e la lettura della storia. Un commit che tocca quaranta file per cinque motivi diversi non si può annullare selettivamente, non si può capire, e quando bisect lo indica come colpevole non ha detto niente di utile.

Il messaggio#

anatomia di un buon messaggio
Correggi il totale del carrello ignorando la quantità di rigaIl calcolo sommava solo il prezzo unitario, quindi due unità dellostesso prodotto risultavano una sola. Il bug era invisibile nei testperché tutti gli esempi avevano quantità 1.Aggiunto un caso di test con quantità > 1 per impedire la regressione.Rif: #412┌─ La riga di oggetto risponde a COSA (max ~50 caratteri, imperativo,│  senza punto finale: completa la frase "questo commit vuole...")├─ Riga vuota: git la usa per separare oggetto e corpo└─ Il corpo risponde a PERCHÉ. Il "come" si legge nel diff; il perché   non si legge da nessuna parte, e fra un anno sarà l'unica cosa che   serve davvero.

Quasi niente è davvero perso#

recupero
# "Ho perso il lavoro." Quasi mai e' vero: reflog registra ogni# posizione di HEAD negli ultimi 90 giorni, anche quelle "cancellate".git reflog9f3a2c1 HEAD@{0}: reset: moving to HEAD~34b2e8d1 HEAD@{1}: commit: Aggiungi validazione modulo8d10cc4 HEAD@{2}: commit: Rifattorizza calcolo totale# Il commit 4b2e8d1 non e' sparito: e' solo diventato irraggiungibile.git reset --hard 4b2e8d1        # oppure: git branch recupero 4b2e8d1# Anche le modifiche messe in staging e poi perse si ritrovano:git fsck --lost-founddangling blob a1c0f77e8b2d...git cat-file -p a1c0f77 > recuperato.js# Regola pratica: prima di un'operazione che riscrive la storia,# un ramo di sicurezza. Costa un secondo.git branch prima-del-rebase
Il reflog è locale e non viene condiviso: registra dove è stato HEAD su questa macchina, comprese le posizioni rese irraggiungibili da un reset.

Questo è il motivo per cui vale la pena sperimentare senza timore: un reset --hard sbagliato, un rebase finito male, un ramo cancellato per errore sono quasi sempre recuperabili. La regola di prudenza che costa un secondo: prima di un'operazione che riscrive la storia, git branch prima-del-rebase.

La trappola numero uno: i segreti#

incidente
# La trappola numero uno: un segreto committato per errore.# Rimuoverlo dal file NON basta: resta in ogni commit precedente,# e chiunque abbia clonato il repository ce l'ha gia'.git log --oneline -S 'sk_live_' --all8d10cc4 Aggiungi integrazione pagamenti# L'ORDINE delle operazioni conta, e la prima non e' tecnica:# 1. REVOCARE la credenziale. Da questo momento il danno e' chiuso.#    Tutto il resto e' pulizia.# 2. Riscrivere la storiagit filter-repo --invert-paths --path config/segreti.json# 3. Forzare la pubblicazione e avvisare chi ha cloni localigit push --force-with-lease --all# 4. Aggiungere il pattern al .gitignore E un hook che lo impedisca# La prevenzione, che costa meno della cura:cat .git/hooks/pre-commit#!/bin/shif git diff --cached | grep -qE '(sk_live_|BEGIN [A-Z ]*PRIVATE KEY|password\s*=)'; then  echo "Possibile segreto nel commit. Verificare prima di procedere."  exit 1fi
L'ordine è quello che conta: la revoca chiude il danno, la pulizia è cosmetica. Invertirli significa passare un'ora a riscrivere la storia mentre la chiave è ancora valida.

La trappola numero due: i binari#

peso del repository
# La seconda trappola: i file binari pesanti gonfiano il repository# in modo PERMANENTE. Ogni versione viene conservata per intero,# perche' la compressione delta non funziona sui formati gia' compressi.git count-objects -vHsize-pack: 1.84 GiB# Chi sono i responsabili:git rev-list --objects --all \>   | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \>   | awk '$1=="blob" {print $3, $4}' | sort -rn | head -5104857600 media/presentazione.mp4 52428800 design/mockup.psd 41943040 media/presentazione-v2.mp4# Un video da 100 MB modificato dieci volte sono 1 GB di storia# che ogni clone scarichera' per sempre. Vanno tenuti fuori:# Git LFS, o un archivio a oggetti con solo il riferimento nel repo.
La compressione delta non funziona sui formati già compressi: ogni versione di un video o di un PSD viene conservata per intero, per sempre, e ogni clone la scarica.

La funzione che quasi nessuno usa#

bisect
# La funzione che vale da sola l'intero controllo di versione, e che# quasi nessuno usa: trovare in quale commit e' comparso un bug,# per ricerca binaria. 1000 commit → 10 tentativi.git bisect startgit bisect bad                  # la versione attuale e' rottagit bisect good v2.4.0          # questa funzionavaBisecting: 512 revisions left to test after this (roughly 9 steps)# E si puo' automatizzare completamente, se esiste un test:git bisect run npm test -- --grep 'totale carrello'...8d10cc4a2b1f9e3d is the first bad commit    Rifattorizza calcolo totale# E' anche il motivo per cui i commit atomici ripagano: bisect# restituisce un commit. Se quel commit tocca quaranta file per# cinque motivi diversi, la risposta non serve a niente.
Con un test che riproduce il bug, l'intera ricerca è automatica: si torna dopo dieci minuti e Git ha il nome del commit colpevole.

La ricerca binaria su mille commit richiede dieci tentativi. È lo strumento più sottoutilizzato di Git, probabilmente perché richiede una precondizione che spesso manca: commit piccoli e con una storia pulita. È anche il motivo per cui l'igiene dei commit non è estetica — è un investimento che si riscuote nel momento peggiore, quando qualcosa si è rotto e nessuno sa da quando.

Che cosa Git non è#

limiti
Cosa Git NON è:  ✗ un backup    Un repository che vive solo sul portatile sparisce con il portatile,    cronologia compresa. Serve almeno un remoto, e un remoto in un posto    diverso da quello dove gira la produzione.  ✗ un archivio per i file grandi    Vedi sopra: la storia è permanente e ogni clone la scarica tutta.  ✗ un posto per i segreti    Nemmeno in un file cifrato, se la chiave sta nello stesso repository.  ✗ una garanzia che il codice funzioni    Registra cosa è cambiato, non se è giusto. Serve altro per quello.  ✗ una sostituzione della documentazione    La storia dice cosa è successo. Non dice come è fatto il sistema    ADESSO — per quello serve un documento che si legge in dieci minuti.
ProblemaComandoNota
Separare modifiche mescolategit add -pblocco per blocco, non file per file
Ho fatto un commit sbagliatogit commit --amendsolo se non è ancora stato pubblicato
Ho perso dei commitgit reflog90 giorni di storia locale
Annullare un commit pubblicatogit revertcrea un commit inverso: non riscrive la storia
Da quando è rotto?git bisect runautomatico se esiste un test
Chi ha scritto questa riga e perchégit log -p -L 42,50:file.jsla storia di un intervallo di righe
Trovare quando è sparita una stringagit log -S 'testo' --allcerca nei contenuti, non nei messaggi
Pubblicare dopo aver riscritto--force-with-leaserifiuta se qualcun altro ha pubblicato nel frattempo

L'ultima riga merita un'annotazione: --force-with-lease va preferito a --force sempre. Il primo verifica che il remoto sia ancora dove ve lo aspettavate e rifiuta se qualcuno ha pubblicato nel frattempo; il secondo sovrascrive e basta, cancellando il lavoro altrui senza avvisare.