Capitolo 3. Una panoramica di Mercurial: le unioni

Indice

Unire flussi di lavoro
Le teste di un repository
Effettuare l’unione
Inserire i risultati dell’unione nel repository
Risolvere i conflitti tra cambiamenti
Usare uno strumento grafico di unione
Un esempio risolto
Semplificare la sequenza di estrazione-unione-inserimento
Rinominare, copiare e unire

Finora abbiamo parlato di come clonare un repository, effettuare modifiche in un repository ed estrarre o trasmettere cambiamenti da un repository all’altro. Il nostro prossimo passo sarà quello di unire le modifiche provenienti da repository separati.

Unire flussi di lavoro

Le unioni giocano un ruolo fondamentale quando si lavora con uno strumento di controllo di revisione distribuito. Ecco alcuni casi in cui può presentarsi il bisogno di unire tra loro due o più flussi di lavoro.

  • Alice e Bruno hanno entrambi una copia personale di un repository per un progetto su cui stanno collaborando. Mentre Alice corregge un bug nel suo repository, Bruno aggiunge una nuova funzione nel proprio. Entrambi vogliono che il repository condiviso contenga sia la correzione del bug sia la nuova funzione.

  • Cinzia lavora frequentemente su più attività differenti alla volta per un singolo progetto, ognuna isolata al sicuro nel proprio repository. Lavorando in questo modo, Cinzia ha spesso bisogno di unire una parte del proprio lavoro con un’altra.

Dato che abbiamo spesso bisogno di eseguire unioni, Mercurial ci viene in aiuto agevolando questo processo. Per capire come funzionano le unioni, seguiamone una passo per passo. Cominceremo creando ancora un altro clone del nostro repository (vedete quante volte spuntano fuori?) apportandovi poi alcune modifiche.

$ cd ..
$ hg clone hello mio-nuovo-hello
aggiorno la directory di lavoro
2 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd mio-nuovo-hello
# Apportiamo alcune semplici modifiche a hello.c...
$ mio-editor-di-testo hello.c
$ hg commit -m "Un nuovo saluto per un nuovo giorno."

Ora abbiamo due copie di hello.c con contenuti differenti e le cronologie dei due repository divergono l’una dall’altra, come illustrato nella Figura 3.1, «Le cronologie divergenti dei repository mio-hello e mio-nuovo-hello». Ecco la copia del nostro file presente nel primo repository.

