Capitolo 9. Trovare e correggere gli errori

Indice

Cancellare la cronologia locale
L’inserimento accidentale
Abortire una transazione
L’estrazione sbagliata
Abortire una transazione è inutile se avete già trasmesso le modifiche
Potete abortire una sola transazione
Rimediare alle modifiche sbagliate
Errori nella gestione dei file
Gestire i cambiamenti inseriti
Ritirare un changeset
Ritirare il changeset di punta
Ritirare un changeset diverso dalla punta
Usate sempre l’opzione --merge
Controllare meglio il processo di ritiro
Perché hg backout funziona in questo modo
Modifiche che non avrebbero mai dovuto essere fatte
Ritirare un’unione
Proteggervi dai cambiamenti che vi sono «sfuggiti»
Cosa fare con i cambiamenti sensibili che sfuggono
Trovare la causa di un bug
Usare il comando hg bisect
Riordinare dopo la vostra ricerca
Suggerimenti per trovare efficacemente i bug
Fornite informazioni consistenti
Automatizzate il più possibile
Controllate i vostri risultati
Fate attenzione alle interferenze tra i bug
Circoscrivete la vostra ricerca in maniera approssimativa

Sbagliare potrebbe essere umano, ma per gestire davvero bene le conseguenze degli errori ci vuole un sistema di controllo di revisione di prima qualità. In questo capitolo, discuteremo alcune tecniche che potete usare quando scoprite che un problema si è insinuato nel vostro progetto. Mercurial è dotato di alcune funzioni particolarmente efficaci che vi aiuteranno a isolare le cause dei problemi e a trattarle in maniera appropriata.

Cancellare la cronologia locale

L’inserimento accidentale

In maniera occasionale ma persistente mi capita di digitare più velocemente di quanto riesca a pensare, cosa che talvolta provoca come conseguenza l’inserimento di un changeset incompleto o completamente sbagliato. Nel mio caso, il classico tipo di changeset incompleto è quello in cui ho creato un nuovo file sorgente ma ho dimenticato di usare hg add per aggiungerlo al repository. Un changeset «completamente sbagliato» non è così comune, ma non è meno fastidioso.

Abortire una transazione

Nella sezione chiamata «Operazioni sicure», ho menzionato che Mercurial tratta ogni modifica del repository come una transazione. Ogni volta che inserite un changeset o estraete i cambiamenti da un altro repository, Mercurial ricorda cosa avete fatto. Potete annullare, o abortire, esattamente una di queste azioni usando il comando hg rollback. (Leggete la sezione chiamata «Abortire una transazione è inutile se avete già trasmesso le modifiche» per un importante avvertimento su come usare questo comando.)

Ecco un errore che mi ritrovo spesso a commettere: inserire un changeset in cui ho creato un nuovo file dimenticandomi di aggiungerlo tramite hg add.

$ hg status
M a
$ echo b > b
$ hg commit -m "Aggiunge il file b."

Un’occhiata al risultato di hg status dopo l’inserimento conferma immediatamente l’errore.

$ hg status
? b
$ hg tip
changeset:   1:249cc777b54f
etichetta:   tip
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:51:13 2009 +0000
sommario:    Aggiunge il file b.

Il commit ha catturato le modifiche al file a, ma non il nuovo file b. È molto probabile che qualcosa in a si riferisca a b, ma se trasmettessi questo changeset a un repository condiviso, i collaboratori che estrarranno i miei cambiamenti non troverebbero b nei loro repository. Di conseguenza, diventerei oggetto di una certa indignazione.

Tuttavia, la fortuna è dalla mia parte—mi sono accorto dell’errore prima di trasmettere il changeset. Ora uso il comando hg rollback e Mercurial farà sparire quell’ultimo changeset.

$ hg rollback
abortisco l'ultima transazione
$ hg tip
changeset:   0:ce49f5b59f20
etichetta:   tip
utente:      Bryan O'Sullivan <[email protected]>
data:        Fri Jun 05 15:51:13 2009 +0000
sommario:    Primo commit.

$ hg status
M a
? b

Notate che il changeset non è più presente nella cronologia del repository e che la directory di lavoro pensa ancora che il file a sia stato modificato. Il commit e la sua cancellazione hanno lasciato la directory di lavoro esattamente nello stato in cui si trovava prima dell’inserimento: il changeset è stato completamente rimosso. Ora posso tranquillamente usare hg add per aggiungere il file b e rieseguire il commit.

$ hg add b
$ hg commit -m "Aggiunge il file b, questa volta per davvero."

L’estrazione sbagliata

È pratica comune usare Mercurial mantenendo in repository differenti i rami di sviluppo separati di un progetto. Il vostro gruppo di sviluppo potrebbe avere un repository condiviso per la release «0.9» del vostro progetto e un altro, contenente cambiamenti differenti, per la release «1.0».

In questa situazione, potete immaginare che pasticcio accadrebbe se aveste un repository «0.9» locale e vi propagaste accidentalmente i cambiamenti dal repository «1.0» condiviso. Nel caso peggiore, potreste non fare abbastanza attenzione e trasmettere quei cambiamenti nell’albero «0.9» condiviso, disorientando tutti gli altri sviluppatori (ma non preoccupatevi, ritorneremo a questo orribile scenario più avanti). Tuttavia, è più probabile che notiate immediatamente l’errore, perché Mercurial vi mostrerà l’URL da cui sta estraendo i cambiamenti, o perché vedrete Mercurial propagare un numero sospettosamente alto di cambiamenti nel repository.

Il comando hg rollback cancellerà scrupolosamente tutti i changeset che avete appena estratto. Mercurial raggruppa tutti i cambiamenti provenienti da un’invocazione di hg pull in una singola transazione, quindi un’unica invocazione di hg rollback è tutto quello che vi serve per annullare questo errore.

Abortire una transazione è inutile se avete già trasmesso le modifiche

Il valore di hg rollback scende a zero una volta che avete trasmesso le vostre modifiche a un altro repository. Abortire un cambiamento lo fa scomparire interamente, ma solo nel repository in cui invocate hg rollback. Dato che una cancellazione elimina parte della cronologia, non è possibile che la scomparsa di un cambiamento si propaghi tra i repository.

