SCM

Le soluzioni di Software Configuration Management nascono da problemi complessi purtroppo molto comuni nel mondo dello sviluppo software, come:

  • pubblicare un hotfix su una versione precedente a quella in cui si sta sviluppando. Può essere difficile localizzare le versioni vecchie, modificarle e rimappare le modifiche sulle versioni nuove;
  • condividere lavori con altri gestendo accessi contemporanei e conflitti;
  • stabilire la responsabilità di ciascuna linea di codice.

Il Software Configuration Management è l’insieme di pratiche che hanno l’obiettivo di rendere sistematico il processo di sviluppo, tenendo traccia dei cambiamenti in modo che il prodotto sia in ogni instante in uno stato (configurazione) ben definito.

Storia

Il Configuration Management negli anni ‘50 nell’ambito dell’industria aerospaziale. Alla fine degli anni ‘70 inizia ad essere applicato all’ingegneria del software.

Gli oggetti di cui si controlla l’evoluzione sono detti configuration item o artifact (manufatti, solitamente file). Dunque l’SMC ci permette di controllare le revisioni degli artifact e il risultato di tali revisioni,questo processo è molto utile per la generazione di un prodotto a partire da una configurazione ben determinata.

Manufatti

Gli “oggetti” di cui si controlla l’evoluzione sono detti configuration item o manufatti; generalmente sono file. Se si cambia nome a un file è come eliminarne uno e partire da zero con uno nuovo. Originariamente i tool tracciavano i file indipendentemente, senza un senso logico (una configurazione) comune.

  • anni ‘80: strumenti locali (SCCS, …)
  • anni ‘90: strumenti client-server centralizzati (CVS, subversion, …)
  • anni ‘00: strumenti distribuiti peer-to-peer (git, mercurial, bazaar, …)

Git nasce da un’esigenza di Linus Torvalds con il kernel Linux.

Centralizzato vs decentralizzato

Il mondo open source preferisce un approccio decentralizzato al version control. Perché?

  • è possibile lavorare offline;
  • è molto più veloce, perché la rete non fa più da bottleneck;
  • supporta diversi modi di lavorare:
    • simil centralizzato: un repository viene considerato “di riferimento”;
    • due peer che collaborano direttamente;
    • gerarchico a più livelli (kernel Linux).

Non c’è sincronizzazione automatica, ma ci sono comandi espliciti per fare merge tra repository remote. In Git, per via della sua struttura modulare, è possibile utilizzare il proprio algoritmo merge rispetto a quelli già inclusi.

Cosa tracciare?

Qualunque sistema si usi, occorre prendere decisioni importanti che influenzano la replicabilità della produzione.

  • Si traccia l’evoluzione anche di componenti fuori dal nostro controllo (librerie, compilatori)?
  • Si archiviano i file che sostitusicono il prodotto (eseguibile, ecc…)?

La risposta ad entrambe queste domande è no, perché è scomodo, anti economico, costoso… Questo porta però problemi, perché non c’è perfetta replicabilità.

Meccanismo di base

Ogni cambiamento è regolato da:

  • check-out: dichiara la volontà di lavorare partendo da una particolare revisione di un manufatto (o di una configurazione di diversi manufatti);
  • check-in (o commit): dichiara la volonta di registrarne una nuova (spesso chiamata change-set).

Queste operazioni vengono attivate rispetto a un repository. Scambio di dati tra il repository (che contiene tutte le configurazioni) e il workspace (l’ambiente in cui si trova nel filesystem).

Solitamente ho un repository e \(n\) workspace, uno per ogni ambiente dove sto lavorando.

Repository

La repository mantiene:

  • date
  • etichette
  • versioni
  • diramazioni (branches)
  • ecc…

Per risparmiare spazio, le repository salvano solo le differenze tra una versione e l’altra. In realtà, Git non fa così, perché usa link simbolici: fare il checkout di una specifica versione è instantaneo.

Le repository possono essere centralizzate o distribuite.

