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#
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#
# 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"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#
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#
# "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-rebaseQuesto è 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#
# 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 1fiLa trappola numero due: i binari#
# 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 funzione che quasi nessuno usa#
# 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.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 è#
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.| Problema | Comando | Nota |
|---|---|---|
| Separare modifiche mescolate | git add -p | blocco per blocco, non file per file |
| Ho fatto un commit sbagliato | git commit --amend | solo se non è ancora stato pubblicato |
| Ho perso dei commit | git reflog | 90 giorni di storia locale |
| Annullare un commit pubblicato | git revert | crea un commit inverso: non riscrive la storia |
| Da quando è rotto? | git bisect run | automatico se esiste un test |
| Chi ha scritto questa riga e perché | git log -p -L 42,50:file.js | la storia di un intervallo di righe |
| Trovare quando è sparita una stringa | git log -S 'testo' --all | cerca nei contenuti, non nei messaggi |
| Pubblicare dopo aver riscritto | --force-with-lease | rifiuta 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.