Se avete trasmesso un cambiamento a un altro repository—in particolare se è un repository condiviso—le modifiche sono essenzialmente «scappate dal recinto» e dovrete rimediare all’errore in un altro modo. Se trasmettete un changeset da qualche parte, lo abortite e poi estraete i cambiamenti dal repository verso cui avete effettuato la trasmissione, il changeset di cui credevate di esservi sbarazzati riapparirà semplicemente nel vostro repository.

Se siete assolutamente sicuri che il cambiamento che volete abortire è quello più recente contenuto nel repository a cui lo avete trasmesso e sapete che nessun altro può averlo estratto da quel repository, potete ritirare il changeset anche là, ma non dovreste aspettarvi che questo funzioni in maniera affidabile. Presto o tardi un cambiamento finirà in un repository su cui non avete un controllo diretto (o vi siete dimenticati di averlo) e vi si ritorcerà contro.

Potete abortire una sola transazione

Mercurial memorizza esattamente una transazione nel suo registro delle transazioni: quella più recente avvenuta nel repository. Questo significa che potete abortire solo una transazione. Se vi aspettate di poter abortire una transazione e poi quella che la precede, questo non è il comportamento che otterrete.

$ hg rollback
abortisco l'ultima transazione
$ hg rollback
nessuna informazione disponibile per abortire una transazione

Una volta che avete abortito una transazione in un repository, non potete effettuare un’altra volta questa operazione in quel repository fino a quando non avete eseguito un nuovo inserimento o una nuova estrazione.

Rimediare alle modifiche sbagliate

Se fate un cambiamento a un file e poi decidete che in realtà non volevate affatto modificare il file, ma non avete ancora inserito i vostri cambiamenti nel repository, il comando che vi serve è hg revert. Questo comando esamina il changeset genitore della directory di lavoro e ripristina il contenuto del file allo stato in cui era in quel changeset. (Questo è un modo prolisso per dire che, nel caso normale, annulla le vostre modifiche.)

Vediamo come funziona il comando hg revert, ancora con un altro piccolo esempio. Cominceremo modificando un file che Mercurial ha già registrato.

$ cat file
contenuto originale
$ echo cambiamento non voluto >> file
$ hg diff file
diff -r d17672b9fe6b file
--- a/file	Fri Jun 05 15:50:07 2009 +0000
+++ b/file	Fri Jun 05 15:50:07 2009 +0000
@@ -1,1 +1,2 @@
 contenuto originale
+cambiamento non voluto

Se non vogliamo quella modifica, possiamo semplicemente usare hg revert sul file.

$ hg status
M file
$ hg revert file
$ cat file
contenuto originale

Il comando hg revert ci fornisce un ulteriore grado di protezione salvando il nostro file modificato con un’estensione .orig.

$ hg status
? file.orig
$ cat file.orig
contenuto originale
cambiamento non voluto
[Suggerimento]Fate attenzione ai file .orig

È estremamente improbabile che stiate usando Mercurial per gestire file con estensione .orig o persino che siate interessati al contenuto di quei file. Nel caso, comunque, è utile ricordare che hg revert sovrascriverà incondizionatamente un file con estensione .orig esistente. Per esempio, se avete già un file foo.orig quando ritornate alla versione precedente del file foo, il contenuto di foo.orig verrà cestinato.

Ecco un riepilogo dei casi che il comando hg revert è in grado di gestire. Li descriveremo in dettaglio nella prossima sezione.

  • Se modificate un file, il comando lo ripristinerà al suo stato non modificato.

  • Se usate hg add su un file, hg revert annullerà lo stato di «aggiunto» del file ma lascerà intatti i contenuti del file.

  • Se cancellate un file senza dirlo a Mercurial, il comando ripristinerà i contenuti del file.

  • Se usate il comando hg remove per cancellare un file, hg revert annullerà lo stato di «rimosso» del file e ne ripristinerà i contenuti.

Errori nella gestione dei file

Il comando hg revert non è utile solo per i file modificati, ma vi permette di invertire i risultati di tutti i comandi Mercurial di gestione dei file come hg add, hg remove, e così via.

Se usate hg add su un file, poi decidete che in effetti non volete che Mercurial ne tenga traccia, potete usare hg revert per annullare l’operazione di aggiunta. Non preoccupatevi, Mercurial non modificherà il file in alcun modo, ma si limiterà a eliminare il «contrassegno» per quel file.

$ echo oops > oops
$ hg add oops
$ hg status oops
A oops
$ hg revert oops
$ hg status
? oops

Similmente, se chiedete a Mercurial di rimuovere un file tramite hg remove, potete usare hg revert per ripristinarne i contenuti allo stato in cui erano nel genitore della directory di lavoro.

$ hg remove file
$ hg status
R file
$ hg revert file
$ hg status
$ ls file
file

Questo funziona altrettanto bene con un file che avete cancellato a mano senza dirlo a Mercurial (ricordatevi che, nella terminologia di Mercurial, questo file viene detto «mancante»).

$ rm file
$ hg status
! file
$ hg revert file
$ ls file
file

Se invertite l’azione del comando hg copy, il file copiato rimane nella vostra directory di lavoro senza che Mercurial ne tenga traccia. Dato che l’operazione di copia non ha effetti sul file originale, Mercurial non agisce in alcun modo su quel file.

$ hg copy file nuovo-file
$ hg revert nuovo-file
$ hg status
? nuovo-file

Gestire i cambiamenti inseriti

Considerate il caso in cui avete inserito un cambiamento a e subito dopo un altro cambiamento b basato sul precedente, poi realizzate che il cambiamento a era sbagliato. Mercurial vi consente di «ritirare» sia un intero changeset automaticamente, sia certi «mattoni da costruzione» che vi permettono di invertire a mano parte di un changeset.

Prima di leggere questa sezione, c’è una cosa che dovete tenere a mente: il comando hg backout annulla gli effetti di un cambiamento effettuando un’aggiunta alla cronologia del vostro repository invece di modificarla o di eliminarne una parte. È lo strumento giusto da usare se state correggendo un bug, ma non se state cercando di annullare qualche cambiamento che potrebbe avere conseguenze catastrofiche. Per trattare con questi ultimi, leggete la sezione chiamata «Modifiche che non avrebbero mai dovuto essere fatte».

Ritirare un changeset

Il comando hg backout vi consente di «annullare» gli effetti di un intero changeset in modo automatico. Dato che la cronologia di Mercurial è immutabile, questo comando non si sbarazza del changeset che volete annullare, ma crea un nuovo changeset che inverte l’effetto del changeset da annullare.

