Capitolo 5. L’uso quotidiano di Mercurial

Indice

Aggiungere file a un repository Mercurial
Designazione esplicita o implicita dei file
Mercurial registra i file, non le directory
Come rimuovere un file dal repository
La rimozione di un file non ha effetti sulla sua cronologia.
File mancanti
Digressione: perché dire esplicitamente a Mercurial di rimuovere un file?
Utile scorciatoia—aggiungere e rimuovere i file in un unico passo
Copiare i file
I risultati di una copia durante un’unione
Perché i cambiamenti dovrebbero seguire le copie?
Come evitare che i cambiamenti seguano una copia
Il comportamento del comando hg copy
Rinominare i file
Rinominare i file e unire i cambiamenti
Cambiamenti di nome divergenti e unioni
Cambiamenti di nome convergenti e unioni
Altri casi particolari legati ai nomi
Rimediare agli errori
Affrontare unioni complesse
Gli stati di risoluzione di un file
Risolvere un’unione di file
Formati di diff più utili
Quali file gestire e quali file evitare
Realizzare backup e mirror

Aggiungere file a un repository Mercurial

Mercurial lavora solo con i file che gli dite di amministrare nel vostro repository. Il comando hg status vi dirà quali sono i file che Mercurial non conosce, usando un «?» per mostrare questi file.

Per dire a Mercurial di tenere traccia di un file, usate il comando hg add. Una volta che avete aggiunto un file, la voce per quel file nell’elenco visualizzato da hg status cambia da «?» ad «A».

$ hg init esempio-add
$ cd esempio-add
$ echo a > miofile.txt
$ hg status
? miofile.txt
$ hg add miofile.txt
$ hg status
A miofile.txt
$ hg commit -m "Aggiunto un file."
$ hg status

Dopo aver eseguito hg commit, i file che avete aggiunto prima dell’inserimento non verranno più elencati dal comando hg status, perché il comportamento predefinito di hg status è quello di segnalarvi solo i file «interessanti» come (per esempio) quelli che avete modificato, rimosso, o rinominato. Se avete un repository che contiene migliaia di file, vorrete raramente sapere qualcosa dei file che Mercurial ha già registrato ma che non sono cambiati. (Potete comunque ottenere questa informazione, come vedremo più avanti.)

Mercurial non agisce immediatamente su un file che avete appena aggiunto, ma scatterà una fotografia dello stato del file la prossima volta che eseguirete un commit. Poi continuerà a tenere traccia dei cambiamenti che apportate al file ogni volta che eseguite un commit, fino a quando non rimuoverete il file.

Designazione esplicita o implicita dei file

Se passate un nome di una directory a un comando, ogni comando Mercurial interpreterà opportunamente questa azione come la richiesta di «operare su ogni file in questa directory e nelle sue sottodirectory».

$ mkdir b
$ echo b > b/qualchefile.txt
$ echo c > b/sorgente.cpp
$ mkdir b/d
$ echo d > b/d/test.h
$ hg add b
aggiungo b/d/test.h
aggiungo b/qualchefile.txt
agginugo b/sorgente.cpp
$ hg commit -m "Aggiunti tutti i file nella sottodirectory."

Notate che, in questo esempio, Mercurial ha stampato i nomi dei file che ha aggiunto, mentre non lo ha fatto quando abbiamo aggiunto il file miofile.txt nell’esempio precedente.

Questo accade perché, nel primo esempio, abbiamo esplicitamente nominato il file da aggiungere sulla riga di comando. In questi casi, Mercurial assume che sappiamo ciò che stiamo facendo, per cui non stampa alcuna informazione.

Tuttavia, quando implichiamo i nomi dei file dando il nome di una directory, Mercurial compie il passo aggiuntivo di stampare il nome di ogni file su cui agisce. Questo rende più chiaro ciò che sta succedendo e riduce la probabilità di una sorpresa sgradita e silenziosa. La maggior parte dei comandi Mercurial si comporta in questo modo.

Mercurial registra i file, non le directory

Mercurial non tiene traccia delle informazioni sulle directory, ma tiene traccia del percorso di un file. Prima di creare un file, crea tutte le directory mancanti che ne compongono il percorso. Dopo che ha cancellato un file, cancella ogni directory vuota che faceva parte del percorso del file cancellato. Questa sembra una distinzione irrilevante, ma ha una conseguenza pratica di secondaria importanza: Mercurial non vi permette di rappresentare una directory completamente vuota.

