Quando ho costruito la mia configurazione Git tra Windows e Mac, il punto non era risparmiare o accelerare i trasferimenti. Avevo stabilito un requisito per i progetti commerciali destinati a durare anni: il codice sorgente principale non deve finire in chiaro su una piattaforma cloud di terzi, nemmeno in un repository GitHub Private.

Non sto dicendo che GitHub sottragga i repository privati. Il problema è non voler affidare anni di lavoro all’assunto che non esisteranno mai abusi dei privilegi interni, compromissioni dell’account, vulnerabilità o attacchi alla supply chain. Private è un controllo degli accessi, non la prova che il provider non possa tecnicamente leggere gli oggetti Git originali. Preferisco conservare personalmente le chiavi e lasciare al cloud soltanto i dati cifrati.

Per questo ho scelto un bare repo Git su Windows come Central locale, con il Mac collegato via SMB. Poi sono arrivati il mirror sul disco E, gli archivi 7z cifrati offline e il vault Cryptomator copiato su OneDrive / Google Drive. Tuttavia sincronizzare in cloud un backup cifrato non equivale a poter fare push e pull verso un vero remote Git cifrato. La differenza è diventata evidente quando ho iniziato a voler lavorare con ChatGPT/Codex anche dal telefono.

Conservo qui i comandi storici del periodo 2024–2026 e il test riproducibile con un repository sintetico su Git 2.47.3. Quel test verifica refs, mirror e recupero, non l’effettiva adozione di un remote cifrato o di uno sviluppo mobile continuo.

Perché GitHub Private non era la mia soluzione iniziale

Questo è un modello di minaccia scelto per il progetto, non un’accusa verso GitHub o altri provider. I termini GitHub per i repository privati regolano riservatezza ed eccezioni di accesso da parte del servizio, cosa diversa dalla cifratura client-side di tutta la storia Git con chiavi gestite soltanto da me. Per moltissimi progetti Private è sufficiente; per il mio codice commerciale principale, no.

SceltaCosa può vedere il servizioCompromesso
GitHub Private standardNormali oggetti Git, sorgenti e cronologia commit protetti dai permessiNon soddisfa il vincolo «niente sorgente in chiaro nel cloud»
Git locale + CryptomatorFile cifrati, con alcune informazioni su dimensioni e tempiBackup remoto sì; collaborazione Git nativa no
Intero Git remote cifrato prima dell’uploadL’obiettivo è archiviare solo testo cifrato; dipende dall’implementazioneCandidato da testare prima dell’uso
Snapshot 7z cifrati e scollegatiArchivi storici non aggiornati in tempo realeProtezione dagli incidenti, non collaborazione

La cifratura lato client non protegge dalla compromissione della macchina, dalla perdita delle chiavi o dall’invio volontario di codice decifrato a un servizio AI. Anche Cryptomator documenta limiti e metadati residui.

Architettura Git che evita sorgenti commerciali in chiaro nel cloud: Windows e Mac sincronizzano un bare Central locale, con mirror verso un backup Cryptomator cifrato e archivi 7z offline separati.

Schema: Git resta su dispositivi fidati; il cloud conserva solo backup cifrati lato client e gli archivi offline mantengono la cronologia. Il Git remote cifrato e lo sviluppo mobile restano soluzioni da verificare.

Sincronizzazione Git in LAN Win/Mac: perché usare un bare repo come Central

Prima, come si relazionavano in esecuzione:

Repository di sviluppo su Windows ─── push / pull ──┐
                                                    ├── Central su Windows (bare repo)
Repository di sviluppo su Mac ─ SMB ─ push / pull ──┘              │
                                                                  │ aggiornamento del mirror
                                                                  ▼
                                                     Mirror bare sul disco E
                                                                  │
                                                  archiviazione cifrata / copia offline
                                                                  ▼
                                                   disco esterno, cloud remoto

Dei quattro livelli, solo gli ultimi tre svolgono davvero una funzione di backup. Mettendoli sul tavolo, diventa chiaro quale può fallire insieme agli altri:

Livello e posizioneAzione chiaveCosa copreCosa non risolve
Central di collaborazione: bare repo sul disco D di Windowspush, pull su Win/MacIl codice già pushato si può recuperare anche se una macchina di sviluppo si rompeWindows spento, SMB giù, il Central stesso danneggiato; non conta come backup a freddo indipendente
Mirror sulla stessa macchina: mirror sul disco E di Windowsclone --mirror, remote updateRipristino dal mirror più recente se la directory del Central si danneggiaGuasto dell’intera macchina (D ed E cadono insieme); --prune può cancellare branch sincronizzati
Archivio offline: disco esterno scollegatoPacchetto cifrato 7z, 7z tGuasto dell’host, ransomware che colpisce le copie onlineL’aggiornamento è manuale; la versione più recente dipende dalla data dell’archivio
Cifratura remota: Cryptomator + OneDrive / Google Driverobocopy, verifica del manifest cifrato, verifica del downloadPerdita totale dei dispositivi locali, recupero da remotoAnche la sincronizzazione cloud può propagare errori; non include codice non committato né dati esterni

L’RPO (fino a quale versione puoi perdere al massimo) è deciso dall’ora dell’ultimo commit, sincronizzazione o archiviazione riusciti di ciascun livello; l’RTO (quanto ci metti a ripristinare) dipende anche dalla velocità del mezzo, dalla decifratura e dalle prove. Non ho mai registrato in modo specifico il tempo reale di ripristino, quindi qui confronto solo i percorsi di recupero, senza travestire minuti ipotetici da RTO/RPO. D ed E sono sullo stesso Windows: due lettere di unità non eliminano il rischio comune del guasto dell’intera macchina.

Il punto chiave del Central non è “avere una cartella condivisa”, ma che dentro c’è un bare repository. Ha gli oggetti, i riferimenti e la configurazione del repository Git, ma nessun working tree normale. Le due macchine di sviluppo hanno ciascuna il proprio albero di lavoro, le modifiche si fanno in locale e vengono pushate al Central solo dopo il commit.

Secondo la documentazione ufficiale di git init, la forma base per creare un bare repo è:

# Percorso di esempio, non era la directory della mia macchina originale
git init --bare "D:/git-central/project.git"

# Configurare il Central in un repository di lavoro Windows già esistente
git remote add central "D:/git-central/project.git"
git push -u central master

Sul Mac prima si monta la condivisione SMB in locale, poi si imposta il percorso montato come remote central. Il master qui sotto era il nome di branch usato dal repository a quell’epoca; se il tuo branch predefinito è main, non copiare master alla lettera.

git remote add central "/Volumes/git-central/project.git"
git fetch central
git pull --ff-only central master

Nel vecchio manuale operativo restavano alcune righe molto concrete di diagnostica sul Mac, che risparmiavano il dubbio “Git non si è aggiornato, oppure l’SMB non è proprio montato?”:

# Montare sul Mac la condivisione SMB di Windows (indirizzo di esempio)
open "smb://<windows-host>/<share-name>"

# Prima confermare dove la condivisione è montata davvero
ls /Volumes
ls "/Volumes/<share-name>"

# Poi, dal workspace del progetto, confermare dove punta il remote Git e su che branch sei
git remote -v
git branch --show-current
git status --short

Le note originali dicevano: prima verificare che nella directory condivisa si veda davvero il bare repo, e poi operare il remote central; non fare reset --hard al primo errore di git pull. Su Windows si seguiva lo stesso ordine: guardare git status, committare e poi git push central master. Questi comandi vengono dalle note operative di allora; nell’articolo i nomi host interni e i percorsi della condivisione sono stati sostituiti con segnaposto di esempio.

Nella routine quotidiana di allora, prima di sviluppare facevo git pull central master, alla fine guardavo git status, poi git add ., git commit, git push central master. L’altra macchina tirava dallo stesso ingresso. Il --ff-only dell’esempio è una protezione che oggi preferisco conservare: in caso di divergenza ci si ferma e la si risolve, invece di lasciare che un pull normale generi silenziosamente un commit di merge.