Le operazioni del comando hg backout sono un po’ intricate, quindi le illustreremo con alcuni esempi. Per prima cosa, creiamo un repository con alcuni semplici cambiamenti.

$ hg init miorepo
$ cd miorepo
$ echo prima modifica >> miofile
$ hg add miofile
$ hg commit -m "Prima modifica."
$ echo seconda modifica >> miofile
$ hg commit -m "Seconda modifica."

Il comando hg backout prende come argomento un singolo identificatore di changeset che indica il changeset da annullare. Normalmente, hg backout vi presenterà un editor di testo per farvi scrivere un messaggio di commit, in modo che possiate registrare il motivo per cui state ritirando il cambiamento. In questo esempio, forniremo un messaggio di commit sulla riga di comando usando l’opzione -m.

Ritirare il changeset di punta

Cominceremo ritirando l’ultimo changeset che abbiamo inserito.

$ hg backout -m "Ritira la seconda modifica." tip
ripristino miofile
il changeset 2:a4abdbaa35f1 ritira il changeset 1:51969da8cdc5
$ cat miofile
prima modifica

Potete vedere che la seconda riga di miofile non è più presente. Un’occhiata all’elenco generato da hg log ci dà un’idea di quello che il comando hg backout ha fatto.

$ hg log --style compact
2[tip]   a4abdbaa35f1   2009-06-05 15:48 +0000   bos
  Ritira la seconda modifica.

1   51969da8cdc5   2009-06-05 15:48 +0000   bos
  Seconda modifica.

0   efd26e99cc79   2009-06-05 15:48 +0000   bos
  Prima modifica.

Notate che il nuovo changeset creato da hg backout è un figlio del changeset che abbiamo ritirato. Questo è più facile da vedere nella Figura 9.1, «Ritirare un cambiamento tramite il comando hg backout», che mostra una rappresentazione grafica della cronologia dei cambiamenti. Come potete vedere, la cronologia è gradevolmente lineare.

Figura 9.1. Ritirare un cambiamento tramite il comando hg backout

XXX add text

Ritirare un changeset diverso dalla punta

Se volete ritirare un cambiamento diverso dall’ultimo che avete inserito, passate l’opzione --merge al comando hg backout.

$ cd ..
$ hg clone -r1 miorepo repo-non-di-punta
richiedo tutte le modifiche
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 2 changeset con 2 cambiamenti a 1 file
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd repo-non-di-punta

Questo rende il ritiro di qualsiasi changeset una «singola» operazione che di solito è semplice e veloce.

$ echo terza modifica >> miofile
$ hg commit -m "Terza modifica."
$ hg backout --merge -m "Ritira la seconda modifica." 1
ripristino miofile
creata una nuova testa
il changeset 3:af281715005d ritira il changeset 1:51969da8cdc5
unisco con il changeset 3:af281715005d
unisco miofile
0 file aggiornati, 1 file uniti, 0 file rimossi, 0 file irrisolti
(unione tra rami, ricordatevi di eseguire il commit)

Se date un’occhiata al contenuto di miofile dopo che l’operazione di ritiro si è conclusa, vedrete che il primo e il terzo cambiamento sono presenti, ma non il secondo.

$ cat miofile
prima modifica
terza modifica

Come illustrato nella rappresentazione grafica della cronologia nella Figura 9.2, «Ritiro automatico di un changeset diverso dalla punta tramite il comando hg backout», Mercurial inserisce ancora un cambiamento in questo tipo di situazione (il nodo rettangolare è quello che Mercurial inserisce automaticamente) ma il grafo delle revisioni ora è diverso. Prima di cominciare il processo di ritiro, Mercurial mantiene in memoria l’identità del genitore corrente della directory di lavoro. Poi ritira il changeset indicato e inserisce quel genitore come un changeset. Infine, incorpora il genitore precedente della directory di lavoro, ma notate che non esegue il commit dei risultati dell’unione. Il repository ora contiene due teste e la directory di lavoro contiene i risultati di un’unione.

Figura 9.2. Ritiro automatico di un changeset diverso dalla punta tramite il comando hg backout

XXX add text

Il risultato è che siete tornati «indietro a dove eravate», ma con una parte aggiuntiva di cronologia che annulla gli effetti del changeset che volevate ritirare.

Potreste chiedervi perché Mercurial non effettua il commit dei risultati dell’unione che ha eseguito. Il motivo è che Mercurial si comporta in maniera conservativa: di solito un’unione ha maggiori probabilità di contenere errori rispetto all’annullamento degli effetti del changeset di punta, quindi il vostro lavoro si troverà più al sicuro se prima ispezionate (e verificate!) i risultati dell’unione e solo poi li inserite nel repository.

Usate sempre l’opzione --merge

In effetti, dato che l’opzione --merge farà la «cosa giusta» a prescindere dal fatto che il changeset sia quello di punta o meno (cioè non cercherà di eseguire un’unione se state ritirando la punta, dato che non ce n’è bisogno), dovreste usare sempre questa opzione quando invocate il comando hg backout.

Controllare meglio il processo di ritiro

Sebbene vi abbia raccomandato di usare sempre l’opzione --merge quando ritirate un cambiamento, il comando hg backout vi permette di decidere come incorporare un changeset ritirato. Avrete raramente bisogno di controllare il processo di ritiro a mano, ma potrebbe essere utile capire quello che il comando hg backout fa per voi automaticamente. Per illustrare queste operazioni, cloniamo il nostro primo repository, ma omettiamo il cambiamento ritirato che contiene.

$ cd ..
$ hg clone -r1 miorepo nuovorepo
richiedo tutte le modifiche
aggiungo i changeset
aggiungo i manifest
aggiungo i cambiamenti ai file
aggiunti 2 changeset con 2 cambiamenti a 1 file
aggiorno la directory di lavoro
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ cd nuovorepo

Come nel nostro esempio precedente, inseriremo un terzo changeset, poi ritireremo il suo genitore e vedremo cosa succede.

$ echo terza modifica >> miofile
$ hg commit -m "Terza modifica."
$ hg backout -m "Ritira la seconda modifica." 1
ripristino miofile
creata una nuova testa
il changeset 3:f3e79edd2b05 ritira il changeset 1:51969da8cdc5
il changeset che ha compiuto il ritiro è una nuova testa - ricordatevi di unire
(usate "backout --merge" se volete unire automaticamente)