Le directory vuote sono raramente utili e ci sono soluzioni non invadenti che potete usare per ottenere un effetto appropriato. Quindi, gli sviluppatori di Mercurial hanno deciso che la complessità che sarebbe stata richiesta per gestire le directory vuote non valesse il limitato beneficio che questa caratteristica avrebbe portato.

Se avete bisogno di una directory vuota nel vostro repository, ci sono alcuni modi per ottenerla. Uno dei modi possibili è quello di creare una directory e usare hg add per aggiungere un file «nascosto» a quella directory. Sui sistemi di tipo Unix, ogni file il cui nome comincia con un punto («.») viene considerato nascosto dalla maggior parte dei comandi e delle applicazioni con interfaccia grafica. Questo approccio è illustrato qui di seguito.

$ hg init esempio-nascosto
$ cd esempio-nascosto
$ mkdir vuota
$ touch vuota/.nascosto
$ hg add vuota/.nascosto
$ hg commit -m "Gestisce una directory che sembra vuota."
$ ls vuota
$ cd ..
$ hg clone esempio-nascosto temp
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ ls temp
vuota
$ ls temp/vuota

Un altro modo per soddisfare il bisogno di una directory vuota è semplicemente quello di farla creare al vostro programma automatico di assemblaggio del progetto nel momento in cui ne avete bisogno.

Come rimuovere un file dal repository

Una volta che avete deciso che un file non appartiene più al vostro repository, usate il comando hg remove. Questo comando cancella il file e dice a Mercurial di non tenerne più traccia (cosa che avverrà nel prossimo commit). Un file rimosso viene rappresentato con una «R» nell’elenco prodotto da hg status.

$ hg init esempio-remove
$ cd esempio-remove
$ echo a > a
$ mkdir b
$ echo b > b/b
$ hg add a b
aggiungo b/b
$ hg commit -m "Piccolo esempio di rimozione di file."
$ hg remove a
$ hg status
R a
$ hg remove b
rimuovo b/b

Dopo che avete rimosso un file tramite hg remove, Mercurial non terrà più traccia di quel file anche se ricreate un file con lo stesso nome nella vostra directory di lavoro. Se ricreate davvero un file con lo stesso nome e volete che Mercurial amministri il nuovo file, usate semplicemente hg add. Mercurial saprà che il nuovo file non è in alcun modo legato al vecchio file con lo stesso nome.

La rimozione di un file non ha effetti sulla sua cronologia.

È importante capire che la rimozione di un file ha solo due effetti.

  • Cancella la versione corrente del file dalla directory di lavoro.

  • Induce Mercurial a smettere di monitorare i cambiamenti del file dal commit successivo in poi.

La rimozione di un file non altera la cronologia del file in alcun modo.

Se aggiornate la directory di lavoro a un changeset che era stato inserito quando Mercurial stava ancora tenendo traccia del file che più tardi avete rimosso, il file riapparirà nella directory di lavoro con i contenuti che aveva quando avete inserito quel changeset. Se poi aggiornate la directory di lavoro a un changeset successivo in cui il file è stato rimosso, Mercurial cancellerà ancora una volta il file dalla directory di lavoro.

File mancanti

Mercurial considera mancante un file che avete cancellato senza usare hg remove. Un file mancante viene rappresentato con «!» nell’elenco mostrato da hg status. Di solito, i comandi Mercurial non agiscono mai sui file mancanti.

$ hg init esempio-mancante
$ cd esempio-mancante
$ echo a > a
$ hg add a
$ hg commit -m "Il file sta per diventare mancante."
$ rm a
$ hg status
! a

Se il vostro repository contiene un file che hg status segnala come mancante e volete che il file rimanga assente, potete eseguire hg remove --after in qualsiasi momento per dire a Mercurial che volevate effettivamente rimuovere il file.

$ hg remove --after a
$ hg status
R a

D’altra parte, se avete cancellato il file mancante per errore, passate al comando hg revert il nome del file da recuperare e il file riapparirà senza alcun cambiamento.

$ hg revert a
$ cat a
a
$ hg status

Digressione: perché dire esplicitamente a Mercurial di rimuovere un file?