Nei sistemi di versioning distribuiti c’è il concetto di hashing, in modo da identificare file uguali anche se in posizioni diverse. Per confrontare storie diverse si utilizzano gli hash dei file e delle directory.

Accesso concorrente

Quando il repository è condiviso da un gruppo di lavoro, nasce il problema di gestirne l’accesso concorrente. Esistono due modelli:

  • modello ‘pessimistico’ (RCS): prevedo il possibile conflitto assicurandomi che chi lavora sia l’unico con l’accesso in scrittura. Funziona solo in ambienti centralizzati, nell’open source non può funzionare.
  • modello ‘ottimistico’ (CVS): il sistema si disinteressa del problema e fornisce supporto per le attività di merge di change-set paralleli potenzialmente conflittuali.

Il modello ottimistico può essere regolato con i branch: l’attività di merge è quindi fondamentale. CVS/Subversion scoraggiava i branch, Git li rende facili e li incoraggia. In Git, l’uso dei branch è talmente comune che a volte è necessario introdurre delle politiche (come GitFlow) sul loro utilizzo.

Git

In figura, possiamo osservare 4 livelli ordinati:

  • working directory (WD): rappresenta la configurazione della directory di lavoro sul filesystem - esiste indipendentemente da Git. Può essere vista come l’unione dei tracked and untracked files;
  • index (o area di staging): insieme dei tracked files da Git.
  • (\(n\)?) local repository: insieme delle modifiche committate e relativo storico.
  • (\(n\)) remote repository: branch remoto; è possibile avere sia più branch per progetto remoto che più progetti remoti configurati.

Il termine repository è abbastanza misleading, perché è comunemente associato ad un progetto mentre in questa astrazione a livelli corrisponde di fatto a un branch.

Il passaggio tra un livello e l’altro non è mai automatico, ma è sempre esplicitato da un’operazione.

Operazioni di base

È consigliata la lettura di Git Cheatsheet.

Per ogni branch c’è un puntatore all’ultimo commit di tale branch. L’HEAD punta all’ultimo commit in cui siamo: normalmente corrisponde al puntatore del branch corrente; quando non è così siamo in una situazione di HEAD scollegato. È utile potersi spostare tra i commit per controllare revisioni precedenti, ma in caso di nuovi commit è importante creare un nuovo branch per poterci riferire ad esse.

git commit - record changes to the repository

Il comando git commit ci permette di salvare del contenuto dall’index al branch locale.

Dopo aver creato il commit, l’HEAD e il puntatore al branch corrente puntano al nuovo commit. Anche il contenuto dell’index equivale al contenuto del commit.

--amend

Con l’opzione --amend è possibile rimpiazziare facilmente l’ultimo commit con uno nuovo.

git switch - switch branches

Il comando git switch ha un sottoinsieme delle funzionalità del comando git checkout ed è più semplice da utilizzare.

Permette di passare a un nuovo branch semplicemente modificando l’HEAD (e di conseguenza il contenuto dell’index e dei file).

git merge - join two or more development histories together

Il comando git merge è utile per unire branch (o più in generale alberi) insieme.

Se i due branch non sono divergenti, il merge avviene in modo banale con un fast-forward: nessun ulteriore commit verrà cambiato, verrà solo modificato il puntatore del branch e l’HEAD. Per forzare la creazione di un merge di commit (in GitFlow è apprezzato) occorre utilizzare l’opzione --no-ff.

In tutti gli altri casi, il merge può concludersi con successo oppure possono avvenire conflitti. Per risolverli, Git ci proporrà un’interfaccia simile alla seguente.

<<<<<<< yours:sample.txt
Conflict resolution is hard;
let's go shopping.
=======
Git makes conflict resolution easy.
>>>>>>> theirs:sample.txt

Una volta risolti tutti i conflitti è sufficiente commitare le modifiche concludendo quindi il merge.

git reset - reset current HEAD to the specified state

Il comando git reset reimposta il contenuto dei file nell’index (e, opzionalmente con l’opzione --hard nella WD) all’ultimo commit puntato da HEAD o ad un altro commit.