Il nostro nuovo changeset è ancora un discendente del changeset che abbiamo ritirato e quindi è una nuova testa, non un discendente di quello che era il changeset di punta. Il comando hg backout è stato piuttosto esplicito nel farcelo notare.

$ hg log --style compact
3[tip]:1   f3e79edd2b05   2009-06-05 15:48 +0000   bos
  Ritira la seconda modifica.

2   629da9559a9e   2009-06-05 15:48 +0000   bos
  Terza modifica.

1   51969da8cdc5   2009-06-05 15:48 +0000   bos
  Seconda modifica.

0   efd26e99cc79   2009-06-05 15:48 +0000   bos
  Prima modifica.

Ancora una volta, è facile vedere quello che è successo osservando il grafo della cronologia delle revisioni nella Figura 9.3, «Ritirare un cambiamento tramite il comando hg backout». Questo grafo chiarifica che quando usiamo hg backout per ritirare un cambiamento diverso dalla punta, Mercurial aggiunge una nuova testa al repository (il cambiamento inserito ha la forma di un rettangolo).

Figura 9.3. Ritirare un cambiamento tramite il comando hg backout

XXX add text

Dopo che il comando hg backout ha terminato, lascia il nuovo changeset «ritirato» come genitore della directory di lavoro.

$ hg parents
changeset:   2:629da9559a9e
user:        Bryan O'Sullivan <[email protected]>
date:        Fri Jun 05 15:48:46 2009 +0000
summary:     Terza modifica.

Ora abbiamo due insiemi isolati di cambiamenti.

$ hg heads
changeset:   3:f3e79edd2b05
tag:         tip
parent:      1:51969da8cdc5
user:        Bryan O'Sullivan <[email protected]>
date:        Fri Jun 05 15:48:47 2009 +0000
summary:     Ritira la seconda modifica.

changeset:   2:629da9559a9e
user:        Bryan O'Sullivan <[email protected]>
date:        Fri Jun 05 15:48:46 2009 +0000
summary:     Terza modifica.

Come mostrato dal grafo della cronologia, il ritiro del secondo cambiamento è stato introdotto come una testa separata, perciò il contenuto della nostra directory di lavoro non è cambiato rispetto al changeset che ha apportato il terzo cambiamento. Questa situazione viene confermata dal contenuto di miofile, che presenta tutte e tre le modifiche effettuate.

$ cat miofile
prima modifica
seconda modifica
terza modifica

Per ottenere solamente il primo e il terzo cambiamento nel file, ci basta eseguire una normale unione tra le nostre due teste.

$ hg merge
unisco miofile
0 file aggiornati, 1 file uniti, 0 file rimossi, 0 file irrisolti
(unione tra rami, ricordatevi di eseguire il commit)
$ hg commit -m "Unisce il changeset che ha compiuto il ritiro con la punta precedente."
$ cat miofile
prima modifica
terza modifica

Successivamente, la cronologia del nostro repository può essere rappresentata graficamente come nella Figura 9.4, «Incorporare manualmente un cambiamento ritirato».

Figura 9.4. Incorporare manualmente un cambiamento ritirato

XXX add text

Perché hg backout funziona in questo modo

Ecco una breve descrizione del funzionamento del comando hg backout.

  1. Si assicura che la directory di lavoro sia «pulita», cioè che l’elenco generato da hg status sia vuoto.

  2. Memorizza il genitore corrente della directory di lavoro. Chiamiamo orig questo changeset.

  3. Esegue l’equivalente di un’invocazione di hg update per sincronizzare la directory di lavoro con il changeset che volete ritirare. Chiamiamo backout questo changeset.

  4. Trova il genitore di quel changeset. Chiamiamo parent questo changeset.

  5. Per ogni file su cui il changeset backout ha avuto effetto, esegue l’equivalente del comando hg revert -r parent sul file per ripristinare il contenuto che aveva prima che quel changeset venisse inserito.

  6. Esegue il commit del risultato come un nuovo changeset che ha backout come genitore.

  7. Se specificate l’opzione --merge sulla riga di comando, esegue un’unione con orig ma non inserisce i risultati dell’unione nel repository.

In alternativa, sarebbe possibile implementare hg backout utilizzando hg export per esportare il changeset da ritirare sotto forma di diff e poi impiegando l’opzione --reverse del comando patch per invertire l’effetto del cambiamento senza gingillarsi con la directory di lavoro. Questo procedimento sembra molto più semplice, ma non funzionerebbe affatto altrettanto bene.

Il comando hg backout esegue un aggiornamento, un inserimento, un’unione e un altro inserimento per dare al meccanismo di unione la possibilità di fare il miglior lavoro possibile nel gestire tutte le modifiche avvenute tra il cambiamento che state ritirando e la revisione di punta corrente.

Se state ritirando un cambiamento che si trova 100 revisioni indietro nella cronologia del vostro progetto, le probabilità che il comando patch sia in grado di applicare un diff invertito in maniera pulita non sono molto alte, perché i cambiamenti intercorsi avranno probabilmente «rovinato il contesto» utilizzato da patch per determinare se può applicare una patch (se questo vi sembra incomprensibile, leggete la sezione chiamata «Capire le patch» per una discussione sul comando patch). In più, il meccanismo di unione di Mercurial riesce a gestire i cambiamenti di nome e di permessi per file e directory e le modifiche ai file binari, mentre patch non è in grado di farlo.

Modifiche che non avrebbero mai dovuto essere fatte

Quasi sempre, il comando hg backout è esattamente quello che vi serve se volete annullare gli effetti di un cambiamento. Il comando lascia una registrazione permanente di quello che avete fatto, sia quando avete inserito il changeset originale che quando avete successivamente rimesso in ordine.

In rare occasioni, comunque, potreste scoprire di aver inserito un cambiamento che non dovrebbe essere presente nel repository proprio per niente. Per esempio, sarebbe molto inusuale, e di solito considerato un errore, inserire in un repository i file oggetto di un progetto software insieme ai suoi file sorgente. I file oggetto non hanno praticamente alcun valore intrinseco e sono grandi, quindi aumentano la dimensione del repository e il tempo necessario a clonarlo o a estrarne i cambiamenti.