Potreste chiedervi perché Mercurial vi costringe a dirgli esplicitamente che state cancellando un file. Nelle prime fasi di sviluppo, Mercurial vi permetteva di cancellare un file nel modo che preferivate: avrebbe notato automaticamente l’assenza del file durante la successiva esecuzione di hg commit e avrebbe smesso di monitorarlo. In pratica, questo modo di operare rendeva troppo facile rimuovere accidentalmente un file senza accorgersene.

Utile scorciatoia—aggiungere e rimuovere i file in un unico passo

Mercurial fornisce il comando combinato hg addremove per aggiungere i file non ancora registrati e segnare i file mancanti come rimossi.

$ hg init esempio-addremove
$ cd esempio-addremove
$ echo a > a
$ echo b > b
$ hg addremove
aggiungo a
aggiungo b

Il comando hg commit offre anche un’opzione -A che effettua la stessa operazione di aggiunta-e-rimozione, immediatamente seguita da un commit.

$ echo c > c
$ hg commit -A -m "Commit con addremove."
aggiungo c

Copiare i file

Mercurial fornisce un comando hg copy che vi permette di creare una nuova copia di un file. Quando copiate un file usando questo comando, Mercurial registra il fatto che il nuovo file è una copia del file originale e tratta i file copiati in maniera speciale quando unite il vostro lavoro con quello di qualcun altro.

I risultati di una copia durante un’unione

Quello che succede durante un’unione è che i cambiamenti «seguono» la copia. Per illustrare al meglio cosa questo significa, creiamo un esempio. Cominceremo con il solito piccolo repository che contiene un singolo file.

$ hg init mia-copia
$ cd mia-copia
$ echo riga > file
$ hg add file
$ hg commit -m "Aggiunto un file."

Abbiamo bisogno di fare alcune modifiche in parallelo, in modo da avere due cambiamenti da unire tra loro. Quindi cloniamo il nostro repository.

$ cd ..
$ hg clone mia-copia vostra-copia
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti

Tornando al nostro repository iniziale, usiamo il comando hg copy per fare una copia del primo file che abbiamo creato.

$ cd mia-copia
$ hg copy file nuovo-file

Se successivamente osserviamo il risultato del comando hg status, il file copiato appare come un normale file aggiunto.

$ hg status
A nuovo-file

Ma se passiamo l’opzione -C al comando hg status, otterremo un’altra riga nell’elenco stampato: questo è il file da cui il nostro file appena aggiunto è stato copiato.

$ hg status -C
A nuovo-file
  file
$ hg commit -m "File copiato."

Ora, tornando al repository che abbiamo clonato, apportiamo un cambiamento in parallelo. Aggiungeremo una riga al contenuto del file originale che abbiamo creato.

$ cd ../vostra-copia
$ echo 'nuovi contenuti' >> file
$ hg commit -m "File modificato."

Ora abbiamo un file modificato in questo repository. Quando estraiamo i cambiamenti dal primo repository e uniamo le due teste, Mercurial propagherà i cambiamenti che abbiamo apportato localmente a file nella sua copia nuovo-file.

$ hg pull ../mia-copia
estraggo da ../mia-copia
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)
$ hg merge
unisco file e nuovo-file in nuovo-file
0 file aggiornati, 1 file uniti, 0 file rimossi, 0 file irrisolti
(unione tra rami, ricordatevi di eseguire il commit)
$ cat nuovo-file
riga
nuovi contenuti

Perché i cambiamenti dovrebbero seguire le copie?

Questo comportamento—dei cambiamenti a un file che si propagano alle copie del file—potrebbe sembrare esoterico, ma nella maggior parte dei casi è altamente desiderabile.

Prima di tutto, ricordatevi che questa propagazione avviene solamente durante un’unione. Quindi se usate hg copy su un file e in seguito modificate il file originale nel normale corso del vostro lavoro, non accadrà nulla.

La seconda cosa da sapere è che le modifiche si propagheranno alla copia solo se il changeset da cui state incorporando le modifiche non ha ancora visto la copia.

Il motivo per cui Mercurial si comporta in questo modo è il seguente. Diciamo che io correggo un bug importante in un file sorgente e inserisco i miei cambiamenti nel repository. Nel frattempo, voi avete deciso di eseguire hg copy per fare una copia del file nel vostro repository, senza sapere del bug o aver visto la correzione, e avete cominciato a lavorare sulla vostra copia del file.