Questo schema aveva un vantaggio molto tangibile: se il Central si rompeva, la cronologia Git delle due macchine di sviluppo restava lì; se si rompeva una macchina di sviluppo, la cronologia non spariva con lei. Ho sempre preso “un repository completo può servire da seme per ricostruire” come principio di progettazione. Ma questa frase ha delle condizioni: quel repository deve conservare davvero i branch, i tag e gli oggetti che ti servono. Un clone di un singolo branch, uno shallow clone, o file non committati lasciati nel working tree, non contano automaticamente come backup completo.

Un altro limite facile da trascurare: accedere al bare repo direttamente via SMB dipende in pratica dai permessi del filesystem condiviso, dai lock e dalla stabilità della rete. Lo schema vecchio copriva la mia abitudine con due macchine, ma questo non lo rende adatto a più regioni, più utenti o reti instabili. Per un nuovo servizio Git duraturo, oggi preferirei un trasporto remoto come Git over SSH piuttosto che trattare un disco condiviso come server Git generico.

Mirror di Git a freddo: la sincronizzazione delle cancellazioni e l’istantanea storica sono due cose diverse

Ciò che ha davvero creato distanza di sicurezza sono stati gli strati dietro il Central. Sul disco E originale c’era un bare repo clonato con --mirror, che non partecipava allo sviluppo e veniva solo aggiornato; il disco esterno conservava archivi cifrati, normalmente offline; e il cloud conservava un’altra copia remota del contenuto cifrato. Non erano quattro git clone identici, ma risposte a diversi ambiti di guasto.

Vale la pena leggere con attenzione la definizione ufficiale di git clone --mirror: il mirror non include solo i branch, ma sincronizza anche altri riferimenti secondo il refspec dello specchio. A differenza di un clone normale di sviluppo, un successivo git remote update può sovrascrivere le ref corrispondenti. La creazione iniziale e l’aggiornamento quotidiano si potevano scrivere così:

# Eseguire solo una volta, all'inizializzazione del repository a freddo
git clone --mirror "D:/git-central/project.git" "E:/git-cold/project.git"

# Aggiornamenti successivi del mirror. --prune può eliminare riferimenti non più esistenti all'origine
git --git-dir="E:/git-cold/project.git" remote update --prune

# Controlla solo gli oggetti Git; non equivale ad aver fatto una prova di ripristino
git --git-dir="E:/git-cold/project.git" fsck --full

Il manuale operativo dell’E a freddo di allora, oltre a git remote update, lasciava scritto un modo per confermare il commit corrente:

# Eseguire dentro il bare repo del mirror; questo esempio mantiene lo storico branch master
git remote update
git log origin/master -1 --oneline

# Non basta confrontare l'ultimo log; un ripristino formale richiede il confronto di ref e oggetti
git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags

Quell’origin/master è la scrittura delle note storiche; se un bare mirror permette di accedere al riferimento con quel nome dipende dalla configurazione reale e dal risultato di git show-ref. La pratica generica più affidabile è guardare direttamente refs/heads/master e le altre ref da conservare.

Con Git 2.47.3 ho creato un repository temporaneo e ho percorso la catena di cancellazione di --prune: ho pushato il branch test-prune, ho sincronizzato il mirror, poi ho cancellato quel branch dal Central e ho aggiornato il mirror. Le righe chiave dell’output sono solo queste:

git_version=git version 2.47.3
feature_after_sync=present 733c4dc75735672f2db8b4ed8185058ceddaf8c7
feature_after_prune=absent
git_fsck_exit=0
git_fsck_output=dangling commit 733c4dc75735672f2db8b4ed8185058ceddaf8c7
restored_master_sha=c7b763f667cbbba6c1bb39a6719236a554257a8c

Nota bene: git fsck restituisce 0 e trova ancora il dangling commit, ma il mirror non ha più il riferimento al branch test-prune. L’oggetto orfano potrebbe sparire in una successiva garbage collection. Il mirror non è una “macchina del tempo”: sincronizza lo stato più recente, e sincronizza anche le cancellazioni. Questo esperimento verifica il comportamento dei riferimenti di Git; l’affidabilità di 7z e del cloud richiede comunque prove di ripristino con archivi reali.