Prima di illustrare le opzioni a vostra disposizione se eseguite il commit di un cambiamento «da sacchetto di carta marrone» (quel tipo di modifiche talmente infelici che vorreste nascondere la testa in un sacchetto di carta marrone), lasciatemi discutere alcuni approcci che probabilmente non funzioneranno.

Dato che Mercurial tratta la cronologia in maniera cumulativa—ogni cambiamento si basa su tutti i cambiamenti che lo precedono—in genere non è possibile far semplicemente sparire i cambiamenti disastrosi. L’unica eccezione capita quando avete appena inserito una modifica che non è ancora stata propagata verso qualche altro repository. In questo caso, potete tranquillamente usare il comando hg rollback, come descritto nella sezione chiamata «Abortire una transazione».

Dopo che avete trasmesso un cambiamento sbagliato a un altro repository, potreste ancora usare hg rollback per far scomparire la vostra copia locale del cambiamento, ma questa azione non avrà le conseguenze che volete. Il cambiamento sarà ancora presente nel repository remoto, quindi riapparirà nel vostro repository locale la prossima volta che effettuerete un’estrazione.

Se vi trovate in una situazione come questa e sapete quali sono i repository verso cui si è propagato il vostro cambiamento sbagliato, potete provare a sbarazzarvi del cambiamento in ognuno di quei repository. Questa, naturalmente, non è una soluzione soddisfacente: se tralasciate anche un singolo repository quando state ripulendo, il cambiamento sarà ancora «là fuori» e potrebbe propagarsi ulteriormente.

Se avete inserito uno o più cambiamenti dopo il cambiamento che vorreste veder sparire, le vostre opzioni si riducono ulteriormente. Mercurial non offre alcun modo per «fare un buco» nella cronologia lasciando gli altri changeset intatti.

Ritirare un’unione

Dato che le unioni sono spesso complicate, si sono sentiti casi di unioni gravemente rovinate, ma i cui risultati sono stati erroneamente inseriti in un repository. Mercurial fornisce un’importante protezione contro le unioni sbagliate rifiutandosi di eseguire il commit di file irrisolti, ma l’ingenuità umana garantisce che sia ancora possibile mettere sottosopra un’unione e registrarne i risultati.

Di solito, il modo migliore per affrontare la registrazione di un’unione sbagliata è semplicemente quello di provare a riparare il danno a mano. Un completo disastro che non possa venire corretto a mano dovrebbe essere molto raro, ma il comando hg backout può aiutare a rendere la pulizia più semplice attraverso l’opzione --parent, che vi consente di specificare a quale genitore tornare quando state ritirando un’unione.

Figura 9.5. Un’unione sbagliata

XXX add text

Supponete di avere un grafo delle revisioni simile a quello della Figura 9.5, «Un’unione sbagliata». Ci piacerebbe rifare l’unione tra le revisioni 2 e 3.

Potremmo eseguire questa operazione nel modo seguente.

  1. Invocare hg backout --rev=4 --parent=2. Questo dice al comando hg backout di ritirare la revisione 4, che è l’unione sbagliata, e di scegliere il genitore 2, uno dei genitori dell’unione, al momento di decidere quale revisione preferire. L’effetto del comando può essere visto nella Figura 9.6, «Ritirare l’unione favorendo un genitore».

    Figura 9.6. Ritirare l’unione favorendo un genitore

    XXX add text

  2. Invocare hg backout --rev=4 --parent=3. Questo dice al comando hg backout di ritirare ancora la revisione 4, ma questa volta scegliendo il genitore 3, l’altro genitore dell’unione. Il risultato è visibile nella Figura 9.7, «Ritirare l’unione favorendo l’altro genitore», in cui il repository ora contiene tre teste.

    Figura 9.7. Ritirare l’unione favorendo l’altro genitore

    XXX add text

  3. Rifare l’unione sbagliata unendo le due teste generate dai ritiri, riducendo quindi a due il numero di teste nel repository, come si può vedere nella Figura 9.8, «Unire i risultati dei ritiri».

    Figura 9.8. Unire i risultati dei ritiri

    XXX add text

  4. Eseguire un’unione con il commit che è stato eseguito dopo l’unione sbagliata, come mostrato nella Figura 9.9, «Unire i risultati dei ritiri».

    Figura 9.9. Unire i risultati dei ritiri

    XXX add text

Proteggervi dai cambiamenti che vi sono «sfuggiti»

Se avete inserito alcuni cambiamenti nel vostro repository locale e li avete propagati da qualche altra parte, questo non è necessariamente un disastro. Potete proteggervi prevenendo la comparsa di alcuni tipi di changeset sbagliati. Questo è particolarmente facile se di solito il vostro gruppo di lavoro estrae i cambiamenti da un repository centrale.

Configurando alcuni hook su quel repository per convalidare i changeset in entrata (si veda il Capitolo 10, Usare gli hook per gestire gli eventi nei repository), potete automaticamente evitare che alcuni tipi di changeset sbagliati compaiano nel repository centrale. Con una tale configurazione, alcuni tipi di changeset sbagliati tenderanno naturalmente a «estinguersi» perché non possono propagarsi verso il repository centrale. Ancora meglio, questo accade senza alcun bisogno di un intervento esplicito.

Per esempio, un hook sui cambiamenti in entrata programmato per verificare che un changeset si possa effettivamente compilare è in grado di prevenire involontari «guasti» al processo di assemblaggio.

Cosa fare con i cambiamenti sensibili che sfuggono

Persino un progetto gestito con attenzione può subire eventi sfortunati come il commit di un file che contiene password importanti e la sua incontrollata propagazione.

Se qualcosa del genere dovesse accadervi e le informazioni che vengono accidentalmente propagate fossero davvero sensibili, il vostro primo passo dovrebbe essere quello di mitigare l’effetto della perdita senza cercare di controllare la perdita stessa. Se non siete sicuri al 100% di sapere esattamente chi può aver visto i cambiamenti, dovreste immediatamente cambiare le password, cancellare le carte di credito, o trovare qualche altro modo per assicurarvi che le informazioni trapelate non siano più utili. In altre parole, assumete che il cambiamento si sia propagato in lungo e in largo e che non ci sia più niente che potete fare.

