Git --everything-is-local
Chapters ▾

6.1 Strumenti di Git - Selezione della revisione

Selezione della revisione

Git ti permette di specificare una o più commit in diversi modi. Non sono sempre ovvi, ma è utile conoscerli.

Singole versioni

Puoi fare riferimento a una singola commit usando l’hash SHA-1 attribuito, ma ci sono altri metodi più amichevoli per fare riferimento a una commit. Questa sezione delinea i modi con cui ci si può riferire a una singolo commit.

SHA breve

Git è abbastanza intelligente da capire a quale commit ti riferisci se scrivi i primi caratteri purché il codice SHA-1 sia di almeno quattro caratteri e sia univoco: ovvero che uno solo degli oggetti nel repository inizi con quel SHA-1.

Per vedere per esempio una specifica commit, immagina di eseguire 'git log' e trovi la commit dove sono state aggiunte determinate funzionalità:

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

In questo caso scegli '1c002dd....'. Se vuoi eseguire 'git show' su quella commit, i seguenti comandi sono equivalenti (assumendo che le versioni più brevi siano univoche):

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

Git riesce a capire un valore SHA-1 intero da uno corto, abbreviato. Se usi l’opzione '--abbrev-commit' col comando 'git-log', l'output userà valori più corti ma garantirà che siano unici: di default usa sette caratteri ma ne userà di più se sarà necessario per mantenere l’univocità del valore SHA-1:

$ git log --abbrev-commit --pretty=oneline
ca82a6d changed the version number
085bb3b removed unnecessary test code
a11bef0 first commit

Da otto a dieci caratteri sono, generalmente, più che sufficienti per essere univoci all'interno di un progetto. Uno dei progetti Git più grandi, il kernel di Linux, inizia a necessitare 12 caratteri, dei 40 possibili, per essere univoco.

Una breve nota su SHA-1

Molte persone si preoccupa che a un certo punto, in modo del tutto casuale, ci possano essere due oggetti nel tuo repository che abbiano lo stesso SHA-1. Cosa succederebbe?

Se dovessi committare un oggetto che abbia lo stesso hash SHA-1 di un altro oggetto che sia già nel tuo repository, Git troverà l'altro oggetto già nel database di Git e lo considererà già scritto. Se in seguito vorrai scaricare quest'ultimo ogetto, otterrai sempre le informazioni del più vecchio.

Dovresti comunque essere consapevole che questo sia uno scenario molto improbabile. Il codice SHA-1 è di 20 bytes o 160 bits. Il numero di oggetti casuali necessari perché ci sia la probabilità del 50% di una singola collisione è di circa 2^80 (la formula per determinare la probabilità di collisione è p = (n(n-1)/2) * (1/2^160)). 2^80 è 1.2 x 10^24 ovvero 1 milione di miliardi di miliardi. È 1.200 volte il numero di granelli di sabbia sulla terra.

Ecco un esempio per dare un'idea di cosa ci vorrebbe per ottenere una collisione SHA-1. Se tutti i 6.5 miliardi di esseri umani sulla Terra programmassero e, ogni secondo, ognuno scrivesse codice che sia equivalente all'intera cronologia del kernel Linux (1 milione di oggetti Git) e ne facesse la push su un enorme repository Git, ci vorrebbero 5 anni per contenere abbastanza oggetti in quel repository per avere il 50% di possibilità di una singola collisione di oggetti SHA-1. Esiste una probabilità più alta che ogni membro del tuo gruppo di sviluppo, in incidenti non correlati venga attaccato e ucciso da dei lupi nella stessa notte.

Riferimenti alle diramazioni

Il modo più diretto per specificare una commit è avere una diramazione (branch in inglese) che vi faccia riferimento, che ti permetterebbe di usare il nome della diramazione in qualsiasi comando Git che richieda un oggetto commit o un valore SHA-1. Se per esempio vuoi vedere l'ultima commit di una diramazione, i comandi seguenti sono equivalenti (supponendo che la diramazione 'topic1' punti a 'ca82a6d'):

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