Se dopo aver estratto e incorporato le mie modifiche Mercurial non avesse propagato i cambiamenti attraverso le copie, il vostro nuovo file sorgente ora conterrebbe il bug e, a meno che voi non sapeste come propagare la correzione a mano, il bug rimarrebbe nella vostra copia del file.

Propagando automaticamente le modifiche che hanno corretto il bug dal file originale alla copia, Mercurial previene questo tipo di problemi. A quanto ne so, Mercurial è l’unico sistema di controllo di revisione che propaga i cambiamenti verso le copie in questo modo.

Una volta che la vostra cronologia dei cambiamenti contiene la registrazione che la copia e la successiva unione sono avvenute, di solito non c’è più bisogno di propagare cambiamenti dal file originale al file copiato. Questo è il motivo per cui Mercurial propaga i cambiamenti verso le copie solo durante la prima unione ma non successivamente.

Come evitare che i cambiamenti seguano una copia

Se per qualche ragione decidete che questa faccenda di propagare automaticamente i cambiamenti verso le copie non fa per voi, utilizzate il normale comando per la copia di file fornito dal vostro sistema (cp per i sistemi di tipo Unix) per effettuare la copia di un file, poi aggiungete a mano la nuova copia invocando hg add. Prima di farlo, però, rileggete la sezione chiamata «Perché i cambiamenti dovrebbero seguire le copie?» e prendete una decisione informata sulla validità di questo comportamento nel vostro caso specifico.

Il comportamento del comando hg copy

Quando usate il comando hg copy, Mercurial esegue la copia dei file originali contenuti nella directory di lavoro nello stato in cui si trovano in quel momento. Questo significa che, se fate alcune modifiche a un file e poi lo copiate tramite hg copy senza prima aver inserito quelle modifiche nel repository, anche la nuova copia conterrà le modifiche che avete apportato fino a quel momento. (Trovo che questo comportamento sia leggermente controintuitivo ed è per questo che lo menziono qui.)

Il comando hg copy agisce in maniera simile al comando Unix cp (potete usare l’alias hg cp se preferite). Dobbiamo fornirgli due o più argomenti, di cui l’ultimo viene trattato come destinazione e tutti gli altri vengono trattati come sorgenti.

Se invocate hg copy con un singolo file come sorgente e la destinazione non esiste, il comando crea un nuovo file con quel nome.

$ mkdir k
$ hg copy a k
$ ls k
a

Se la destinazione è una directory, Mercurial copia le sorgenti in quella directory.

$ mkdir d
$ hg copy a b d
$ ls d
a  b

Copiare una directory è un’operazione ricorsiva e preserva la struttura delle directory della sorgente.

$ hg copy z e
copio z/a/c in e/a/c

Se la sorgente e la destinazione sono entrambe directory, l’albero della sorgente viene ricreato nella directory di destinazione.

$ hg copy z d
copio z/a/c in d/z/a/c

Come con il comando hg remove, se copiate un file manualmente e poi volete informare Mercurial di aver copiato il file, usate semplicemente l’opzione --after di hg copy.

$ cp a n
$ hg copy --after a n

Rinominare i file

È molto più comune aver bisogno di rinominare un file piuttosto che aver bisogno di copiarlo. La ragione per cui ho discusso il comando hg copy prima di parlare di come rinominare i file è che Mercurial tratta un cambiamento di nome essenzialmente nello stesso modo di una copia. Perciò, se sapete cosa fa Mercurial quando copiate un file, sapete anche cosa aspettarvi quando rinominate un file.

Quando usate il comando hg rename, Mercurial crea una copia del file originale, poi lo cancella e segnala il file come rimosso.

$ hg rename a b

Il comando hg status mostra la nuova copia del file come aggiunta e il file da cui è stata effettuata la copia come rimosso.

$ hg status
A b
R a

Come accade per i risultati del comando hg copy, dobbiamo usare l’opzione -C del comando hg status per vedere che Mercurial considera il file aggiunto come una copia del file originale ora rimosso.

$ hg status -C
A b
  a
R a

Come con hg remove e hg copy, potete usare l’opzione --after per informare Mercurial del cambiamento di nome dopo che il fatto è avvenuto. Nella maggior parte degli altri aspetti, il comportamento del comando hg rename e le opzioni che accetta sono simili a quelli del comando hg copy.