Potreste sperare che ci sia qualche meccanismo da usare per scoprire chi ha visto un cambiamento o per cancellare il cambiamento permanentemente e ovunque, ma ci sono buone ragioni per cui queste operazioni non sono possibili.

Mercurial non fornisce una traccia registrata di chi ha estratto i cambiamenti da un repository, perché di solito questa informazione è impossibile da raccogliere o è facile da falsificare. In un ambiente multi-utente o di rete, dovreste quindi dubitare fortemente di voi stessi se pensate di aver identificato ogni luogo in cui un cambiamento sensibile si è propagato. Non dimenticate che le persone possono spedire allegati via email, salvare i propri dati su altri computer tramite il software di backup, trasportare i repository su chiavi USB e trovare altri modi completamente innocenti di confondere i vostri tentativi di rintracciare ogni copia di un cambiamento problematico.

In più, Mercurial non vi fornisce un modo per far completamente sparire un changeset dalla cronologia perché non c’è alcun modo di imporre la sua sparizione, dato che qualcuno potrebbe facilmente modificare la propria copia di Mercurial per ignorare quelle direttive. E poi, se anche Mercurial fornisse questa funzione, qualcuno che semplicemente non abbia estratto il changeset che «fa sparire questo file» non ne godrebbe gli effetti, né lo farebbero i programmi di indicizzazione web che visitano un repository al momento sbagliato, i backup del disco, o altri meccanismi. In effetti, nessun sistema distribuito di controllo di revisione può far sparire dati in maniera affidabile. Dare l’illusione di un controllo di questo tipo potrebbe facilmente conferirvi un falso senso di sicurezza, peggiorando le cose rispetto a non darvela affatto.

Trovare la causa di un bug

Essere in grado di ritirare un changeset che ha introdotto un bug va benissimo, ma richiede che sappiate quale changeset va ritirato. Mercurial offre un inestimabile comando, chiamato hg bisect, che vi aiuta ad automatizzare questo processo e a completarlo in maniera molto efficiente.

L’idea alla base del comando hg bisect è che un changeset abbia introdotto una modifica di comportamento che potete identificare con un semplice test binario di successo o fallimento. Non sapete quale porzione di codice ha introdotto il cambiamento, ma sapete come verificare la presenza del bug. Il comando hg bisect usa il vostro test per dirigere la propria ricerca del changeset che ha introdotto il codice che ha causato il bug.

Ecco alcuni scenari di esempio per aiutarvi a capire come potreste applicare questo comando.

  • La versione più recente del vostro software ha un bug che non ricordate fosse presente alcune settimane prima, ma non sapete quando il bug è stato introdotto. Qui, il vostro test binario controlla la presenza di quel bug.

  • Avete corretto un bug in tutta fretta e ora è il momento di chiudere la relativa voce nel database dei bug del vostro gruppo. Il database dei bug richiede un identificatore di changeset quando chiudete una voce, ma non ricordate in quale changeset avete corretto il bug. Ancora una volta, il vostro test binario controlla la presenza del bug.

  • Il vostro software funziona correttamente, ma è più lento del 15% rispetto all’ultima volta che avete compiuto questa misurazione. Volete sapere quale changeset ha introdotto il calo di prestazioni. In questo caso, il vostro test binario misura le prestazioni del vostro software per vedere se è «veloce» o «lento».

  • La dimensione dei componenti del progetto che rilasciate è esplosa di recente e sospettate che qualcosa sia cambiato nel modo in cui assemblate il progetto.

Da questi esempi, dovrebbe essere chiaro che il comando hg bisect non è utile solo per trovare le cause dei bug. Potete usarlo per trovare qualsiasi «proprietà emergente» di un repository (qualsiasi cosa che non potete trovare con una semplice ricerca di testo sui file contenuti nell’albero) per la quale sia possibile scrivere un test binario.

Ora introdurremo un po’ di terminologia, giusto per chiarire quali sono le parti del processo di ricerca di cui siete responsabili voi e quali sono quelle di cui è responsabile Mercurial. Un test è qualcosa che voi eseguite quando hg bisect sceglie un changeset. Una sonda è ciò che hg bisect esegue per dirvi se una revisione è buona. Infine, useremo la parola «bisezione» per intendere la «ricerca tramite il comando hg bisect».

Un modo semplice per automatizzare il processo di ricerca sarebbe quello di collaudare semplicemente tutti i changeset. Tuttavia, questo approccio è scarsamente scalabile. Se ci volessero dieci minuti per collaudare un singolo changeset e il vostro repository contenesse 10.000 changeset, l’approccio completo impiegherebbe una media di 35 giorni per trovare il changeset che ha introdotto un bug. Anche se sapeste che il bug è stato introdotto in uno degli ultimi 500 changeset e limitaste la ricerca a quelli, dovrebbero trascorrere più di 40 ore per trovare il changeset che ha introdotto il vostro bug.

Il comando hg bisect invece usa la propria conoscenza della «forma» della cronologia delle revisioni del vostro progetto per effettuare una ricerca in tempo proporzionale al logaritmo del numero dei changeset da controllare (il tipo di ricerca che esegue viene chiamata ricerca dicotomica). Con questo approccio, la ricerca attraverso 10.000 changeset impiegherà meno di 3 ore, anche a 10 minuti per test (la ricerca richiederà circa 14 test). Limitate la vostra ricerca agli ultimi cento changeset e il tempo impiegato sarà solo circa un’ora (approssimativamente sette test).

Il comando hg bisect è consapevole della natura «ramificata» della cronologia delle revisioni di un progetto Mercurial, quindi non ha problemi a trattare con rami, unioni, o molteplici teste in un repository. Opera in maniera così efficiente perché è in grado di potare interi rami di cronologia con una singola sonda.

Usare il comando hg bisect

Ecco un esempio di hg bisect in azione.

[Nota]Nota

Fino alla versione 0.9.5 di Mercurial compresa, hg bisect non era uno dei comandi principali, ma veniva distribuito con Mercurial sotto forma di estensione. Questa sezione descrive il comando predefinito, non la vecchia estensione.

Ora creiamo un nuovo repository in modo che possiate provare il comando hg bisect in isolamento.

$ hg init miobug
$ cd miobug

Simuleremo un progetto con un bug in modo molto semplice: creiamo cambiamenti elementari in un ciclo e designamo uno specifico cambiamento che conterrà il «bug». Questo ciclo crea 35 changeset, ognuno dei quali aggiunge un singolo file al repository. Rappresenteremo il nostro «bug» con un file che contiene il testo «ho un gub».