Secondo la documentazione di git remote, remote update fa un fetch; --prune pulisce secondo la configurazione i riferimenti remoti ormai scaduti. Ogni aggiornamento normalmente trasferisce solo i nuovi oggetti necessari, senza dover ricopiare l’intero repository. Ma un mirror è una copia sincronizzata, non un’istantanea immutabile. Se all’origine un branch viene cancellato per errore, la successiva sincronizzazione con prune può propagare quella cancellazione al mirror. È ideale per recuperare in fretta lo stato corrente, ma non può caricarsi da solo di tutta la responsabilità “anti-cancellazione e anti-ransomware”.

Per questo allora mantenevo due rami di backup distinti: uno impacchettava il repository a freddo in un archivio 7z cifrato datato e lo lasciava su un disco esterno normalmente scollegato; l’altro conservava il mirror di Git dentro il vault cifrato di Cryptomator e copiava la directory cifrata del vault nella directory di sincronizzazione di OneDrive / Google Drive. Potevano proteggere la stessa cronologia Git, ma non si può disegnare alla leggera come “tutte le copie sul cloud passano prima da 7z e poi da Cryptomator”. Ciò che ha davvero valore è che un determinato archivio storico non venga sovrascritto al successivo aggiornamento del Central. La password di cifratura non va tenuta insieme all’archivio, e una copia riuscita non equivale a un ripristino riuscito.

Il Central su D e il mirror su E, sullo stesso Windows, attutiscono un uso errato del repository o il danno a un singolo volume, ma non fermano un guasto dell’intera macchina, un furto o un ransomware sulla stessa macchina. Il disco esterno ha senso come isolamento offline solo se viene davvero scollegato a backup terminato; lo storage cloud, se sincronizza una cancellazione o una corruzione, non è detto che conservi una vecchia versione utilizzabile. Questa differenza fu ciò che mi fece smettere di accontentarmi di “ho già diverse copie”.

E una precisazione di ambito: Git salva gli oggetti e i riferimenti Git già committati, ma non ti fa automaticamente il backup dei dati grandi esclusi da .gitignore, delle modifiche non committate nel working tree né dei database esterni. Nei progetti con Git LFS o sottomoduli bisogna verificare separatamente se quegli oggetti e dipendenze sono coperti dal backup. Che il mirror del repository sia completo non equivale alla completezza dell’intero ambiente di sviluppo. Dati in esecuzione come i volumi Docker richiedono un altro schema di backup; ne ho già scritto separatamente in la pratica di disaster recovery con backup dei volumi Docker su GitHub.

Cryptomator + cloud: come verificare con SHA-256 il backup cifrato

Nello strato cloud la cosa più laboriosa non era caricare, ma sapere dopo il caricamento che non mancasse nessun file. Allora lasciai due note operative specifiche per Cryptomator + OneDrive e Cryptomator + Google Drive, con il manifest cifrato di riferimento, l’ordine di chiamata di VaultIntegrityCheck.ps1 e come localizzare MISSING, CHANGED ed EXTRA dopo la copia.

Cryptomator si occupa della cifratura di contenuti e nomi lato client, il client del cloud si occupa del trasferimento: non sono due funzioni dello stesso software. La nota di sicurezza ufficiale di Cryptomator sottolinea anche che la cifratura non sostituisce il backup. Il vecchio flusso reale assomigliava più a questo:

E: mirror a freddo / vault cifrato di Cryptomator (riferimento)
     │
     ├─ Generare il manifest delle impronte della directory cifrata stabile (Update)
     │
     ├─ robocopy nella directory locale di sincronizzazione di OneDrive o Google Drive
     │
     ├─ Attendere che il client cloud mostri la sincronizzazione completa
     │
     └─ Confronto in sola lettura della directory cifrata di destinazione (Verify)
          ├─ MISSING: esiste all'origine, manca a destinazione
          ├─ CHANGED: stesso percorso, ma impronta del file cambiata
          ├─ EXTRA: file in più a destinazione, bisogna distinguere conflitti o temporanei
          └─ Corrisponde: la directory locale sincronizzata corrisponde al manifest