Se vuoi vedere a quale SHA specifico punti una diramazione, o se vuoi vedere a quali SHA puntino questi esempi, puoi usare il comando 'rev-parse' di Git, che fa parte dei comandi sottotraccia (plumbing in inglese) . Nel Capitolo 9 trovi maggiori informazioni sui comandi sottotraccia ma, brevemente, 'rev-parse' esiste per operazioni di basso livello e non è concepito per essere usato nelle operazioni quotidiane. Può comunque essere d'aiuto quando hai bisogno di vedere cosa sta succedendo davvero. Qui puoi quindi eseguire 'rev-parse' sulla tua diramazione.

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

Nomi brevi dei riferimenti

Una delle cose che Git fa dietro le quinte è aggiornare il registro dei riferimenti (reflog in inglese), che registra la posizione dei tuoi riferimenti HEAD e delle diramazione su cui hai lavorato negli ulti mesi.

Puoi consultare il registro con il comando 'git reflog':

$ git reflog
734713b HEAD@{0}: commit: fixed refs handling, added gc auto, updated
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by recursive.
1c002dd HEAD@{2}: commit: added some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD

Ogni volta che una diramazione viene aggiornata per qualsiasi ragione, Git memorizza questa informazione in questa cronologia temporanea. E puoi anche specificare commit più vecchie. Se vuoi vedere la cronologia a partire dalla quintultima commit a partire dalla HEAD del tuo repository, puoi usare il riferimento '@{n}' che vedi nel output del registro:

$ git show HEAD@{5}

Puoi anche usare questa sintassi per vedere dov'era una diramazione a una certa data. Se vuoi vedere, per esempio, dov'era ieri la diramazione 'master' puoi scrivere:

$ git show master@{yesterday}

Che mostra dov'era ieri la diramazione. Questa tecnica funziona solo per i dati che sono ancora nel registri e non puoi quindi usarla per vedere commit più vecchie di qualche mese.

Per vedere le informazioni del registro formattate come l’output di git log, puoi eseguire il comando git log -g:

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: fixed refs handling, added gc auto, updated
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

E' importante notare che l'informazione del registro è solamente locale: è un registro di ciò che hai fatto nel tuo repository. I riferimenti non saranno uguali sui cloni degli altri. Appena dopo aver clonato un repository il tuo registro sarà vuoto perché non è successo ancora nulla nel tuo repository. Potrai eseguire 'git show HEAD@{2.months.ago}' solo se hai clonato il progetto almeno due mesi fa: se è stato clonato cinque minuti fa non otterrai nessun risultato.

Riferimenti ancestrali

L'altro modo principale per specificare una commit è attraverso i suoi ascendenti. Se metti un ^ alla fine di un riferimento, Git lo risolve interpretandolo come il padre padre di quella determinata commit. Immagina di vedere la cronologia del tuo progetto:

$ git log --pretty=format:'%h %s' --graph
* 734713b fixed refs handling, added gc auto, updated tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd added some blame and merge stuff
|/
* 1c36188 ignore *.gem
* 9b29157 add open3_detach to gemspec file list

Puoi quindi vedere la commit precedente specificando HEAD^, che significa "l'ascendente di HEAD":

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

Puoi specificare anche un numero dopo la ^: per esempio d921970^2 significa "il secondo ascendente di d921870." Questa sintassi è utile solo per incorporare delle commit che hanno più di un ascendente. Il primo ascendente è la diramazione dove ti trovi al momento dell'incorporamento, e il secondo è la commit sulla diramazione da cui hai fatto l'incorporamento:

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

Un altro modo di specificare un riferimento ancestrale è la ~. Questo si riferisce anche al primo ascendente, quindi HEAD~ e HEAD^ sono equivalenti. La differenza diventa evidente quando specifichi un numero. HEAD~2 significa "il primo ascendente del primo ascendente", o "il nonno”: attraversa i primi ascendenti il numero di volte specificato. Per esempio, nella cronologia precedente, HEAD~3 sarebbe

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

Che può essere scritto anche come HEAD^^^ che, di nuovo, è sempre il primo genitore del primo genitore del primo genitore:

$ git show HEAD^^^
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