Se avete familiarità con la riga di comando Unix, sarete felici di sapere che il comando hg rename può essere invocato come hg mv.

Rinominare i file e unire i cambiamenti

Dato che Mercurial rinomina i file tramite un’operazione di copia-e-rimozione, i cambiamenti vengono propagati nello stesso modo quando effettuate un’unione sia dopo aver copiato un file che dopo averlo rinominato.

Se io modifico un file e voi lo rinominate e poi uniamo i nostri rispettivi cambiamenti, le mie modifiche al file con il suo nome originale verranno propagate al file con il suo nuovo nome. (Vi potreste aspettare che questo «funzioni e basta» ma in realtà non tutti i sistemi di controllo di revisione lo fanno.)

Sebbene la propagazione dei cambiamenti alle copie sia una funzione che potreste approvare dicendo «sì, questo potrebbe essere utile», deve essere chiaro che propagare i cambiamenti ai file rinominati è assolutamente importante. Senza questo meccanismo, i cambiamenti a un file si potrebbero perdere con troppa facilità quando il file viene rinominato.

Cambiamenti di nome divergenti e unioni

Il caso dei nomi divergenti si verifica quando due sviluppatori cominciano con un file—chiamiamolo foo—nei loro rispettivi repository.

$ hg clone orig anna
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ hg clone orig bruno
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti

Anna cambia il nome del file a bar.

$ cd anna
$ hg rename foo bar
$ hg ci -m "Rinominato foo a bar."

Nel frattempo, Bruno lo rinomina quux. (Ricordatevi che hg mv è un alias di hg rename.)

$ cd ../bruno
$ hg mv foo quux
$ hg ci -m "Rinominato foo a quux."

Mi piace pensare a questo come a un conflitto perché entrambi gli sviluppatori hanno espresso intenzioni differenti a proposito di come il file dovrebbe essere chiamato.

Cosa pensate che dovrebbe accadere quando Anna e Bruno uniscono il loro lavoro? L’effettivo comportamento di Mercurial è quello di preservare sempre entrambi i nomi quando unisce changeset che contengono cambiamenti di nome divergenti.

# Si veda http://www.selenic.com/mercurial/bts/issue455
$ cd ../orig
$ hg pull -u ../anna
estraggo da ../anna
cerco i cambiamenti
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 1 changeset con 1 cambiamenti a 1 file
1 file aggiornati, 0 file uniti, 1 file rimossi, 0 file irrisolti
$ hg pull ../bruno
estraggo da ../bruno
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)
$ hg merge
attenzione: cambiamenti di nome divergenti di foo a:
 bar
 quux
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
(unione tra rami, ricordatevi di eseguire il commit)
$ ls
bar  quux

Notate che, sebbene Mercurial vi avverta del cambiamento di nome divergente, lascia che siate voi a riconciliare la divergenza dopo l’unione.

Cambiamenti di nome convergenti e unioni

Un altro tipo di conflitto tra i cambiamenti di nome avviene quando due persone rinominano differenti file sorgente alla stessa destinazione. In questo caso, Mercurial esegue l’unione normalmente e lascia che siate voi a guidarlo verso una risoluzione ragionevole.

Altri casi particolari legati ai nomi

Mercurial soffre di un bug noto da tempo che gli impedisce di portare a termine un’unione in cui una parte contiene un file con un certo nome mentre l’altra contiene una directory con lo stesso nome. Questo è documentato come problema 29.

$ hg init problema29
$ cd problema29
$ echo a > a
$ hg ci -Ama
aggiungo a
$ echo b > b
$ hg ci -Amb
aggiungo b
$ hg up 0
0 file aggiornati, 0 file uniti, 1 file rimossi, 0 file irrisolti
$ mkdir b
$ echo b > b/b
$ hg ci -Amc
aggiungo b/b
creata una nuova testa
$ hg merge
fallimento: è una directory: /tmp/problema29/b

Rimediare agli errori

Mercurial possiede alcuni comandi utili che vi aiuteranno a rimediare a diversi errori comuni.

