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 + messaggio​Conseguenze 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,?]? y​git 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 riga​Il 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.