$ cambiamento_difettoso=22
$ for (( i = 0; i < 35; i++ )); do
>   if [[ $i = $cambiamento_difettoso ]]; then
>     echo 'ho un gub' > miofile$i
>     hg commit -q -A -m "Changeset difettoso."
>   else
>     echo 'niente da vedere, circolate' > miofile$i
>     hg commit -q -A -m "Changeset normale."
>   fi
> done

La prossima cosa che vorremmo fare è capire come usare il comando hg bisect. Possiamo usare il normale meccanismo di aiuto predefinito di Mercurial per fare questo.

$ hg help bisect
hg bisect [-gbsr] [-c CMD] [REV]

ricerca di changeset tramite suddivisioni

    Questo comando vi aiuta a cercare changeset che introducono problemi.
    Per usarlo, contrassegnate il changeset più recente che esibisce il
    problema come guasto, poi contrassegnate l'ultimo changeset libero dal
    problema come funzionante. La bisezione aggiornerà la vostra directory di
    lavoro a una revisione di collaudo (a meno che l'opzione --noupdate sia
    specificata). Una volta che avete effettuato i test, contrassegnate la
    directory di lavoro come guasta o funzionante e bisect la aggiornerà a
    un altro changeset candidato o vi comunicherà di aver trovato la revisione
    guasta.

    Come scorciatoia, potete anche usare l'argomento revisione per
    contrassegnare una revisione come funzionante o guasta senza prima
    controllarla.

    Se fornite un comando, verrà usato per operare la bisezione in modo
    automatico. Lo stato di uscita del comando verrà usato come indicatore per
    contrassegnare una revisione come guasta o funzionante. Nel caso lo stato
    di uscita sia 0 la revisione viene contrassegnata come funzionante; 125 -
    viene saltata; 127 (comando non trovato) - la bisezione verrà bloccata;
    qualsiasi altro stato maggiore di zero contrassegnerà la revisione come
    guasta.

opzioni:

 -r --reset     inizializza lo stato della bisezione
 -g --good      contrassegna un changeset come funzionante
 -b --bad       contrassegna un changeset come guasto
 -s --skip      salta il collaudo del changeset
 -c --command   usa un comando per controllare lo stato del changeset
 -U --noupdate  non aggiorna alla revisione da collaudare

usate "hg -v help bisect" per vedere le opzioni globali

Il comando hg bisect lavora in più passi. Ogni passo procede nella maniera seguente.

  1. Eseguite il vostro test binario.

    • Se il test ha avuto successo, informate hg bisect invocando il comando hg bisect --good.

    • Se il test è fallito, invocate il comando hg bisect --bad.

  2. Il comando usa le vostre informazioni per decidere quale changeset collaudare successivamente.

  3. Il comando aggiorna la directory di lavoro a quel changeset e il processo ricomincia da capo.

Il processo termina quando hg bisect identifica un unico cambiamento che indica il punto in cui il vostro test passa dallo stato di «successo» a quello di «fallimento».

Per cominciare la ricerca, dobbiamo eseguire il comando hg bisect --reset.

$ hg bisect --reset

Nel nostro caso, il test binario che usiamo è semplice: controlliamo il repository per vedere se qualche file contiene la stringa «ho un gub». Se è così, questo changeset contiene il cambiamento che ha «causato il bug». Per convenzione, un changeset che ha la proprietà che stiamo cercando è «guasto», mentre uno che non ce l’ha è «funzionante».

Quasi sempre, la revisione su cui la directory di lavoro è sincronizzata (di solito, la punta) esibisce già il problema introdotto dal cambiamento malfunzionante, quindi la indicheremo come «guasta».

$ hg bisect --bad

Il nostro compito successivo consiste nel nominare un changeset che sappiamo non contenere il bug, in modo che il comando hg bisect possa «circoscrivere» la ricerca tra il primo changeset funzionante e il primo changeset guasto. Nel nostro caso, sappiamo che la revisione 10 non conteneva il bug. (Spiegherò meglio come scegliere il primo changeset «funzionante» più avanti.)

$ hg bisect --good 10
Collaudo il changeset 22:b8789808fc48 (24 changeset rimanenti, ~4 test)
0 file aggiornati, 0 file uniti, 12 file rimossi, 0 file irrisolti

Notate che questo comando ha stampato alcune informazioni.

  • Ci ha detto quanti changeset deve considerare prima di poter identificare quello che ha introdotto il bug e quanti test saranno richiesti dal processo.

  • Ha aggiornato la directory di lavoro al prossimo changeset da collaudare e ci ha detto quale changeset sta collaudando.

Ora eseguiamo il nostro test nella directory di lavoro, usando il comando grep per vedere se il nostro file «guasto» è presente. Se c’è, questa revisione è guasta, altrimenti è funzionante.

$ if grep -q 'ho un gub' *
> then
>   risultato=bad
> else
>   risultato=good
> fi
$ echo il contrassegno di questa revisione è: $risultato
il contrassegno di questa revisione è bad
$ hg bisect --$risultato
Collaudo il changeset 16:e61fdddff53e (12 changeset rimanenti, ~3 test)
0 file aggiornati, 0 file uniti, 6 file rimossi, 0 file irrisolti

Questo test sembra un perfetto candidato per l’automazione, quindi trasformiamolo in una funzione di shell.

$ miotest() {
$   if grep -q 'ho un gub' *
>   then
>     contrassegno=bad
>     risultato=guasta
>   else
>     contrassegno=good
>     risultato=funzionante
>   fi
>   echo questa revisione è $risultato
>   hg bisect --$contrassegno
> }

Ora possiamo eseguire un intero passo di collaudo con il singolo comando miotest.

$ miotest
questa revisione è funzionante
Collaudo il changeset 19:706df39b003b (6 changeset rimanenti, ~2 test)
3 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti

Ancora qualche altra invocazione del comando che abbiamo preparato per il passo di collaudo e abbiamo finito.