Il comando hg revert vi permette di annullare i cambiamenti che avete apportato alla vostra directory di lavoro. Per esempio, se avete aggiunto un file invocando hg add per errore, vi basta eseguire hg revert con il nome del file che avete aggiunto e il file non verrà toccato in alcun modo né sarà più considerato per essere aggiunto da Mercurial. Potete anche usare hg revert per disfarvi di cambiamenti sbagliati apportati a un file.

È utile ricordare che il comando hg revert serve per i cambiamenti che non avete ancora inserito. Una volta che avete inserito un cambiamento, se decidete che è stato un errore potete ancora fare qualcosa, sebbene le vostre opzioni siano molto più limitate.

Per maggiori informazioni sul comando hg revert e dettagli su come trattare i cambiamenti che avete gia inserito, leggete il Capitolo 9, Trovare e correggere gli errori.

Affrontare unioni complesse

In un progetto grande o complicato, può capitare che l’unione tra due changeset provochi qualche mal di testa. Supponete che ci sia un file sorgente di grandi dimensioni che è stato ampiamente modificato da entrambe le parti di un’unione: quasi inevitabilmente, questo risulterà in conflitti, alcuni dei quali potrebbero avere bisogno di più di un tentativo per venire risolti.

Costruiamoci un semplice esempio di questa eventualità e vediamo come affrontarlo. Cominceremo con un repository contenente un file e lo cloneremo due volte.

$ hg init conflitto
$ cd conflitto
$ echo primo > miofile.txt
$ hg ci -A -m primo
aggiungo miofile.txt
$ cd ..
$ hg clone conflitto sinistra
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ hg clone conflitto destra
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti

In uno dei cloni, modificheremo il file in un modo.

$ cd sinistra
$ echo sinistra >> miofile.txt
$ hg ci -m sinistra

Nell’altro, modificheremo il file in modo differente.

$ cd ../destra
$ echo destra >> miofile.txt
$ hg ci -m destra

Poi, propagheremo entrambi i cambiamenti nel nostro repository originale.

$ cd ../conflitto
$ hg pull -u ../sinistra
estraggo da ../sinistra
cerco i cambiamenti
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 1 changeset con 1 cambiamenti a 1 file
1 file aggiornati, 0 file uniti, 1 file rimossi, 0 file irrisolti
$ hg pull -u ../destra
estraggo da ../destra
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)

Ora ci aspettiamo che il nostro repository contenga due teste.

$ hg heads
changeset:   2:6e8cd0b94b4c
etichetta:   tip
genitore:    0:05f41910a168
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:49:28 2009 +0000
sommario:    destra

changeset:   1:ab0fbd8c502d
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:49:27 2009 +0000
sommario:    sinistra

Normalmente, se eseguissimo il comando hg merge a questo punto, ci verrebbe presentata un’applicazione grafica tramite la quale riconciliare manualmente le modifiche in conflitto su miofile.txt. Tuttavia, per semplificare le cose ai fini della presentazione, vorremmo invece che l’unione fallisse immediatamente. Ecco un modo in cui possiamo farlo.

$ export HGMERGE=false

Abbiamo chiesto al meccanismo di unione di Mercurial di eseguire il comando false (che, come desideriamo, fallisce immediatamente) se si accorge di non riuscire a risolvere un’unione automaticamente.

Se ora lanciamo hg merge, il comando dovrebbe fermarsi e riportare un fallimento.

$ hg merge
unisco miofile.txt
merge: attenzione: conflitti durante l'unione
unione di miofile.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

Se anche non avessimo notato che l’unione è fallita, Mercurial eviterà di farci accidentalmente inserire nel repository i risultati di un’unione fallita.

$ hg commit -m "Tentativo di inserire i risultati di un'unione fallita."
fallimento: conflitti di unione irrisolti (si veda hg resolve)

In questo caso, hg commit fallisce e ci suggerisce di usare il comando hg resolve, che noi ancora non conosciamo. Come al solito, hg help resolve stamperà una pratica sinossi.

Gli stati di risoluzione di un file

Quando avviene un’unione, di solito la maggior parte dei file rimarrà tale e quale. Mercurial terrà traccia dello stato di ogni file su cui deve operare.

  • Un file risolto è stato unito con successo, o automaticamente da Mercurial o con un intervento umano.

  • Un file irrisolto non è stato unito con successo e necessita di ulteriori attenzioni.

