Cosa fa#
Sistema di version control distribuito. Traccia modifiche ai file nel tempo, permette collaborazione, branching e rollback. Ogni sviluppatore ha una copia completa del repository, inclusa tutta la storia.
Sintassi#
git [comando] [opzioni] [argomenti]
Comandi essenziali#
| Comando | Flag | Significato flag | Cosa fa |
|---|---|---|---|
git init | — | — | Inizializza un nuovo repo nella cartella corr |
git clone <url> | — | — | Clona un repo remoto in |
git status | — | — | Mostra lo stato dei file (staged, modified, un |
git add <file> | -A | all | Aggiunge tutti i file modificati allo st |
git add -f <file> | -f | force | forza l'aggiunta di un file anche se è escluso da .gitignore. |
git commit -m "msg" | -m | message | Crea un commit c |
git push | -u origin main | upstream | Pusha e imposta il t |
git pull | --rebase | rebase invece di merge | Aggiorna il branch locale c |
git log | --oneline --graph | formato compatto + grafo | Visualizza la |
git log | p | patch Mostra ogni commit con il diff completo allegato — cosa è stato aggiunto (+) e cosa è stato rimosso (-) in ogni file.git log -p | grep -i "password|secret|key|token|api" oken|api" ogni file. | |
git diff | --staged | staged | Mostra le differen |
git stash | pop | ripristina | Salva temporaneamente |
git diff HASH1 HASH2 | — | — | Differenz |
git push --force | — | — | Sovrascrive la storia del remote — pe |
git clone ssh://utente@host:2220/path | — | — | Clone su porta SSH non standard — la port |
git fetch --all --prune | --prune | prune | Rimuove branch remoti locali che non esistono piu' sul remote |
git show-ref pattern | — | — | Mostra tutti i riferimenti che contengono il pattern |
La cartella .git/#
.git/ è il repo. La cartella di lavoro sono solo i file estratti dall'ultimo commit. Se cancelli .git/ perdi tutta la storia — i file rimangono ma non sono più un repo Git.
# Struttura interna
.git/
├── HEAD ← punta al branch corrente ("ref: refs/heads/main")
├── config ← configurazione locale del repo (remote, user, ecc.)
├── index ← staging area — file pronti per il prossimo commit
├── objects/ ← database di tutti i commit, tree, blob (i dati reali)
│ ├── ab/ ← primi 2 char dell'hash → subdirectory
│ └── cd/ ← ogni oggetto identificato dal suo SHA-1
├── refs/
│ ├── heads/ ← branch locali (main, feature, ecc.)
│ └── remotes/ ← branch remoti (origin/main, ecc.)
└── logs/ ← storia di tutti i movimenti di HEAD (reflog)# Vedere a cosa punta HEAD
cat .git/HEAD
# Vedere l'hash dell'ultimo commit
cat .git/refs/heads/main
# Recuperare commit "persi" (anche dopo reset --hard)
git refloggitdir#
La cartella .git/ è chiamata gitdir — è la directory che contiene l'intero stato del repository. Il path del gitdir è sempre referenziato dalla variabile $GIT_DIR. Normalmente vive dentro la cartella di lavoro, ma può stare altrove (es. nei worktree o nei submodule).
# GIT_DIR può essere impostato manualmente per puntare
# a un gitdir in una posizione non standard
GIT_DIR=/path/al/gitdir git statusCosa fa gitdir in pratica:
Il gitdir separa i metadati Git dai file di lavoro. Normalmente stanno insieme (progetto/.git/), ma possono stare separati — questo abilita scenari avanzati:
| Scenario | Come funziona |
|---|---|
| Repo normale | .git/ dentro la cartella di lavoro |
| Bare repo | Solo il gitdir, nessuna cartella di lavoro — usato sui server remoti (GitHub, GitLab) |
| Worktree | Stessa storia, due cartelle di lavoro diverse — git worktree add |
| Submodule | Il gitdir del submodule vive in .git/modules/nome/ del repo padre, non dentro il submodule stesso |
# Bare repo — solo il gitdir, nessun file di lavoro
# È quello che gira su GitHub dietro le quinte
git init --bare repo.git
# Verificare se sei in un bare repo
git rev-parse --is-bare-repositoryQuando fai git clone di un repo privato su un server, il server ha un bare repo — solo il gitdir, nessun file estratto. Tu scarichi la storia e estrai i file nella tua cartella di lavoro.
# Vedere il path del gitdir corrente
git rev-parse --git-dir
# Output tipico:
# /Users/barno/progetti/obsidian-vault/.gitEsempio reale — gitdir fuori da iCloud:
# Problema: iCloud sincronizza .git/ → conflitti, file duplicati, corruzione repo
# Soluzione: cartella di lavoro su iCloud, gitdir fuori
# Crea il gitdir in una posizione fuori da iCloud
git init --separate-git-dir /Users/barno/gitdirs/obsidian-vault ~/Library/Mobile\ Documents/obsidian-vault
# Risultato:
# ~/Library/Mobile Documents/obsidian-vault/ ← su iCloud (file di lavoro)
# └── .git ← file di testo con path al gitdir
# NON una cartella, solo un puntatore
# /Users/barno/gitdirs/obsidian-vault/ ← fuori da iCloud (il vero gitdir)# Il file .git nella cartella di lavoro contiene solo:
cat ~/Library/Mobile\ Documents/obsidian-vault/.git
# gitdir: /Users/barno/gitdirs/obsidian-vaultCon --separate-git-dir il .git nella cartella di lavoro non è più una cartella — è un file di testo con una sola riga che punta al gitdir reale. iCloud sincronizza solo i tuoi file, mai la storia Git.
Non committare mai file sensibili (password, token, chiavi SSH) — anche se li rimuovi nel commit successivo, restano per sempre in .git/objects/. Per rimuoverli dalla storia serve git filter-repo.
Branch#
| Comando | Flag | Significato flag | Cosa fa |
|---|---|---|---|
git branch | — | — | Lista tutti i branch locali |
git branch -a | -a | all | Lista branch locali e remoti |
git branch -r | -r | remote | Lista solo i branch remoti |
git branch nome | — | — | Crea un nuovo branch |
git checkout nome | — | — | Switcha su un branch esistente |
git checkout -b nome | -b | branch | Crea e switcha in un colpo solo |
git merge nome | — | — | Mergia il branch nome nel branch corrente |
git branch -d nome | -d | delete | Elimina un branch (solo se gia' mergiato) |
git branch -D nome | -D | Delete forzato | Elimina un branch forzatamente |
Rebase#
Il rebase riscrive la storia dei commit. Invece di creare un commit di merge, "sposta" i tuoi commit sopra il branch target.
Prima del rebase — feature parte da B, main va avanti con C:
gitGraph commit id: "A" commit id: "B" branch feature checkout feature commit id: "D" commit id: "E" checkout main commit id: "C"
Dopo il rebase — i commit D ed E vengono riscritti come D' ed E' sopra C:
gitGraph commit id: "A" commit id: "B" commit id: "C" commit id: "D'" commit id: "E'"
Rebase interattivo — squash di 3 commit in 1:
# prima: D───E───F ← 3 commit separati
# dopo: D' ← squashati in 1 solo commit
git rebase -i HEAD~3git checkout feature
git rebase main # sposta i commit di feature sopra main
git rebase -i HEAD~3 # rebase interattivo: modifica/squash ultimi 3 commit
git rebase --abort # annulla il rebase se ci sono conflitti
git rebase --continue # continua dopo aver risolto i conflittiTag#
| Comando | Flag | Significato flag | Cosa fa |
|---|---|---|---|
git tag | — | — | Lista tutti i tag del repository |
git tag nome | — | — | Crea un tag leggero sul commit corrente |
git tag -a nome -m "msg" | -a | annotated | Crea un tag annotato con messaggio |
git show nome | — | — | Mostra il contenuto del tag |
git tag -d nome | -d | delete | Elimina un tag locale |
I tag sono puntatori fissi a un commit — non si spostano come i branch. Usati normalmente per marcare versioni (v1.0, v2.3.1). In secret scanning vanno controllati sempre con git tag + git show <tag> — spesso dimenticati.
# Differenza tag leggero vs annotato
git tag v1.0 # leggero — solo un puntatore
git tag -a v1.0 -m "msg" # annotato — ha metadati propri (autore, data, messaggio)Merge vs Rebase#
Merge — preserva la storia, crea un commit di unione:
gitGraph commit id: "A" commit id: "B" branch feature checkout feature commit id: "D" commit id: "E" checkout main commit id: "C" merge feature id: "M"
Rebase — riscrive la storia, lineare e pulita:
gitGraph commit id: "A" commit id: "B" commit id: "C" commit id: "D'" commit id: "E'"
| Merge | Rebase | |
|---|---|---|
| Storia | Preservata, con commit di merge | Riscritta, lineare |
| Grafo | Intrecciato | Pulito |
| Commit hash | Invariati | Cambiano (D→D') |
| Branch condivisi | ✅ Sicuro | ❌ Pericoloso |
| Branch locali | ✅ Ok | ✅ Preferibile |
| Traccia dell'unione | Sì | No |
| Conflitti | Risolti una volta nel commit M | Risolti commit per commit |
Rebase in privato, merge in pubblico. Finché il branch è solo tuo → rebase. Appena è condiviso → merge.
Il rebase riscrive i commit hash. Su un repo condiviso questo causa problemi — usalo solo su branch locali non ancora pushati. Se vedi commit hash che cambiano su un repo aziendale, potrebbe essere un segnale di manomissione della storia.
Force push — cosa risolve e cosa no
# Scenario: hai pushato una password per errore
# 1. Riscrivi la storia locale rimuovendo il commit con il segreto
git rebase -i HEAD~2
# 2. Sovrascrivi il remote con la storia riscritta
git push --force
# Il commit con hash 12345 non esiste piu' nella storia del remote.
# Chi fa git clone ORA non vedra' mai la password.Ma il force push non raggiunge le copie locali esistenti:
| Situazione | Effetto del force push |
|---|---|
git clone dopo il force push | Non vede la password — storia pulita |
| Aveva gia' clonato prima | Il commit e' in .git/objects/ sul suo disco |
git pull dopo il force push | Git rifiuta — storia divergente. Deve fare git reset --hard origin/main per allinearsi, ma nel frattempo il commit e' ancora in locale |
git fetch --all | Scarica i riferimenti aggiornati ma non tocca .git/objects/ — il vecchio commit rimane fino al prossimo git gc |
Non puoi controllare le copie locali degli altri. Il force push pulisce il remote, non le macchine che hanno gia' fatto fetch. Ruota sempre la credenziale — il force push e' igiene, non sicurezza.
Cherry-pick#
Prende un singolo commit da un altro branch e lo applica sul branch corrente, senza fare merge di tutto il branch.
gitGraph commit id: "A" commit id: "B" branch feature checkout feature commit id: "D" commit id: "E" commit id: "F" checkout main commit id: "C" cherry-pick id: "E"
git cherry-pick <hash-commit> # applica quel commit sul branch corrente
git cherry-pick A..F # range di commit
git cherry-pick --no-commit # applica le modifiche senza creare il commit
git cherry-pick --abort # annulla se ci sono conflittiScenario tipico: hai una hotfix su feature che serve subito su main, ma non vuoi portare tutto il branch — prendi solo quel commit con cherry-pick.
Reset#
Sposta HEAD indietro nella storia. Tre modalità con impatto crescente sui file di lavoro.
git reset --soft HEAD~1 # annulla il commit → file restano in staging
git reset --mixed HEAD~1 # annulla il commit → file tornano a untracked
git reset --hard HEAD~1 # annulla il commit → file cancellatiPrima del reset:
gitGraph commit id: "A" commit id: "B" commit id: "C"
Dopo git reset HEAD~1:
gitGraph commit id: "A" commit id: "B"
| Modalità | Commit | Staging | File di lavoro |
|---|---|---|---|
--soft | ✗ annullato | ✓ conservato | ✓ conservato |
--mixed | ✗ annullato | ✗ svuotato | ✓ conservato |
--hard | ✗ annullato | ✗ svuotato | ✗ cancellato |
--hard cancella le modifiche ai file in modo irreversibile. L'unico modo per recuperare è git reflog — ma solo se il commit esisteva già prima del reset.
# Recuperare un commit perso dopo reset --hard
git reflog # trova l'hash del commit perso
git checkout <hash> # ispeziona
git cherry-pick <hash> # o ripristina sul branch correnteRename file/cartella#
git mv vecchio-nome.md nuovo-nome.md
git add .
git commit -m "rename: vecchio-nome → nuovo-nome"Su macOS il filesystem è case-insensitive — git non vede il rename guide → Guide. Fix:
git mv guide guide_temp
git mv guide_temp Guide
git add .
git commit -m "fix: rename guide → Guide"Su macOS il filesystem è case-insensitive: git non rileva il rename guide → Guide. Usa sempre git mv con passaggio intermedio _temp.
Submodule#
Un submodule è un repo Git dentro un altro repo Git. Il repo padre non traccia i file del submodule — traccia solo un puntatore al commit specifico.
# Struttura tipica
cyber-appunti/ ← repo padre (pubblico, Hugo)
├── themes/
│ └── dark-theme-editor/ ← submodule (repo del tema)
├── content/
│ └── notes/ ← submodule (repo Obsidian privato)
└── hugo.tomlAggiungere un submodule#
git submodule add https://github.com/user/repo.git path/destinazione
git commit -m "feat: aggiunto submodule X"Clonare un repo con submodule#
# I submodule arrivano vuoti dopo il clone
git clone <url>
git submodule update --init # popola i submodule
git submodule update --init --recursive # submodule annidatiDopo git clone i submodule arrivano vuoti — esegui sempre git submodule update --init per popolarli.
La sintassi host:porta/path ricorda scp perché entrambi usano lo stesso protocollo — SSH. scp usa : per separare host da path, git SSH usa la stessa convenzione nell'URL.
Aggiornare un submodule#
git submodule update --remote path/submodule # aggiorna all'ultimo commit remoto
git add .
git commit -m "update: submodule X aggiornato"Comandi utili#
git submodule status # stato di tutti i submodule
git submodule foreach git pull # pull in tutti i submodule in un colpoRimuovere un submodule#
git submodule deinit path/submodule
git rm path/submodule
rm -rf .git/modules/path/submodule
git commit -m "remove: submodule X rimosso"Come funziona il puntatore#
# repo padre vede: non vede:
# .gitmodules ← url + path i file dentro il submodule
# commit hash ← ABC123 le modifiche interneIl repo padre sa solo "il submodule deve stare al commit ABC123". Se il submodule avanza di 3 commit, il padre non si aggiorna da solo — devi fare git submodule update --remote + commit nel padre.
Un repo pubblico ha rimosso una API key — ma la storia e' permanente:
git clone https://github.com/target/repo
git log --all --oneline # cerca commit tipo "remove credentials", "fix config"
git show abc1234 # mostra il contenuto di quel commitSe trovi password=, api_key=, SECRET= → segreto esposto Prima di pushare, verifica che non ci siano segreti:
git diff --staged | grep -i "password\|secret\|key\|token"Secret scanning — segreti nella storia#
Un segreto committato anche una sola volta resta recuperabile per sempre, anche se rimosso nel commit successivo.
# Ispeziona la storia cercando commit sospetti
git log --all --oneline
# cerca messaggi tipo: "fix info leak", "remove credentials", "fix config"
# Metodo 1 — mostra il diff di un commit specifico
git show abc1234
# Metodo 2 — confronta due commit esplicitamente
git diff abc1234 def5678
# Verifica prima del push
git diff --staged | grep -i "password\|secret\|key\|token\|api"Strumenti automatici per la scansione: truffleHog, gitleaks, git-secrets.
Un segreto pushato va considerato compromesso, sempre. La pulizia della storia con git filter-repo e' igiene, non sicurezza. L'unica azione corretta e' ruotare immediatamente la credenziale.
.git/objects — il database fisico#
Tutto cio' che git traccia — file, directory, commit — viene salvato in .git/objects/ come oggetto compresso identificato dal suo hash SHA-1.
Sono
.git/objects/
├── d0/
│ └── cf2ab7dd7e... # il commit con la password — sempre qui
├── b0/
│ └── 354c7be30f... # il commit "fix info leak"
└── pack/ # oggetti compressi insieme dopo git gcQuando fai git clone, git copia tutti questi oggetti sul tuo disco. Quando fai force push, cambi solo i puntatori sul remote — gli oggetti nelle copie locali restano finche' git gc non li pulisce.
# Vedere tutti gli oggetti nel database locale
git cat-file --batch-all-objects --batch-check
- `.pack` — gli oggetti compressi (i dati reali: commit, system, alberi)
- `.idx` — l'indice per trovare velocemente un oggetto nel pack per hash
- `.rev` — indice inverso, da posizione nel pack → hash
# Forzare la pulizia degli oggetti non referenziati
git gc --prune=nowgit gc rimuove solo gli oggetti non piu' referenziati da nessun branch o tag. Se un collega non ha ancora allineato il suo branch al remote dopo il force push, l'oggetto con la password e' ncora referenziato nella sua storia locale — git gc non lo tocca.
Caso reale#
Un developer pusho' credenziali AWS su GitHub pubblico. Se ne accorse dopo 4 minuti e fece force push immediato. Bot automatici scansionano GitHub in tempo reale cercando pattern di API key. In quei 4 minuti il bot aveva gia' trovato le credenziali, creato istanze EC2 e iniziato a minare criptovaluta. Costo sulla fattura AWS: ~50.000 dollari. Il force push aveva pulito il repository. .git/objects/ sul server del bot aveva ancora tutto.
Collegato a#
- system — categoria
- ssh — git usa SSH per autenticazione remota