$ cat hello.c
/*
 * Rilasciato al pubblico dominio da Bryan O'Sullivan. Questo
 * programma non è protetto da brevetti negli Stati Uniti o in
 * altri paesi.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("ancora una volta, ciao.\n");
	printf("ciao, mondo!\");
	printf("ancora ciao!\n");
	return 0;
}

Ed ecco la versione leggermente differente contenuta nell’altro repository.

$ cat ../mio-hello/hello.c
/*
 * Rilasciato al pubblico dominio da Bryan O'Sullivan. Questo
 * programma non è protetto da brevetti negli Stati Uniti o in
 * altri paesi.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("ciao, mondo!\");
	printf("ancora ciao!\n");
	return 0;
}

Figura 3.1. Le cronologie divergenti dei repository mio-hello e mio-nuovo-hello

XXX add text

Sappiamo già che l’estrazione dei cambiamenti dal nostro repository mio-hello non avrà alcun effetto sulla directory di lavoro.

$ hg pull ../mio-hello
estraggo da ../mio-hello
cerco i cambiamenti
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 1 changeset con 1 cambiamenti a 1 file (+1 teste)
(eseguite 'hg heads' per vedere le teste, 'hg merge' per unire)

Tuttavia, il comando hg pull dice qualcosa a proposito di «teste» (in inglese, heads).

Le teste di un repository

Come ricorderete, Mercurial memorizza l’identità del genitore di ogni changeset. Se un cambiamento possiede un genitore, lo chiamiamo figlio o discendente del genitore. Una testa è un changeset che non ha figli. Quindi, la revisione di punta è una testa, perché la revisione più recente in un repository non possiede alcun figlio. Ci sono occasioni in cui un repository può contenere più di una testa.

Figura 3.2. I contenuti del repository dopo aver propagato i cambiamenti da mio-hello a mio-nuovo-hello

XXX add text

Dalla Figura 3.2, «I contenuti del repository dopo aver propagato i cambiamenti da mio-hello a mio-nuovo-hello», potete vedere l’effetto della propagazione dei cambiamenti dal repository mio-hello al repository mio-nuovo-hello. La cronologia già presente in mio-nuovo-hello non viene toccata, ma al repository è stata aggiunta una nuova revisione. Riferendoci alla Figura 3.1, «Le cronologie divergenti dei repository mio-hello e mio-nuovo-hello», possiamo vedere che nel nuovo repository l’identificatore di changeset rimane lo stesso, ma il numero di revisione è cambiato. (Questo, incidentalmente, è un buon esempio del perché sia inopportuno usare i numeri di revisione per discutere i changeset.) Possiamo vedere le teste di un repository utilizzando il comando hg heads.

$ hg heads
changeset:   6:764347e47e75
etichetta:   tip
genitore:    4:2278160e78d4
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:51:52 2009 +0000
sommario:    Inserisce una riga con un messaggio aggiuntivo.

changeset:   5:1c343117a2e3
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:52:03 2009 +0000
sommario:    Un nuovo saluto per un nuovo giorno.

Effettuare l’unione

Cosa succede se proviamo a usare normalmente il comando hg update per aggiornare la directory di lavoro alla nuova punta?

$ hg update
fallimento: attraversa i rami (usate 'hg merge' o 'hg update -C')

Mercurial ci sta dicendo che hg update non è in grado di effettuare un’unione. Il comando non aggiornerà la directory di lavoro se pensa che potremmo voler eseguire un’unione, a meno che non gli chiediamo esplicitamente di farlo. (Incidentalmente, forzare l’aggiornamento tramite hg update -C provocherebbe la perdita di tutte le modifiche contenute nella directory di lavoro e non ancora inserite nel repository.)

Per avviare un’unione tra le due teste, usiamo il comando hg merge.

$ hg merge
unisco hello.c
0 file aggiornati, 1 file uniti, 0 file rimossi, 0 file irrisolti
(unione tra rami, ricordatevi di eseguire il commit)

Il contenuto del file hello.c viene sistemato. L’operazione aggiorna la directory di lavoro in modo che contenga le modifiche provenienti da entrambe le teste, cosa che si riflette sia nell’elenco visualizzato dal comando hg parents sia nei contenuti del file hello.c.

$ hg parents
changeset:   5:1c343117a2e3
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:52:03 2009 +0000
sommario:    Un nuovo saluto per un nuovo giorno.

changeset:   6:764347e47e75
etichetta:   tip
genitore:    4:2278160e78d4
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:51:52 2009 +0000
sommario:    Inserisce una riga con un messaggio aggiuntivo.

$ cat hello.c
/*
 * Rilasciato al pubblico dominio da Bryan O'Sullivan. Questo
 * programma non è protetto da brevetti negli Stati Uniti o in
 * altri paesi.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("ancora una volta, ciao.\n");
	printf("ciao, mondo!\");
	printf("ancora ciao!\n");
	return 0;
}

Inserire i risultati dell’unione nel repository

Ogni volta che eseguiamo un’unione, il comando hg parents mostrerà due genitori fino a quando non invocheremo hg commit per inserire i risultati dell’unione nel repository.

$ hg commit -m "Unisce i cambiamenti."

Ora abbiamo una nuova revisione di punta che, come potete notare, possiede entrambe le nostre vecchie teste come genitori. Queste sono le stesse revisioni che erano state precedentemente mostrate da hg parents.

$ hg tip
changeset:   7:e300c3dcceca
etichetta:   tip
genitore:    5:1c343117a2e3
genitore:    6:764347e47e75
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:52:07 2009 +0000
sommario:    Unisce i cambiamenti.

Nella Figura 3.3, «Lo stato della directory di lavoro e del repository durante l’unione e dopo l’inserimento», potete vedere una rappresentazione di quanto accade alla directory di lavoro durante l’unione e di come questo abbia effetto sul repository quando avviene l’inserimento. Durante l’unione, la directory di lavoro possiede due changeset genitori che poi diventano i genitori del nuovo changeset.

Figura 3.3. Lo stato della directory di lavoro e del repository durante l’unione e dopo l’inserimento

XXX add text

Talvolta si dice che un’unione è composta da due parti: la parte sinistra è costituita dal primo genitore elencato da hg parents e la parte destra dal secondo. Se per esempio la directory di lavoro si fosse trovata alla revisione 5 prima che cominciassimo l’unione, quella revisione sarebbe diventata la parte sinistra dell’unione.

Risolvere i conflitti tra cambiamenti

La maggior parte delle unioni è semplice, ma a volte vi troverete a unire cambiamenti in cui ognuna delle due parti modifica le stesse porzioni degli stessi file. A meno che entrambe le modifiche siano identiche, questa situazione risulterà in un conflitto che dovrete cercare di riconciliare decidendo come combinare i diversi cambiamenti in un risultato coerente.

Figura 3.4. Modifiche in conflitto a un documento

XXX add text

La Figura 3.4, «Modifiche in conflitto a un documento» illustra un esempio di due diversi cambiamenti a uno stesso documento che sono in conflitto tra loro. Siamo partiti da una singola versione del file, a cui poi abbiamo apportato alcune modifiche mentre qualcun altro faceva modifiche differenti allo stesso testo. Nella risoluzione del conflitto tra i cambiamenti, il nostro compito è quello di decidere che cosa dovrebbe contenere il file.

Mercurial non è dotato di alcuna funzione predefinita per gestire i conflitti. Invece, richiama un programma esterno, di solito uno che mostra un qualche tipo di interfaccia grafica per la risoluzione dei conflitti, trovato tra i tanti strumenti di unione differenti che è probabile siano installati sul vostro sistema. Come prima cosa, Mercurial prova a cercare alcuni strumenti di unione completamente automatici; poi, se non riesce a trovarli (perché il processo di risoluzione richiede una guida umana) o se non sono presenti, prova a richiamare diversi strumenti grafici di unione.

È anche possibile far eseguire a Mercurial uno specifico programma, impostando il valore della variable d’ambiente HGMERGE al nome del vostro programma preferito.

Usare uno strumento grafico di unione

Il mio strumento grafico preferito per gestire le unioni è kdiff3, che userò per descrivere le caratteristiche comuni a queste applicazioni. Potete vedere kdiff3 in azione nella Figura 3.5, «Usare kdiff3 per unire diverse versioni di un file». Il tipo di unione che sta eseguendo si chiama unione a tre vie, perché ci sono tre differenti versioni di un file che ci interessano. La porzione superiore della finestra è divisa in tre pannelli:

  • a sinistra troviamo la versione base del file, cioè la versione più recente da cui discendono le due versioni che stiamo cercando di unire;

  • nel mezzo troviamo la «nostra» versione del file, con i contenuti che abbiamo modificato;

  • a destra troviamo la «loro» versione del file, quella che proviene dal changeset che stiamo cercando di incorporare.

Il pannello sottostante contiene il risultato corrente dell’unione. Il nostro compito è quello di sostituire tutto il testo in rosso, che indica conflitti irrisolti, con una qualche combinazione ragionevole della «nostra» e della «loro» versione del file.

Tutti e quattro questi pannelli sono collegati tra loro: se ci spostiamo in verticale o in orizzontale in un pannello qualsiasi, gli altri vengono aggiornati per mostrare le sezioni corrispondenti dei rispettivi file.

Figura 3.5. Usare kdiff3 per unire diverse versioni di un file

XXX add text

Per ogni porzione conflittuale del file, possiamo scegliere di risolvere il conflitto usando una qualche combinazione di testo dalla versione base, dalla nostra, o dalla loro. Possiamo anche modificare manualmente il file risultante in ogni momento, nel caso avessimo bisogno di effettuare ulteriori cambiamenti.

Esistono molti strumenti per gestire l’unione di file, davvero troppi per elencarli qui. Si differenziano a seconda della piattaforma per cui sono disponibili e delle loro particolari forze e debolezze. La maggior parte è calibrata per unire file contenenti testo semplice, mentre alcuni sono orientati a particolari formati di file (generalmente XML).

Un esempio risolto

In questo esempio, riprodurremo la cronologia delle modifiche ai file della Figura 3.4, «Modifiche in conflitto a un documento» vista in precedenza. Per cominciare, creiamo un repository con una versione base del nostro documento.

$ cat > lettera.txt <<EOF
> Salve!
> Sono Mariam Abacha, moglie dell'ex
> dittatore nigeriano Sani Abacha.
> EOF
$ hg add lettera.txt
$ hg commit -m 'truffa 419, prima bozza'

Cloniamo il repository e apportiamo un cambiamento al file.

$ cd ..
$ hg clone truffa truffa-cugino
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd truffa-cugino
$ cat > lettera.txt <<EOF
> Salve!
> Sono Shehu Musa Abacha, cugino dell'ex
> dittatore nigeriano Sani Abacha.
> EOF
$ hg commit -m 'truffa 419, con cugino'

E ora aggiungiamo un altro clone, per simulare qualcun altro che effettui un cambiamento al file. (Questo suggerisce l’idea che non è affatto inusuale ritrovarsi a unire tra loro i propri repository contenenti attività isolate, risolvendo i conflitti incontrati nel corso di questo processo.)

$ cd ..
$ hg clone truffa truffa-figlio
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd truffa-figlio
$ cat > lettera.txt <<EOF
> Salve!
> Sono Alhaji Abba Abacha, figlio dell'ex
> dittatore nigeriano Sani Abacha.
> EOF
$ hg commit -m 'truffa 419, con figlio'

Avendo creato due versioni differenti del file, predisponiamo un ambiente adeguato a eseguire la nostra unione.

$ cd ..
$ hg clone truffa-cugino truffa-unione
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd truffa-unione
$ hg pull -u ../truffa-figlio
estraggo da ../truffa-figlio
cerco i cambiamenti
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 1 changeset con 1 cambiamenti a 1 file (+1 teste)
non aggiorno, sono state aggiunte nuove teste
(eseguite 'hg heads' per vedere le teste, 'hg merge' per unire)

In questo esempio, imposterò la variabile d’ambiente HGMERGE per dire a Mercurial di usare il comando non interattivo merge. Questo comando è incluso in molti sistemi di tipo Unix. (Se state seguendo questo esempio sul vostro computer, non preoccupatevi di impostare HGMERGE. Vi verrà presentata un’applicazione grafica da utilizzare per unire i file, che è un’alternativa di gran lunga preferibile.)

$ export HGMERGE=merge
$ hg merge
unisco lettera.txt
merge: attenzione: conflitti durante l'unione
unione di lettera.txt fallita!
0 file aggiornati, 0 file uniti, 0 file rimossi, 1 file irrisolti
usate 'hg resolve' per riprovare a unire i file irrisolti o 'hg up --clean' per abbandonare
$ cat lettera.txt
Salve!
<<<<<<< /tmp/panoramica-unioni-conflitto/truffa-unione/lettera.txt
Sono Shehu Musa Abacha, cugino dell'ex
=======
Sono Alhaji Abba Abacha, figlio dell'ex
>>>>>>> /tmp/lettera.txt~other.01poS4
dittatore nigeriano Sani Abacha.

Dato che merge non riesce a risolvere il conflitto tra i cambiamenti, inserisce alcuni marcatori di unione all’interno del file che esibisce i conflitti, per indicare quali righe sono in conflitto e se quelle righe provengono dalla nostra versione del file o dalla loro.

Dal modo in cui merge termina, Mercurial riconosce che non è stato in grado di operare con successo, quindi ci dice quali comandi abbiamo bisogno di eseguire se vogliamo rifare l’operazione di unione. Questo potrebbe essere utile se, per esempio, stessimo lavorando con un’applicazione grafica per la gestione delle unioni e la chiudessimo perché siamo confusi o realizziamo di aver commesso un errore.

Nel caso in cui l’unione automatica o manuale fallisca, nulla ci impedisce di «correggere» i file interessati per conto nostro, per poi inserire i risultati della nostra unione nel repository:

$ cat > lettera.txt <<EOF
> Salve!
> Sono Bryan O'Sullivan, nessuna parentela con l'ex
> dittatore nigeriano Sani Abacha.
> EOF
$ hg resolve -m lettera.txt
$ hg commit -m 'Mandatemi i vostri soldi'
$ hg tip
changeset:   3:df19ae0ef4d4
etichetta:   tip
genitore:    1:ae14113af250
genitore:    2:11ab3b2b6d1b
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:52:13 2009 +0000
sommario:    Mandatemi i vostri soldi

[Nota]Dov’è il comando hg resolve?

Il comando hg resolve è stato introdotto nella verisone 1.1 di Mercurial rilasciata nel dicembre 2008. Se state usando una versione più vecchia (eseguite hg version per controllare) questo comando non sarà presente. Nel caso la vostra versione di Mercurial sia precedente alla 1.1, vi consiglio vivamente di installare una versione più recente prima di affrontare unioni complicate.

Semplificare la sequenza di estrazione-unione-inserimento

Il processo di unione dei cambiamenti appena delineato è molto semplice, ma richiede di eseguire tre comandi in sequenza.

hg pull -u
hg merge
hg commit -m 'Incorporati i cambiamenti remoti'

Nel caso dell’inserimento finale, avete anche bisogno di includere un messaggio di commit, che sarà quasi sempre composto da testo «standard» non particolarmente interessante.

Se fosse possibile, sarebbe comodo ridurre il numero di passi necessari. In effetti, Mercurial viene distribuito con un’estensione chiamata fetch pensata proprio per questo scopo.

Mercurial fornisce un meccanismo flessibile per consentire ad altre persone di estendere le sue funzionalità, preservando le dimensioni ridotte e la manutenibilità del proprio nucleo. Alcune estensioni aggiungono nuovi comandi che potete usare dalla riga di comando, mentre altre lavorano «dietro le quinte», per esempio accrescendo la funzionalità della modalità server predefinita di Mercurial.

L’estensione fetch aggiunge un nuovo comando chiamato, naturalmente, hg fetch. Questa estensione agisce come una combinazione di hg pull -u, hg merge e hg commit. Comincia propagando verso il repository corrente i cambiamenti estratti da un altro repository. Se si accorge che i cambiamenti hanno aggiunto una nuova testa al repository, aggiorna la directory di lavoro alla nuova testa, poi avvia il processo di unione e infine, se l’unione ha successo, ne inserisce il risultato nel repository con un messaggio di commit generato automaticamente. Se non sono state aggiunte nuove teste, il comando si limita ad aggiornare la directory di lavoro alla nuova revisione di punta.

Abilitare l’estensione fetch è facile. Aprite il file .hgrc che si trova nella vostra directory personale e andate alla sezione extensions (oppure createla se non esiste già). Poi aggiungete una riga che contenga semplicemente «fetch=».

[extensions]
fetch =

(Normalmente, la parte a destra del simbolo «=» indicherebbe dove trovare l’estensione, ma dato che l’estensione fetch è compresa nella distribuzione standard, Mercurial sa già dove andarla a cercare.)

Rinominare, copiare e unire

Durante la vita di un progetto, vorremo spesso cambiare la disposizione dei suoi file e delle sue directory. Queste modifiche possono essere tanto semplici quanto rinominare un singolo file, o tanto complesse quanto ristrutturare l’intera gerarchia dei file nell’ambito del progetto.

Mercurial supporta questi tipi di cambiamenti in maniera fluida, a patto che gli diciamo quello che stiamo facendo. Se vogliamo rinominare un file, dovremmo usare il comando hg rename[2] per cambiare il nome del file, in modo che Mercurial possa comportarsi in maniera appropriata nel caso più tardi effettuassimo un’unione.

Tratteremo l’uso di questi comandi in maniera più estesa nella sezione chiamata «Copiare i file».



[2] Se siete utenti Unix, sarete felici di sapere che il comando hg rename si può abbreviare in hg mv.

Volete rimanere aggiornati? Abbonatevi al feed delle modifiche per il libro italiano.

Copyright 2006, 2007, 2008, 2009 Bryan O’Sullivan. Icone realizzate da Paul Davey alias Mattahan.

Copyright 2009 Giulio Piancastelli per la traduzione italiana.