GitHub è un servizio che ospita repository Git in rete e ci costruisce sopra gli strumenti per lavorare in più persone: copia di sicurezza, discussione, revisione del codice, controlli automatici. Git è il programma sul tuo computer; GitHub è un posto dove i repository si incontrano. La funzione che conta di più si chiama pull request: una richiesta di integrare un tuo branch in un altro, che gli altri possono leggere, commentare e approvare prima che il codice entri. Per un progetto ML è il momento in cui qualcuno guarda anche la soglia della metrica e il dataset usato, non solo lo stile del codice.
Le parole della collaborazione
Questa lezione usa il vocabolario di Git (commit, branch, merge, spiegati in Git, la macchina del tempo del tuo progetto) più alcune parole che riguardano il lavoro condiviso. Un remoto (remote) è un'altra copia dello stesso repository, su un server: il "bancone condiviso" dove i colleghi appoggiano il lavoro, e il principale si chiama per convenzione origin. Clone significa scaricare una copia completa di un repository remoto, con tutta la storia, e collegarla a lui. Push manda i tuoi commit al remoto, fetch scarica le novità senza toccare il tuo lavoro, pull fa fetch e poi integra.
Un fork è una copia personale di un repository altrui sul server, su cui puoi scrivere; il repository originale si chiama di solito upstream. La pull request (PR) è la richiesta formale "integrate il mio branch nel vostro" ed è anche il luogo della discussione e della code review, cioè la lettura critica del codice da parte di un'altra persona, come il correttore di bozze prima della stampa. Un branch protetto è un branch su cui la piattaforma impone regole (revisione obbligatoria, controlli superati, niente push diretto). La CI (integrazione continua) sono i controlli automatici, test e linter, che girano a ogni modifica: qui basta sapere che compare accanto alla PR come un segno verde o rosso, la spiega a fondo la lezione sul deploy manuale contro CI/CD.
Il flusso con pull request in un colpo d'occhio:
L'analogia: la redazione di un giornale
Ogni giornalista scrive il suo pezzo sul proprio computer (il repository locale). Il giornale ha un server comune dove i pezzi pubblicati vivono (il remoto). Prima che un articolo vada in pagina passa dal caporedattore, che lo legge, annota correzioni, e solo dopo lo approva. La pull request è quel passaggio: ciò che è sul tuo computer non è ancora "del giornale" finché qualcun altro non l'ha visto.
Il meccanismo: locale, remoto, e tre comandi da non confondere
Un repository può avere uno o più remoti, cioè indirizzi di copie dello stesso repository. I comandi di base sono questi; sono uno schema da adattare, perché l'indirizzo github.com/utente/progetto è un segnaposto (lo scenario eseguito davvero è più sotto).
git clone https://github.com/utente/progetto.git # copia il remoto e imposta origin
git remote -v # elenca i remoti configurati
git push -u origin esperimento/c-regolarizzazione # invia il branch (e ricorda il collegamento)
git fetch origin # SCARICA le novità, senza toccare i tuoi file
git pull # fetch + integrazione nel branch corrente
La distinzione fetch contro pull evita molti pasticci. fetch aggiorna solo la tua conoscenza di cosa c'è sul remoto, ed è sempre sicuro. pull in più prova a fondere quelle novità nel tuo lavoro, e può quindi produrre conflitti. Se vuoi guardare prima di integrare: git fetch, poi git log HEAD..origin/main --oneline.
Il flusso con pull request, passo per passo
- Parti aggiornato:
git switch main && git pull. - Apri un branch con un nome parlante:
git switch -c fix/soglia-auc. - Lavora a commit piccoli e fai push:
git push -u origin fix/soglia-auc. - Apri la pull request dall'interfaccia web, oppure con l'interfaccia a riga di comando di GitHub:
gh pr create --fill. - Revisione: i colleghi commentano righe precise; tu rispondi o correggi con nuovi commit sullo stesso branch (la PR si aggiorna da sola).
- Controlli automatici: se configurati, test e linter girano e mostrano un esito verde o rosso accanto alla PR.
- Merge quando è approvata. Poi cancelli il branch.
L'ordine qui è parte del contenuto: se apri la PR prima di avere fatto push non c'è nulla da rivedere, e se fai il merge prima dei controlli perdi proprio la garanzia per cui la PR esiste. Non ho potuto eseguire gh pr create in questo lavoro (serve un account GitHub e una rete autenticata), quindi il comando è riportato dalla documentazione di GitHub CLI e non è stato provato. Una buona descrizione di PR risponde a tre domande: che problema risolve, come l'ho verificato, cosa dovrebbe guardare con più attenzione chi la legge. In un progetto ML aggiungi: quale metrica è cambiata, su quale dataset e con quale seed. "Migliora il modello" senza numeri non è un'informazione.
Fork: contribuire a un progetto che non è tuo
Se non hai il permesso di scrivere nel repository (è il caso di quasi ogni progetto open source), fai un fork: una tua copia del repository sul tuo account. Lavori sulla copia, e dal tuo fork apri una PR verso l'originale. Gli indirizzi sono segnaposto, e anche questo blocco è uno schema da adattare:
git remote add upstream https://github.com/organizzazione/progetto.git
git fetch upstream
git switch -c fix/refuso-nella-documentazione upstream/main
Il terzo comando parte da upstream/main e non dal tuo main, così il branch nasce dallo stato attuale del progetto originale, non da una copia che potrebbe essere rimasta indietro. Come contribuire a un progetto altrui, licenze incluse, lo vedi in open source, licenze e come contribuire.
Branch protetti: far rispettare le regole alle macchine
Una regola scritta in un documento si dimentica. Una regola applicata dalla piattaforma no. Su GitHub puoi proteggere un branch (di solito main) con una regola di protezione nelle impostazioni del repository: richiedere almeno un'approvazione prima del merge, richiedere che i controlli automatici passino, vietare il push diretto, vietare la riscrittura della storia. È il modo con cui "i test devono passare" smette di essere un'intenzione. Il dettaglio delle opzioni è nella documentazione sui branch protetti; nomi e posizioni delle voci possono cambiare, quindi controllala invece di fidarti di questa lezione.
I conflitti, senza paura
Un conflitto di merge compare quando due persone hanno modificato le stesse righe. Git non decide al posto tuo: marca il file così.
<<<<<<< HEAD
AUC_MIN = 0.70
=======
AUC_MIN = 0.72
>>>>>>> fix/soglia-auc
Il compito è scegliere (o combinare) le due versioni, cancellare i marcatori, poi git add file e git commit. Il consiglio pratico è che i conflitti diventano rari se i branch vivono poco e le PR sono piccole: un branch aperto per tre settimane è un conflitto che si sta accumulando.
Il progetto di questo sito lavora diversamente, ed è un buon esempio
Qui non si usano feature branch né pull request: si lavora e si committa direttamente su master, con un solo sviluppatore e un repository privato (regola scritta in CLAUDE.md). Non è in contraddizione con quanto sopra, è un trade-off: la pull request esiste per far guardare il lavoro a un'altra persona. Con una persona sola, il costo (aprire, descrivere, approvare da soli) supera il beneficio, e al suo posto lavorano altri controlli: test eseguiti prima del commit e una storia pulita con messaggi nel formato tipo(ambito): oggetto. Appena entra una seconda persona, il calcolo cambia.
| Contesto | Flusso ragionevole |
|---|---|
| Da solo, repository privato, deploy manuale | Commit diretto su main, test prima del commit |
| Piccolo team, codice condiviso | Branch breve + PR + un revisore + test obbligatori |
| Progetto open source | Fork + PR verso upstream, seguendo le regole del progetto |
Trappole frequenti
La prima è il push di un segreto: se una chiave finisce su un remoto, cancellarla in un commit successivo non basta, resta nella storia, e la chiave va revocata e sostituita. La seconda sono i file grandi: la documentazione di GitHub dice che la piattaforma blocca i file oltre i 100 MiB, e comunque Git non è lo strumento per i dataset (se ne parla nella lezione su Git). La terza è git push --force su un branch condiviso, che cancella il lavoro altrui: se proprio serve, --force-with-lease rifiuta l'operazione se il remoto è cambiato nel frattempo. La quarta sono le PR gigantesche: nessuno le legge davvero, e più piccola è la PR, migliore è la revisione.
Un remoto vero, senza account: provarlo in locale
Per capire push, fetch e conflitti non serve GitHub: un "remoto" è solo un repository in un'altra cartella. Con Git 2.53 ho creato un repository bare (senza area di lavoro, come fa un server) e due cloni, "Anna" e "Bruno", che lavorano sullo stesso progetto. Ho eseguito l'intero scenario due volte, con LC_ALL=C, identità e date dei commit fissate: le uscite sono risultate identiche, hash compresi. Sul tuo computer gli hash saranno diversi e il percorso del remoto sarà il tuo (qui l'ho sostituito con /percorso).
git init --bare -b main remoto.git # il "server": solo storia, nessun file di lavoro
git clone remoto.git anna
# Cloning into 'anna'...
# warning: You appear to have cloned an empty repository.
--bare crea un repository destinato a ricevere push; il clone di un repository vuoto avverte, correttamente, che non c'è ancora nulla.
cd anna
printf 'AUC_MIN = 0.70\n' > cfg.py
git add . && git commit -m "feat: soglia"
git push -u origin main
# To /percorso/remoto.git
# * [new branch] main -> main
# branch 'main' set up to track 'origin/main'.
Il -u (--set-upstream) ha collegato il tuo main locale a origin/main: da ora in poi git push e git pull senza argomenti sanno dove andare, e git status sa dirti se sei avanti o indietro rispetto al remoto.
git clone remoto.git bruno
cd bruno && git remote -v
# origin /percorso/remoto.git (fetch)
# origin /percorso/remoto.git (push)
git log --oneline
# 0abce59 feat: soglia
Bruno ottiene l'intera storia e git remote -v mostra origin con due righe, (fetch) e (push): da dove scarica e dove invia. Su GitHub l'indirizzo è un URL https://... o git@github.com:...: cambia il percorso, non il meccanismo.
Fetch: guardare prima di toccare
Anna pubblica una modifica (tune: soglia 0.72). Bruno non la vede finché non la chiede:
git fetch origin
# 0abce59..824c4ca main -> origin/main
git log HEAD..origin/main --oneline
# 824c4ca tune: soglia 0.72
git status -sb
# ## main...origin/main [behind 1]
Il fetch ha aggiornato la sua copia locale del remoto; HEAD..origin/main significa "ciò che è nel remoto e non nel mio branch", e la riga [behind 1] dice "sei indietro di un commit". Il tuo lavoro non è stato toccato: è per questo che fetch è sempre sicuro.
Il conflitto, dal vivo
Prima di aggiornarsi, Bruno modifica la stessa riga con un altro valore e committa localmente (tune: soglia 0.65). Poi fa git pull --no-rebase (la fusione classica):
git pull --no-rebase
# Auto-merging cfg.py
# CONFLICT (content): Merge conflict in cfg.py
# Automatic merge failed; fix conflicts and then commit the result.
git status --short
# UU cfg.py
cat cfg.py
# <<<<<<< HEAD
# AUC_MIN = 0.65
# =======
# AUC_MIN = 0.72
# >>>>>>> 824c4ca9b641f60c30a74fb53876179e8013b3a4
Git non sa quale delle due soglie sia quella giusta: lo dice. Lo stato mostra UU cfg.py, "modificato da entrambi" (unmerged). Sopra la riga ======= c'è la tua versione (HEAD), sotto quella in arrivo. Risolvere ha quattro mosse in ordine. Prima decidi qual è il valore giusto: non è una scelta tecnica di Git, è una scelta di progetto, e qui bisogna parlare con Anna (perché 0.72? perché 0.65?), perché una soglia scelta a casaccio per chiudere un conflitto è un errore di battitura nel posto peggiore. Poi modifichi il file, lasciando solo la riga finale e cancellando le tre righe di marcatori. Con git add cfg.py dichiari "ho risolto", e con git commit completi la fusione.
Se invece vuoi tornare al punto di prima, git merge --abort annulla il tentativo e ti riporta come prima del pull. Dopo averlo fatto, git push viene rifiutato:
git merge --abort
git status -sb
# ## main...origin/main [ahead 1, behind 1]
git push
# ! [rejected] main -> main (non-fast-forward)
# hint: Updates were rejected because the tip of your current branch is behind
# hint: its remote counterpart. If you want to integrate the remote changes,
# hint: use 'git pull' before pushing again.
Il remoto ha un commit (quello di Anna) che il branch di Bruno non ha: se Git accettasse il push, cancellerebbe il lavoro di Anna. Il rifiuto è una protezione, non un errore da aggirare con --force. La regola: se un push è rifiutato, fai fetch, guarda cosa manca, integra, risolvi i conflitti, poi riprova. Qui Bruno ripete il pull, sceglie 0.70 (un valore deciso con Anna, per l'esempio), e chiude la fusione:
git pull --no-rebase # di nuovo il conflitto; risolvo cfg.py a mano
git add cfg.py
git commit --no-edit
# [main dadbdb8] Merge branch 'main' of /percorso/remoto
git log --oneline --graph
# * dadbdb8 Merge branch 'main' of /percorso/remoto
# |\
# | * 824c4ca tune: soglia 0.72
# * | 13a9756 tune: soglia 0.65
# |/
# * 0abce59 feat: soglia
git push
# 824c4ca..dadbdb8 main -> main
Il grafico mostra le due linee che si riuniscono nel commit di fusione, e il push ora è accettato perché il remoto può avanzare da 824c4ca a dadbdb8 senza perdere nulla.
L'accesso a GitHub: credenziali senza segreti in giro
Per fare push su GitHub devi dimostrare chi sei, e le strade comuni sono due. Con SSH generi una coppia di chiavi (ssh-keygen -t ed25519): la parte pubblica la carichi su GitHub (nelle impostazioni dell'account), quella privata resta sul tuo computer, protetta da una passphrase, e l'indirizzo del remoto diventa git@github.com:utente/progetto.git; è come una serratura (pubblica, la dai a tutti) e una chiave (privata, solo tua). Con HTTPS e token al posto della password usi un personal access token, una stringa con permessi limitati e scadenza che crei dalle impostazioni; non incollarlo mai in un file del progetto (vedi variabili d'ambiente e segreti) né nei messaggi di commit. In entrambi i casi la credenziale è un segreto: se la pubblichi per errore, non basta cancellarla dal repository, va revocata dal pannello di GitHub e sostituita. Il funzionamento di SSH su GitHub è nella documentazione ufficiale; non ho provato l'autenticazione verso GitHub in questo lavoro.
Come scrivere una pull request che si legge in due minuti
Un modello utile, adattato al machine learning, che puoi copiare nella descrizione di ogni PR. I numeri dell'esempio sono quelli misurati nella lezione su come si testa un modello.
## Cosa cambia
Porta la soglia AUC minima da 0.60 a 0.70 nel test di regressione del modello.
## Perché
Un modello con la sola feature tenure ha AUC 0.643 e passava il controllo a 0.60; il minimo
osservato per il modello sano su 10 seed di split è 0.737, quindi 0.70 li separa.
## Come l'ho verificato
- uv run pytest -q -> 6 passed
- Rotto di proposito il modello (una feature in meno): il test diventa rosso.
## Cosa guardare con attenzione
La scelta del margine (sezione "soglie" del test).
Struttura: cosa (una frase), perché (con i numeri), verifica (comandi e risultati che il revisore può rifare), punto di attenzione (dove vuoi che guardi di più). Chi legge capisce cosa deve decidere senza leggere tutto il diff. Una PR senza queste quattro cose costringe il revisore a indovinarle.
Chi revisiona cosa, in un progetto ML
La revisione di un progetto ML guarda quattro cose diverse. Il codice: chiarezza, test presenti, nessun segreto, nessun file grande. I numeri: la metrica citata è misurata su un insieme di valutazione separato dall'addestramento? Con quale seed? Il miglioramento supera la variabilità tra split? I dati: la modifica cambia i dati in ingresso o la loro preparazione, e cosa succede ai valori mancanti? L'impatto: cambia il formato dell'output del modello, e quindi cosa si aspetta chi lo consuma? Le prime due domande sono quelle che un revisore di software ordinario non pensa di fare, ed è per questo che conviene scriverle nel modello di descrizione.
Il perché: quattro modi in cui il lavoro in due si rompe
Collaborare sullo stesso codice sembra un problema di organizzazione ("chi fa cosa"), ma è prima di tutto un problema tecnico: due versioni dello stesso insieme di file nate da un punto comune e divergenti. Gli errori seguono quattro schemi, e ogni funzione di GitHub ne corregge uno. Il primo è la sovrascrittura silenziosa: due persone modificano lo stesso file e la seconda che salva cancella il lavoro della prima; Git lo impedisce (il push rifiutato visto sopra, il conflitto segnalato), ma solo se il lavoro passa da Git, mentre un file su un disco condiviso o inviato per posta torna al caso peggiore. Il secondo è la modifica non rivista: un errore (una soglia sbagliata, una colonna dei dati interpretata male) entra nel codice perché l'unica persona che l'ha guardato è chi l'ha scritto, e la pull request introduce un secondo paio d'occhi prima dell'integrazione, quando correggere costa un commento e non un incidente.
Il terzo schema è il branch di lunga durata: più un ramo vive isolato, più diverge da main, e più è doloroso riunirlo, perché i conflitti crescono e, peggio, le assunzioni cambiano senza che nessuno se ne accorga; la cura è organizzativa, non tecnica, e si chiama rami brevi, integrati spesso. Il quarto è la regressione dopo l'integrazione: il codice di Anna funziona, quello di Bruno funziona, la somma no, e solo un controllo automatico eseguito sul risultato della fusione (la CI) lo vede prima del rilascio.
Le tre strategie di merge, e cosa fanno alla storia
Quando una pull request viene approvata, GitHub offre di solito tre modi per integrarla (documentazione sulle pull request). La scelta cambia la forma della storia, quindi ciò che git log e git bisect ti mostreranno in futuro.
| Strategia | Cosa produce | Pro | Contro |
|---|---|---|---|
| Merge commit | Un commit di fusione con due genitori; i commit del branch restano com'erano | Storia fedele a ciò che è successo; nessun commit riscritto | Storia a grafo, più rumorosa; commit "wip" del branch restano |
| Squash and merge | Tutti i commit del branch diventano un solo commit su main | Storia lineare e pulita; un commit = una funzionalità | Si perde il dettaglio dei passaggi; bisect lavora a grana più grossa |
| Rebase and merge | I commit del branch vengono riscritti uno dopo l'altro in cima a main (hash nuovi) | Storia lineare con il dettaglio dei commit | Gli hash cambiano: chi aveva il branch deve riallinearsi |
Una scelta ragionevole per un progetto ML con commit piccoli e ben scritti è rebase and merge o merge commit, perché bisect funziona meglio con commit piccoli e autonomi. Lo squash conviene quando i commit del branch sono disordinati (molti "wip") e la PR è una sola unità logica.
Il rebase, visto da vicino
Il rebase merita una dimostrazione, perché è il comando che i principianti temono di più e che chiarisce meglio come funziona la storia. Nel repository di prova, Anna ha pubblicato un nuovo commit (feat: extra di anna) mentre Bruno ne aveva uno solo suo, non ancora pubblicato (docs: note di bruno, hash dc98de9). Bruno aggiorna con git pull --rebase:
git pull --rebase
# dadbdb8..46f4284 main -> origin/main
# Rebasing (1/1)Successfully rebased and updated refs/heads/main.
git log --oneline --graph
# * 189d0f8 docs: note di bruno
# * 46f4284 feat: extra di anna
# * dadbdb8 Merge branch 'main' of /percorso/remoto
# |\
# | * 824c4ca tune: soglia 0.72
# * | 13a9756 tune: soglia 0.65
# |/
# * 0abce59 feat: soglia
Invece di creare un commit di fusione, Git ha ripreso il commit di Bruno e lo ha rimesso in cima a quello di Anna, come se Bruno avesse iniziato a lavorare dopo di lei. Dopo il commit di fusione, la storia prosegue in linea retta. Il commit di Bruno ha un hash nuovo: era dc98de9, ora è 189d0f8, perché il suo genitore è cambiato, e il vecchio non esiste più nel suo ramo. È il motivo della regola d'oro: mai fare rebase di commit già pubblicati e usati da altri, perché cambiarne l'hash costringe tutti a riallinearsi. Per i commit locali, non ancora condivisi, è sicuro e produce una storia più leggibile. Se il rebase incontra un conflitto, si ferma e lo segnala: si risolve come descritto (modifica, git add) e si prosegue con git rebase --continue, oppure ci si ferma con git rebase --abort; la sintassi è in git-rebase, e il caso del conflitto durante il rebase non l'ho provato.
Configurare un branch protetto, con criterio
Le opzioni disponibili nelle impostazioni di un repository GitHub coprono quasi sempre questi controlli (nomi e posizioni possono variare):
- Richiedere una pull request prima di fare merge: impedisce il push diretto su
main. - Richiedere un numero di approvazioni (una è sufficiente per un piccolo team). Se l'autore è anche chi approva, il controllo non ha senso: la piattaforma può vietarlo.
- Richiedere che i controlli automatici passino, scegliendo quali (per esempio il job di test).
- Richiedere che il branch sia aggiornato rispetto a
mainprima del merge: i test vengono eseguiti sul risultato reale, non su uno stato già superato. - Vietare il push forzato e la cancellazione del branch protetto.
- CODEOWNERS: un file che indica chi deve approvare le modifiche a certe cartelle (per esempio, un esperto di dati per
data/). La piattaforma chiede automaticamente la loro revisione, come descritto in about code owners.
Le prime quattro voci agiscono sul momento del merge, le ultime due sulla storia e sulle responsabilità; tutte hanno un costo, perché rallentano ogni PR. Il criterio generale è introdurre una regola quando previene un errore che ti è già costato: cominciare con due regole (PR richiesta, test verdi) è più sostenibile che partire con sei e finire per aggirarle.
Dare e ricevere una revisione
La revisione è il momento più prezioso e più sprecato di una PR. Da revisore leggi prima la descrizione e poi il diff: se la descrizione non basta a capire cosa viene chiesto, il primo commento è su quella. Distingui i commenti bloccanti ("questo test non verifica nulla: va corretto") dai suggerimenti ("preferirei un nome più chiaro, non è obbligatorio"), perché un revisore che rende tutto bloccante rallenta tutti. Spiega il perché, non solo il cosa: "qui la soglia è sotto la variabilità osservata" insegna, "togli" no. Quando la modifica è delicata esegui il codice: gh pr checkout NUMERO porta il branch della PR sul tuo computer (comando dalla documentazione, non provato qui) e puoi rilanciare i test. Guarda anche ciò che manca, un test, un aggiornamento della documentazione, una migrazione, perché il diff mostra ciò che c'è e l'assenza si vede solo ripensando alla modifica.
Da autore rileggi per primo il tuo diff nell'interfaccia: vedi errori che nell'editor non vedi, perché guardi come lo vedrà un altro. Tieni la PR piccola, perché una di cinquanta righe riceve una revisione vera e una da cinquecento riceve un "sembra ok" (è un'osservazione di esperienza, non una misura). Rispondi a ogni commento, anche solo con "fatto", e se non sei d'accordo dillo con un argomento, non con il silenzio. E non prendere i commenti sul codice come giudizi su di te: l'utilità della revisione dipende dalla franchezza.
Flusso breve o flusso lungo: due modelli di organizzazione
Due filosofie convivono. Nel trunk-based development tutti integrano spesso (anche più volte al giorno) in un ramo principale, con rami molto brevi o nessuno; è ciò che fa questo progetto, con un unico sviluppatore che committa su master. In modelli come GitFlow esistono rami di lunga durata (develop, release/*, hotfix/*) con regole di promozione fra l'uno e l'altro, adatti a software con versioni rilasciate separatamente. Per un progetto ML tipico (un modello, un servizio, un team piccolo) il trunk-based con PR brevi è quasi sempre sufficiente e molto meno faticoso: i rami di lunga durata moltiplicano i conflitti proprio dove i dati e i parametri cambiano spesso. L'eccezione sono gli esperimenti lunghi di ricerca: lì si accetta un ramo isolato, a patto di riallinearlo a main con regolarità (git merge main dentro il ramo, per non accumulare divergenza).
Le issue come memoria del progetto
Una issue è una scheda: un errore, una richiesta, una domanda, con una discussione. Il suo valore sta nel collegamento con il codice: scrivere Fixes #12 nella descrizione di una PR fa sì che la issue 12 si chiuda automaticamente quando la PR viene integrata nel branch predefinito del repository, lasciando la traccia "questo cambiamento ha risolto quel problema" (la regola è nella documentazione sul collegare PR e issue). Per un modello è una forma di documentazione economica: "perché la soglia è 0.65?" ha una risposta cercando la issue che ne ha parlato. Non servono strumenti di gestione di progetto complessi per cominciare: un elenco di issue ben scritte e un'etichetta per lo stato bastano.
Una settimana in un piccolo team (scenario illustrativo)
Per fissare il flusso, ecco una settimana ipotetica di tre persone sul progetto churn (è uno scenario costruito per spiegare, non una cronaca):
- Lunedì. Una persona apre una issue: "Il modello sottostima i clienti senza reclami". Un'altra la prende in carico e crea il branch
fix/clienti-senza-reclami. - Martedì. Tre commit piccoli: un test che riproduce il problema (rosso), la correzione, un aggiornamento della soglia. Push e apertura della PR con il modello di descrizione visto sopra.
- Mercoledì. La CI segna rosso: un test di determinismo fallisce. Il revisore trova il motivo (un seed mancante in una funzione nuova); l'autore lo aggiunge con un nuovo commit; la PR si aggiorna da sola.
- Giovedì. Approvazione, merge, il branch viene cancellato. Una issue si chiude in automatico.
- Venerdì. Il terzo membro, che lavorava su un altro branch, fa
git pull --rebasesumainper riallinearsi e prosegue.
Niente di questo richiede strumenti sofisticati: è la combinazione di branch brevi, PR piccole, test automatici e descrizioni chiare. L'ordine dei giorni è solo illustrativo, ma la sequenza no: il test rosso precede la correzione, la CI precede l'approvazione, il merge chiude la issue.
Prova tu: quattro esercizi guidati
Gli esercizi si fanno in locale, senza account GitHub. Ogni esercizio dice cosa fare, cosa dovresti vedere (i risultati qui sono quelli della mia esecuzione con Git 2.53) e cosa ti insegna.
Esercizio 1: un remoto in una cartella
Crea un repository --bare e due cloni (anna e bruno) come nella lezione. In anna crea cfg.py con AUC_MIN = 0.70, committa e fai git push -u origin main. Poi git clone di nuovo come bruno. Dovresti vedere: nel push, [new branch] main -> main; in bruno, git log --oneline con il commit di Anna. Cosa impari: che un remoto è solo un altro repository, e che origin è il suo nome locale.
Esercizio 2: essere "indietro" senza accorgertene
In anna cambia il valore in 0.72, committa e fai push. In bruno esegui git fetch e poi git status -sb. Dovresti vedere ## main...origin/main [behind 1]. Prova poi git log HEAD..origin/main --oneline. Cosa impari: la differenza tra sapere che il remoto è avanti (fetch) e integrare (pull), e che status ti dice quanti commit hai di vantaggio o di ritardo.
Esercizio 3: provocare e risolvere un conflitto
Prima di aggiornare, in bruno cambia la stessa riga in 0.65 e committa. Poi git pull --no-rebase. Dovresti vedere CONFLICT (content): Merge conflict in cfg.py e, con git status --short, UU cfg.py. Apri il file: ci sono i marcatori <<<<<<< HEAD, ======= e >>>>>>>. Scegli un valore, cancella i marcatori, poi git add cfg.py e git commit. Controlla con git log --oneline --graph che esista un commit di fusione. Cosa impari: che un conflitto non è un errore ma una domanda a cui Git non sa rispondere; e che la risposta è una decisione di progetto, non di sintassi. Variante: ripeti con git merge --abort al posto della risoluzione, e osserva che il repository torna com'era.
Esercizio 4: la stessa situazione con il rebase
Ripristina i due cloni allo stesso punto. Fai fare ad anna un commit su un file nuovo (extra.cfg) e fai push; in bruno committa un file diverso (note.cfg) senza pubblicarlo. Poi, in bruno: git pull --rebase. Dovresti vedere Successfully rebased and updated refs/heads/main e un grafico lineare, con il commit di Bruno in cima a quello di Anna e un hash diverso da prima (con git log --oneline prima e dopo). Cosa impari: che il rebase riscrive i tuoi commit locali; ed è per questo che non lo si fa su commit già pubblicati.
Se fai solo questi quattro esercizi, hai già incontrato tutte le situazioni che compaiono in un lavoro di squadra reale: aggiornamento, ritardo, conflitto e storia lineare.
Cosa sai fare adesso
Sai distinguere Git, che è locale, da GitHub, che è un remoto con strumenti di collaborazione sopra, e fetch, che guarda, da pull, che guarda e integra. Sai scrivere una pull request che risponde a cosa cambia, perché e come l'hai verificato, con metrica, dataset e seed quando il modello è coinvolto. Sai riconoscere e risolvere un conflitto, capire perché un push viene rifiutato e scegliere tra merge, squash e rebase. E sai che il flusso giusto dipende da quante persone lavorano al progetto, non da una moda. Prossima lezione: open source, licenze e come contribuire.
Nella stessa serie trovi anche ML-Ops: dal notebook al servizio, ML-Ops: registry di container, tag, digest e versioni che non cambiano sotto i piedi e ML-Ops: il deploy, dove gira il modello e come scegliere.
Fonti
- Documentazione GitHub sulle pull request - le strategie di merge (merge commit, squash, rebase), consultata il 2026-11-12
- GitHub: informazioni sui branch protetti - le regole di protezione elencate, consultata il 2026-11-12
- GitHub: file di grandi dimensioni - il blocco dei file oltre 100 MiB (letta la frase), consultata il 2026-11-12
- GitHub: collegare una pull request a una issue - la chiusura automatica nel branch predefinito, consultata il 2026-11-12
- Manuale di GitHub CLI: gh pr create - la sintassi del comando (non eseguito), consultata il 2026-11-12
- git-fetch, git-pull e git-rebase - il comportamento dei comandi della sessione, consultata il 2026-11-12
Lo scenario con repository bare, Anna e Bruno è stato eseguito due volte con Git 2.53.0 (LC_ALL=C, date e identità fissate) e le uscite, hash compresi, sono identiche; il percorso del remoto è sostituito da /percorso. Non sono stati eseguiti: i comandi verso GitHub (git clone https://github.com/..., gh pr create, gh pr checkout), il fork con upstream e il conflitto durante un rebase. I nomi Anna e Bruno e lo scenario della settimana sono inventati.
Commenti
Caricamento commenti...
Accedi per commentare