Se Mercurial vede un qualsiasi file nello stato irrisolto dopo un’unione, considera fallita l’unione. Fortunatamente, non abbiamo bisogno di ricominciare l’intera unione da zero.

L’opzione --list o -l del comando hg resolve mostra lo stato di ogni file coinvolto in un’unione.

$ hg resolve -l
U miofile.txt

Nell’elenco stampato da hg resolve, un file risolto è contrassegnato con una R mentre un file irrisolto è contrassegnato con una U. Se un file qualsiasi viene elencato con una U, sappiamo che un tentativo di inserire i risultati dell’unione nel repository andrà incontro al fallimento.

Risolvere un’unione di file

Abbiamo diverse opzioni per far passare un file dallo stato irrisolto a quello risolto. Quella di gran lunga più comune consiste nell’eseguire nuovamente hg resolve. Se passiamo i nomi di singoli file o directory, il comando riproverà a unire i file irrisolti presenti in quelle ubicazioni. Possiamo anche passare l’opzione --all o -a per riprovare a unire tutti i file irrisolti.

Mercurial ci permette anche di modificare direttamente lo stato di risoluzione di un file. Possiamo contrassegnare manualmente un file come risolto usando l’opzione --mark o come irrisolto usando l’opzione --unmark. Questo ci consente di ripulire a mano un’unione particolarmente disordinata e di tenere traccia dei nostri progressi con ogni file man mano che procediamo.

Formati di diff più utili

Il formato predefinito del testo stampato dal comando hg diff è compatibile all’indietro con il normale comando diff, ma questo presenta alcuni svantaggi.

Considerate il caso in cui si utilizzi hg rename per rinominare un file.

$ hg rename foo bar
$ hg diff
diff -r b01d46ff402d foo
--- foo/foo	Fri Jun 05 15:49:22 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-a
diff -r b01d46ff402d bar
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ bar/bar	Fri Jun 05 15:49:23 2009 +0000
@@ -0,0 +1,1 @@
+a

Il risultato di hg diff mostrato qui sopra offusca il fatto che abbiamo semplicemente rinominato un file. Il comando hg diff accetta l’opzione --git o -g per usare un formato di diff più nuovo che presenta queste informazioni in una forma più leggibile.

$ hg diff -g
diff --git foo/foo bar/bar
cambiamento di nome da foo
cambiamento di nome a bar

Questa opzione ci viene in aiuto anche in un caso che altrimenti risulterebbe confuso: un file che sembra essere stato modificato secondo hg status, ma per il quale hg diff non stampa nulla. Questa situazione può presentarsi se cambiamo i permessi di esecuzione di un file.

$ chmod +x a
$ hg st
M a
$ hg diff

Il normale comando diff non considera i permessi dei file, perciò la semplice invocazione di hg diff non stampa nulla. Se però utilizziamo l’opzione -g, il comando ci dice che cos’è realmente accaduto.

$ hg diff -g
diff --git a/a b/a
vecchia modalità 100644
nuova modalità 100755

Quali file gestire e quali file evitare

Generalmente, i sistemi di controllo di revisione danno il meglio nella gestione dei file di testo scritti da esseri umani, come il codice sorgente, i cui file non cambiano molto da una revisione all’altra. Alcuni sistemi centralizzati di controllo di revisione possono anche destreggiarsi abbastanza bene con i file binari, come le immagini bitmap.

Per esempio, gli sviluppatori di un gioco dovranno tipicamente gestire sia il proprio codice sorgente sia le proprie risorse binarie (e.g. dati geometrici, texture, schemi di mappe) attraverso un sistema di controllo di revisione.

Dato che di solito è impossibile unire due modifiche a un file binario in conflitto tra loro, spesso i sistemi centralizzati forniscono un meccanismo di bloccaggio dei file che permette a un utente di dire «sono la sola persona che può modificare questo file».

Rispetto a un sistema centralizzato, un sistema distribuito di controllo di revisione modifica alcuni dei fattori che guidano le decisioni su quali file gestire e come gestirli.

Per esempio, un sistema distribuito di controllo di revisione non può, per sua natura, offrire un meccanismo di bloccaggio dei file. Quindi non esiste alcun meccanismo predefinito per evitare che due persone apportino cambiamenti in conflitto a un file binario. Se fate parte di un gruppo in cui diverse persone potrebbero modificare frequentemente i file binari, potrebbe non essere una buona idea impiegare Mercurial—o un qualsiasi altro sistema distribuito di controllo di revisione—per gestire quei file.