Il mio VaultIntegrityCheck.ps1 distingueva Update e Verify: la base si aggiornava solo dopo aver verificato la copia master cifrata su E:, mentre il confronto della destinazione non la modificava. Queste sono invocazioni storiche dello script privato; il confronto PowerShell nativo più avanti può verificare due directory locali, ma non mantiene un manifest di riferimento persistente:

# Aggiornare il riferimento solo quando il vault di origine su E: ha smesso di scrivere ed è corretto
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "E:\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Update

# Quando OneDrive ha finito di caricare, verifica in sola lettura della directory cifrata di destinazione
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "C:\CloudSync\OneDrive\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Verify

La versione per Google Drive conservava inoltre un comando robocopy di copia tra i due passi, con i parametri /E /R:2 /W:2 /MT:8 /FFT /Z /NP. Con directory neutre, lo scheletro riutilizzabile è:

robocopy "E:\BackupVault" "G:\MyDrive\BackupVault" /E /R:2 /W:2 /MT:8 /FFT /Z /NP
if ($LASTEXITCODE -ge 8) { throw "Robocopy failed with code $LASTEXITCODE" }

La documentazione Microsoft sui codici di uscita di Robocopy conferma: solo un codice pari o superiore a 8 significa che almeno una copia è fallita. Ma uno 0–7 indica solo che la fase di copia non ha riportato quel fallimento; non va mai scritto come “tutti gli SHA-256 dei file sono passati”. /E include le sottodirectory, ma non si occupa di cancellare a destinazione i file superflui già esistenti; non passerei alla leggera a /MIR, che cancella a destinazione, avendo un’unica copia sul cloud.

Il feedback di verifica lasciato dalle note storiche includeva tipi come [OK] Vault matches the manifest exactly., MISSING, CHANGED o EXTRA. Davanti a un MISSING o un CHANGED, prima bisogna controllare se l’upload è finito e se il file è ancora in scrittura; non eseguire Update sulla directory del cloud per coprire l’anomalia con un nuovo riferimento. Le note di Google Drive registravano anche file temporanei .tmp.driveupload; possono essere uno stato intermedio di sincronizzazione, non vanno ignorati senza condizioni: la cosa prudente è riverificare a sincronizzazione terminata.

Se il lettore non ha quello script privato, almeno può usare Get-FileHash, incluso in Windows, per controllare lo SHA-256 di un singolo file. È un cmdlet standard di PowerShell; l’algoritmo e il parametro -LiteralPath sono nella documentazione ufficiale Microsoft:

Get-FileHash -Algorithm SHA256 -LiteralPath "E:\BackupVault\vault.cryptomator"
Get-FileHash -Algorithm SHA256 -LiteralPath "C:\CloudSync\OneDrive\BackupVault\vault.cryptomator"

Ma controllare un solo file di configurazione non basta nemmeno lontanamente. Il manifest dell’intero vault dovrebbe coprire almeno il percorso relativo, la dimensione e lo SHA-256 di ogni file cifrato, oltre a rilevare a destinazione file in più, in meno o cambiati a parità di nome; e deve avere una data di riferimento chiara, senza confrontare mentre un estremo sta ancora scrivendo. Senza uno strumento di verifica già pronto, va bene anche uno script PowerShell generico in sola lettura: elenca ricorsivamente percorso relativo, byte e SHA-256 dei file di origine e destinazione, e trova le differenze con Compare-Object:

function Get-Inventory([string]$root) {
  $base = (Resolve-Path -LiteralPath $root -ErrorAction Stop).ProviderPath.TrimEnd([char]'\')
  Get-ChildItem -LiteralPath $base -File -Recurse -Force -ErrorAction Stop |
    ForEach-Object {
      [pscustomobject]@{
        Path   = $_.FullName.Substring($base.Length).TrimStart([char]'\')
        Bytes  = $_.Length
        SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
      }
    }
}
$original = @(Get-Inventory "E:\BackupVault")
$replica  = @(Get-Inventory "C:\CloudSync\OneDrive\BackupVault")
if (!$original.Count -or !$replica.Count) { throw "Empty or unavailable vault" }
$diff = Compare-Object $original $replica -Property Path,Bytes,SHA256
if ($diff) {
  $diff | Format-Table -AutoSize
  throw "Local encrypted vaults differ"
}
Write-Host "[OK] Local encrypted vault file hashes match"

Questo nuovo esempio copre solo i file, non le directory vuote né lo stato sul server cloud, e non ha la capacità di riferimento persistente dello storico Update/Verify. Alla verifica reale sul cloud manca ancora un passo: riscaricare in una posizione estranea alla directory di sincronizzazione una determinata versione dell’archivio cifrato sul cloud, e confrontare l’hash del file scaricato con il registro affidabile salvato prima del caricamento. Che l’icona di sincronizzazione del cloud dica “completo” più una directory locale coincidente, da soli, non provano che il file remoto sia recuperabile.

Per il disco esterno offline, allora optai per il pacchetto cifrato completo di 7z invece di dipendere sempre dal mirror caldo. Il -mhe=on di 7z cifra i nomi dei file dentro il formato 7z, -p indica di usare una password; non mettere la password in chiaro dentro script condivisi, cronologia dei comandi o log. La specifica ufficiale del formato 7z di 7-Zip conferma il supporto di AES-256. Quando crei l’archivio, consiglio di confermare password e “cifratura dei nomi file” nella GUI; qui sotto lascio l’esempio di parametri CLI, ma prima di operare verifica con 7z -h sulla tua macchina la versione e il modo di inserire la password, e non scrivere la password direttamente in script o cronologia:

# Solo quando il repository a freddo non è in scrittura: crearne un 7z con password
7z a -t7z -p -mhe=on "git-snapshot-YYYYMMDD.7z" "E:\GitCold\project.git"

# Provare se l'archivio si legge per intero: dopo aver inserito la password, controllare il codice di uscita
7z t "git-snapshot-YYYYMMDD.7z"

Ad esempio, se quell’archivio viene riscaricato dal cloud in D:\Downloads, così si verifica che i byte coincidano prima e dopo il caricamento:

$before = (Get-FileHash "D:\GitArchive\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
$after  = (Get-FileHash "D:\Downloads\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
if ($before -ne $after) { throw "Downloaded archive SHA-256 mismatch" }

Che gli hash coincidano dimostra solo che quei due archivi sono byte per byte identici; non prova che il riferimento non sia stato manomesso, né sostituisce 7z t, l’estrazione, git fsck, il confronto delle ref e il clone reale. Il manifest di riferimento conviene tenerlo separato e protetto dallo stesso guasto che potrebbe alterarlo; il backup del disco esterno va davvero scollegato a lavoro finito. Solo separando “il trasferimento cifrato è corretto” da “la cronologia del codice è recuperabile” in verifiche distinte eviterai, al momento del problema, di prendere un [OK] per un’assicurazione totale.

Il tassello mancante: Git push verso un remote cifrato

La configurazione originale aveva due percorsi distinti: Windows e Mac facevano git push e git pull contro il bare Central locale in chiaro; separatamente, un mirror veniva inserito nel vault Cryptomator e solo i file cifrati del vault venivano sincronizzati sul cloud. Un trasferimento frequente del vault non diventa automaticamente un remote Git con branch, diff e pull request leggibili da GitHub. Non posso descriverlo come un sistema di push cifrato in tempo reale già operativo.

Vorrei trasmettere appena possibile dopo ogni commit una rappresentazione cifrata dell’intera cronologia, senza affidare al cloud le chiavi. Prima bisogna verificare cifratura prima della trasmissione, recupero delle chiavi, aggiornamenti concorrenti, trasferimenti interrotti, sovrascritture forzate e un ripristino reale. Le modifiche non ancora committate non sono incluse.

Un candidato è git-remote-gcrypt, che cifra la rappresentazione del remote con GnuPG. Il README del progetto segnala che il backend basato su normali URL Git ritrasmette tutta la storia a ogni push e presenta rischi con i push forzati. Un backend rsync può trasferire gli incrementi, ma non è l’uso consueto di GitHub Private. Non ho ancora collaudato questa soluzione nel repository commerciale.

Nemmeno git-crypt risolve l’intero problema: cifra file selezionati senza nascondere nomi, messaggi di commit e altri metadati. Automatizzare backup cifrati più frequenti è un’altra strada, utile per il ripristino ma incapace di rendere direttamente utilizzabili i PR, i diff e gli agenti cloud sul codice cifrato.

Disaster recovery in Git: da quale copia ripristinare codice e cronologia

Allora mi lasciai due percorsi di ripristino. Davanti a un incidente, prima giudicherei quale copia conserva ancora le ref obiettivo, invece di ricostruire subito un progetto vuoto:

Bisogna recuperare un progetto Git
  ├─ La macchina di sviluppo conserva commit / branch / tag completi
  │    └─ Creare un Central bare vuoto → controllare le ref → pushare branch locali e tag
  ├─ La macchina di sviluppo è incompleta, ma il mirror sul disco E è ancora leggibile
  │    └─ fsck → controllare le ref → clonare un workspace e confrontare la cronologia
  └─ Nessuna copia online disponibile
       └─ Cercare un archivio storico offline o remoto → verificare SHA-256 → testare ed estrarre il 7z
                                     → git fsck → confrontare le ref → clone

Questa decisione mi ricordò anche una cosa: git push --all pusha solo i branch locali. Che il workspace attuale si avvii non significa che tutti i branch di tutte le macchine di sviluppo siano in quella copia.

Un percorso è: il Central è andato perso, ma resta un repository di sviluppo completo. Allora si può ricreare un bare repo vuoto e restituirgli dalla macchina di sviluppo i branch e i tag necessari:

# Creare un bare repo vuoto in una posizione scrivibile del nuovo Central
git init --bare "/path/to/recovered/project.git"

# Eseguire dentro il repository di sviluppo superstite
git remote add recovered "/path/to/recovered/project.git"
git push recovered --all
git push recovered --tags

Qui va detto chiaro: --all pusha i branch locali di questa macchina di sviluppo, non tutti i refs/remotes/*. Se un’altra macchina ebbe una volta un branch indipendente e questa macchina non lo salvò come branch locale, non si può pretendere che git push --all lo recuperi dal nulla. Prima di spostare formalmente il Central, bisogna confrontare il manifest delle ref dei repository superstiti e, se serve, completarlo da altre copie superstiti.

L’altro, più estremo: né la macchina di sviluppo né il Central sono disponibili. L’ingresso del recupero diventa l’archivio sul disco esterno o sul cloud. L’ordine dovrebbe essere: prendere un backup con data chiara, completare decifratura ed estrazione, controllare gli oggetti del bare repo, confrontare le ref attese e infine clonare il workspace. Non sovrascrivere ancora il backup appena estratto, e non eseguire come primo passo un push del mirror con --force o --prune.

# Eseguire sul bare repo ottenuto dall'estrazione
git --git-dir="/restore/project.git" fsck --full
git --git-dir="/restore/project.git" for-each-ref \
  --format='%(refname) %(objectname)' refs/heads refs/tags

# Quando il repository è leggibile, creare il workspace
git clone "/restore/project.git" recovered-worktree

git fsck controlla la connettività e la validità degli oggetti Git; git for-each-ref elenca gli object ID corrispondenti a branch e tag. Insieme, almeno rispondono a “il pacchetto è danneggiato?” e “ci sono i branch e i tag attesi?”. Ma per provare che il ripristino è reale, bisogna ancora controllare il contenuto dei file, le dipendenze, i dati esterni, e se su una macchina nuova si può continuare a compilare e lavorare.

Quando più tardi ordinai verify-coldbackup.ps1, fissai l’ordine di controllo in una catena: test di integrità del 7z → decifratura ed estrazione → clonare il workspace → git fsck degli oggetti → confrontare lo SHA di tutti i branch e i tag. Guardare solo “il compresso si apre” non basta nemmeno lontanamente; per confermare che il codice è davvero coperto dal backup, bisogna percorrere il ripristino completo in una directory indipendente.

Tra parentesi, l’altro percorso “fare mirror dei progetti pubblici upstream nel mio repo privato GitHub” è una cosa diversa da questo Central in LAN. Quel percorso deve gestire le ref limitate di GitHub, la copertura dei branch e il rischio di cancellazione a destinazione di --force --prune, e non dovrebbe mescolare branch personali di sviluppo nella destinazione del mirror. I problemi concreti del mirror li ho già messi separatamente in la sincronizzazione dei repository GitHub pubblici in mirror privati. Anche la documentazione ufficiale di git push spiega con chiarezza le differenze tra --mirror, --prune e i refspec.

Il limite emerso quando provo a sviluppare dal telefono

Finché lavoravo da Windows o Mac, avevo due copie di lavoro complete e un Central locale. Ma oggi, a volte, vorrei far analizzare o modificare un dettaglio a ChatGPT/Codex quando ho solo lo smartphone. Con Windows spento o la condivisione SMB inaccessibile, la collaborazione si blocca. Partial clone e sparse checkout sul Mac riducono lo spazio occupato, non risolvono la disponibilità né decidono chi può decifrare.

Gli agenti di programmazione nel cloud, normalmente, devono leggere ed eseguire codice sorgente. Se il remote contiene solo testo cifrato, GitHub non può offrire diff, ricerca, PR e agenti che lo comprendano. Se decifrassi il progetto in una macchina cloud di cui non mi fido, verrebbe meno proprio il vincolo che ha motivato l’architettura.

La direzione più coerente è usare il telefono solo come console di controllo, raggiungendo via rete privata e SSH un Mac/Linux di mia fiducia, acceso e protetto, dove restano codice, Git e strumenti di sviluppo. È una proposta da verificare, non un sistema già accettato in produzione: disponibilità, autenticazione, isolamento dei permessi e gestione delle chiavi restano problemi reali. E se un modello AI remoto riceve il sorgente decifrato, si apre comunque un altro confine di uscita del codice in chiaro.

Anche un Git remote interamente cifrato migliora il trasferimento delle copie, non rende automaticamente leggibile il codice a un agente cloud. Devo ancora chiarire chi decifra, dove gira il codice e cosa accade se il telefono perde la connessione. Fino a quella verifica, promuovere un normale GitHub Private a Central unico non risponderebbe alla mia esigenza originaria.

Domande frequenti su bare repo di Git, SMB e ripristino dei backup

Perché non condividere direttamente via SMB un workspace Git normale?

Git rifiuta per impostazione predefinita il push sul branch attualmente checkout in un repository non bare (receive.denyCurrentBranch). Un bare repo non ha working tree e funge da Central, lasciando a Win/Mac le proprie copie.

Il Mac monta l’SMB ma git pull fallisce: cosa controllare prima?

Controlla prima ls /Volumes, git remote -v e i permessi SMB. Non eliminare file .lock prima di aver verificato che nessun processo Git sia ancora attivo; i lock di un bare repo possono trovarsi anche nelle refs. Per uso di rete frequente valuta Git over SSH.

git clone --mirror è più sicuro di un git clone normale?

--mirror sincronizza le refs, anche le eliminazioni del repository sorgente. Per prevenire la perdita di storia servono archivi datati non aggiornati insieme al mirror.

OneDrive / Google Drive dice “sincronizzato”: è già una verifica hash superata?

No. Stato di sincronizzazione, confronto del manifest cifrato locale, SHA-256 di un nuovo download dal cloud e ripristino tramite decifratura, git fsck, refs e clone sono verifiche distinte. L’[OK] del vecchio script riguardava solo la copia locale confrontata con la base.

Perché non GitHub Private? Quanto spesso provare il ripristino?

GitHub Private controlla l’accesso, ma non soddisfa la mia regola di non depositare sorgenti leggibili sul cloud. Dopo la configurazione o la modifica del backup cifrato, prova un recupero indipendente con chiave, oggetti Git, refs e clone: senza chiave, la copia cifrata è inutilizzabile.

Riferimenti: Git init · Git clone · Git remote · Git push · Git fsck · Robocopy · Get-FileHash · Limiti di sicurezza di Cryptomator · Formato 7z ufficiale di 7-Zip