È anche possibile combinare queste sintassi: puoi prendere il secondo genitore del riferimento precedente (assumendo che si tratti di una commit d'incorporamento) usando HEAD~3^2, e così via.

Intervalli di commit

Ora che sai come specificare singole commit, vediamo come specificare intervalli di commit. Ciò è particolarmente utile per gestire le tue diramazioni: se ne hai molte puoi usare gli intervalli per rispondere a domande come “cosa c’è in questa diramazione che non ho ancora incorporato?”

Due punti

Il modo più comune per specificare un intervallo è con i due punti che, praticamente, chiede a Git di risolvere l’intervallo tra commit che sia raggiungibile da una commit, ma non dall’altra. Immaginiamo di avere la cronologia dell’immagine 6-1


Figure 6-1. Esempio di cronologia per la selezione di intervalli.

Vuoi vedere cosa sia nella tua diramazione sperimentale che non sia ancora stato incorporato nella master: puoi chiedere a Git di mostrarti solo il registro delle commit con master..experiment: questo significa “tutte le commit raggiungibili da experiment che non lo siano da master”. Affinché questi esempi siano sintetici ma chiari, invece del registro effettivo di Git, userò le lettere degli oggetti commit del diagramma:

$ git log master..experiment
D
C

Se volessi invece vedere il contrario, ovvero tutte le commit in master che non siano in experiment, puoi invertire i nomi dei branch: experiment..master ti mostra tutto ciò che è in master e che non sia raggiungibile da experiment:

$ git log experiment..master
F
E

Questo è utile se vuoi mantenere aggiornata la diramazione experiment e sapere cosa stai per incorporare. Un’altro caso in cui si usa spesso questa sintassi è quando stai per condividere delle commit verso un repository remoto:

$ git log origin/master..HEAD

Questo comando mostra tutte le commit della tua diramazione che non sono in quella master del tuo repository remoto origin. Se esegui git push quando la tua diramazione attuale è associata a origin/master, le commit elencate da git log origin/master..HEAD saranno quelle che saranno inviate al server. Puoi anche omettere una delle parti della sintassi, e Git assumerà che sia HEAD. Per esempio puoi ottenere lo stesso risultato dell’esempio precedente scrivendo git log origin/master..: Git sostituisce la parte mancante con HEAD.

Punti multipli

La sintassi dei due punti è utile come la stenografia, ma potresti voler specificare più di due branch per indicare la tua revisione, per vedere le commit che sono nelle varie diramazioni che non siano in quella attuale. Git ti permette di farlo sia con ^ che con l’opzione --not prima di ciascun riferimento del quale vuoi vedere le commit raggiungibili. Quindi questi tre comandi sono equivalenti:

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

Questo è interessante, perché con questa sintassi puoi specificare più di due riferimenti nella tua richiesta, cosa che non puoi fare con i due punti. Se per esempio vuoi vedere tutte le commit che siano raggiungibili da refA o da refB ma non da refC puoi usare una delle seguenti alternative:

$ git log refA refB ^refC
$ git log refA refB --not refC

Questo produce un sistema di revisione molto potente che dovrebbe aiutarti a capire cosa c’è nelle tue diramazioni.

Tre punti

L’ultima sintassi per la selezione di intervalli è quella dei tre punti, che indica tutte le commit raggiungibili da uno qualsiasi dei riferimenti ma non da entrambi. Rivedi la cronologia delle commit nella Figura 6-1. Se vuoi vedere cosa ci sia nel master o in experiment ma non i riferimenti comuni, puoi eseguire

$ git log master...experiment
F
E
D
C

Che ti mostra l’output normale del log mostrando solo le informazioni di quelle quattro commit nell'ordinamento cronologico normale.

Un'opzione comunemente usata in questi casi con il comando log è il parametro --left-right, che mostra da che lato dell'intervallo si trovi ciascuna commit dell’intervallo selezionato, che rende le informazioni molto più utili:

$ git log --left-right master...experiment
< F
< E
> D
> C

Con questi strumenti puoi dire facilmente a Git quale o quali commit vuoi ispezionare.