Quando Mercurial memorizza le modifiche a un file, di solito salva solo le differenze tra la versione corrente del file e quella precedente. Per la maggior parte dei file di testo questo approccio si rivela estremamente efficiente. Tuttavia, alcuni file (in particolare i file binari) sono fatti in modo tale che persino una piccola modifica al contenuto logico del file risulta nel cambiamento di molti o della maggior parte dei byte contenuti nel file. Per esempio, i file compressi sono particolarmente sensibili a questo effetto. Se le differenze tra ogni versione di un file e la successiva sono sempre grandi, Mercurial non riuscirà a memorizzare la cronologia del file in maniera molto efficiente. Questo potrebbe avere effetti sia sul bisogno di spazio di memorizzazione locale sia sulla quantità di tempo che viene impiegata per clonare un repository.

Per avere un’idea di come questo problema potrebbe riguardarvi nella pratica, supponete di voler usare Mercurial per gestire un documento OpenOffice. OpenOffice memorizza i documenti su disco sotto forma di file ZIP compressi. Modificate anche solo una lettera nel vostro documento in OpenOffice e quasi ogni byte nell’intero file cambierà quando lo salverete. Ora supponete che le dimensioni di quel file siano pari a 2MB. Dato che la maggior parte del file cambia ogni volta che lo salvate, Mercurial dovrà memorizzare tutti i 2MB del file ogni volta che eseguite un commit, anche se dal vostro punto di vista forse solo poche parole vengono cambiate ogni volta. Un singolo file modificato frequentemente che non rispetti le assunzioni dei meccanismi di memorizzazione di Mercurial può facilmente avere un effetto fuori misura sulle dimensioni del repository.

Di male in peggio, se due di voi modificano il documento OpenOffice su cui state lavorando, non c’è alcun modo utile di effettuare un’unione tra le diverse versioni. In effetti, non c’è nemmeno un buon modo di capire quali sono le differenze tra i vostri rispettivi cambiamenti.

Quindi, ci sono alcune chiare raccomandazioni da fare sui tipi di file ai quali dovete stare molto attenti.

  • I file che sono molto grandi e incomprimibili, come per esempio le immagini ISO dei CD-ROM, renderanno la clonazione attraverso la rete molto lenta semplicemente a causa delle loro dimensioni.

  • I file che cambiano parecchio da una revisione alla successiva potrebbero essere costosi da memorizzare se li modificate frequentemente, e i conflitti causati da modifiche in parallelo a questi file potrebbero essere difficili da risolvere.

Realizzare backup e mirror

Dato che Mercurial mantiene una copia completa della cronologia in ogni clone, chiunque usi Mercurial per collaborare su un progetto può potenzialmente agire come una sorgente di backup nell’eventualità di una catastrofe. Se un repository centrale diventa inaccessibile, potete costruire un rimpiazzo semplicemente clonando una copia del repository da un collaboratore ed estraendo dai repository di altre persone qualsiasi cambiamento che quella copia potrebbe non avere visto.

Usare Mercurial per effettuare backup separati e mirror remoti è piuttosto semplice. Impostate un’attività periodica (e.g. tramite il comando cron) su un server remoto per estrarre i cambiamenti dai vostri repository principali ogni ora. Questa operazione diventerà complicata solo nell’improbabile caso in cui il numero dei repository principali che mantenete cambi frequentemente, eventualità che potrete affrontare utilizzando uno script per programmare l’aggiornamento della lista dei repository di cui fare il backup.

Se effettuate un backup tradizionale dei vostri repository principali su nastro o su disco e volete fare il backup di un repository chiamato miorepo, usate il comando hg clone -U miorepo miorepo.bak per creare un clone di miorepo prima di cominciare a registrare i vostri backup. L’opzione -U evita di popolare la directory di lavoro dopo che la clonazione si è conclusa, dato che sarebbe superfluo e renderebbe più lungo il backup.

Se poi effettuate il backup di miorepo.bak invece di miorepo, avrete la garanzia di possedere una fotografia consistente del vostro repository a cui nessuno sviluppatore insonne trasmetterà i propri cambiamenti nel bel mezzo di un’operazione di backup.

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.