$ miotest
questa revisione è funzionante
Collaudo il changeset 20:bf7ea9a054e6 (3 changeset rimanenti, ~1 test)
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ miotest
questa revisione è funzionante
Collaudo il changeset 21:921391dd45c1 (2 changeset rimanenti, ~1 test)
1 file aggiornati, 0 file uniti, 0 file rimossi, 0 file irrisolti
$ miotest
questa revisione è funzionante
La prima revisione guasta è:
changeset:   22:b8789808fc48
utente:      Bryan O'Sullivan <[email protected]>
data:        Tue May 05 06:55:14 2009 +0000
sommario:    Changeset difettoso.

Anche se avevamo 40 changeset attraverso cui cercare, il comando hg bisect ci ha permesso di trovare il changeset che ha introdotto il nostro «bug» usando solo cinque test. Dato che il numero di test effettuati dal comando hg bisect cresce con il logaritmo del numero dei changeset da analizzare, il vantaggio che ha rispetto a una ricerca che usa la strategia della «forza bruta» aumenta con ogni changeset che aggiungete.

Riordinare dopo la vostra ricerca

Quando avete finito di usare il comando hg bisect in un repository, potete invocare il comando hg bisect --reset in modo da scartare le informazioni che venivano usate per guidare la vostra ricerca. Il comando non usa molto spazio, quindi non è un problema se vi dimenticate di effettuare questa invocazione. Tuttavia, hg bisect non vi permetterà di cominciare una nuova ricerca in quel repository fino a quando non avrete eseguito hg bisect --reset.

$ hg bisect --reset

Suggerimenti per trovare efficacemente i bug

Fornite informazioni consistenti

Il comando hg bisect vi richiede di indicare correttamente il risultato di ogni test che eseguite. Se gli dite che un test è fallito quando in realtà ha avuto successo, il comando potrebbe essere in grado di scoprire l’inconsistenza. Se riesce a identificare un’inconsistenza nei vostri resoconti, vi dirà che un particolare changeset è sia funzionante che guasto. Tuttavia, non è in grado di farlo perfettamente ed è ugualmente probabile che vi restituisca il changeset sbagliato come causa del bug.

Automatizzate il più possibile

Quando ho cominciato a usare il comando hg bisect, ho provato a eseguire alcune volte i miei test a mano sulla riga di comando. Questo non è l’approccio adatto, almeno per me. Dopo alcune prove, mi sono accorto che stavo facendo abbastanza errori da dover ricominciare le mie ricerche diverse volte prima di riuscire a ottenere i risultati corretti.

I miei problemi iniziali nel guidare a mano il comando hg bisect si sono verificati anche con ricerche semplici su repository di piccole dimensioni, ma se il problema che state cercando è più sottile, o se il numero di test che hg bisect deve eseguire aumenta, la probabilità che un errore umano rovini la ricerca è molto più alta. Una volta che ho cominciato ad automatizzare i miei test, ho ottenuto risultati molto migliori.

La chiave del collaudo automatizzato è duplice:

  • verificate sempre lo stesso sintomo, e

  • fornite sempre informazioni consistenti al comando hg bisect.

Nel mio esempio precedente, il comando grep verifica il sintomo e l’istruzione if usa il risultato di questo controllo per assicurarsi di fornire la stessa informazione al comando hg bisect. La funzione miotest ci permette di riprodurre insieme queste due operazioni, in modo che ogni test sia uniforme e consistente.

Controllate i vostri risultati

Dato che il risultato di una ricerca con hg bisect è solo tanto buono quanto le informazioni che passate al comando, non prendete il changeset che vi indica come la verità assoluta. Un modo semplice di effettuare un riscontro sul risultato è quello di eseguire manualmente il vostro test su ognuno dei changeset seguenti.

  • Il changeset che il comando riporta come la prima revisione guasta. Il vostro test dovrebbe verificare che la revisione sia effettivamente guasta.

  • Il genitore di quel changeset (entrambi i genitori, se è un’unione). Il vostro test dovrebbe verificare che quel changeset sia funzionante.

  • Un figlio di quel changeset. Il vostro test dovrebbe verificare che quel changeset sia guasto.

Fate attenzione alle interferenze tra i bug

È possibile che la vostra ricerca di un bug venga rovinata dalla presenza di un altro bug. Per esempio, diciamo che il vostro software si blocca alla revisione 100 e funziona correttamente alla revisione 50. Senza che voi lo sappiate, qualcun altro ha introdotto un diverso bug bloccante alla revisione 60 e lo ha corretto alla revisione 80. Questo potrebbe distorcere i vostri risultati in vari modi.

È possibile che questo altro bug «mascheri» completamente il vostro, cioè che sia comparso prima che il vostro bug abbia avuto la possibilità di manifestarsi. Se non potete evitare quell’altro bug (per esempio, impedisce al vostro progetto di venire assemblato) e quindi non potete dire se il vostro bug è presente in un particolare changeset, il comando hg bisect non è in grado di aiutarvi direttamente. Invece, invocando hg bisect --skip potete indicare un changeset come non collaudato.

Potrebbe esserci un problema differente se il vostro test per la presenza di un bug non è abbastanza specifico. Se controllate che «il mio programma si blocca», allora sia il vostro bug bloccante che il bug bloccante non correlato che lo maschera sembreranno la stessa cosa e fuorvieranno hg bisect.

Un’altra situazione utile per sfruttare hg bisect --skip è quella in cui non potete collaudare una revisione perché il vostro progetto era guasto e quindi in uno stato non collaudabile in quella revisione, magari perché qualcuno aveva introdotto un cambiamento che impediva al progetto di venire assemblato.

Circoscrivete la vostra ricerca in maniera approssimativa

Scegliere il primo changeset «funzionante» e il primo changeset «guasto» che rappresenteranno i punti estremi della vostra ricerca è spesso facile, ma merita comunque una breve discussione. Dal punto di vista di hg bisect, il changeset «più recente» è convenzionalmente «guasto» e il changeset «più vecchio» è «funzionante».

Se avete problemi a ricordare dove trovare un changeset «funzionante» da fornire al comando hg bisect, non potreste fare di meglio che collaudare changeset a caso. Ricordatevi di eliminare i contendenti che non possono esibire il bug (magari perché la funzione con il bug non era ancora presente) e quelli in cui un altro problema nasconde il bug (come ho discusso in precedenza).

Anche se i vostri tentativi si concludono «in anticipo» di migliaia di changeset o di mesi di cronologia, aggiungerete solo una manciata di test al numero totale che hg bisect deve eseguire, grazie al suo comportamento logaritmico.

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.