Git --distributed-is-the-new-centralized
Chapters ▾

2.2 Basi di Git - Salvare le modifiche sul repository

Salvare le modifiche sul repository

Hai clonato un vero repository Git e hai la copia di lavoro dei file del progetto. Ora puoi fare qualche modifica e inviare gli snapshots di queste al tuo repository ogni volta che il progetto raggiunga uno stato che vuoi salvare.

Ricorda che ogni file della tua directory di lavoro può essere in uno dei due stati seguenti: tracked (tracciato, ndt.) o untracked (non tracciato, ndt.). I file tracked sono già presenti nell'ultimo snapshot; possono quindi essere unmodified (non modificati, ndt.), modified (modificati, ndt.) o staged. I file untracked sono tutti gli altri: qualsiasi file nella tua directory di lavoro che non è presente nell'ultimo snapshot o nella tua area di stage. Quando cloni per la prima volta un repository, tutti i tuoi file sono tracciati e non modificati perché li hai appena prelevati e non hai modificato ancora niente.

Quando editi dei file, Git li vede come modificati, perché sono cambiati rispetto all'ultima commit. Metti nell'area di stage i file modificati e poi fai la commit di tutto ciò che è in quest'area, e quindi il ciclo si ripete. Questo ciclo di vita è illustrato nella Figura 2-1.


Figura 2-1. Il ciclo di vita dello stato dei tuoi file.

Controlla lo stato dei tuoi file

Lo strumento principale che userai per determinare lo stato dei tuoi file è il comando git status. Se esegui questo comando appena dopo un clone, dovresti vedere qualcosa di simile:

$ git status
# On branch master
nothing to commit, working directory clean

Questo significa che hai una directory di lavoro pulita, ovvero che nessuno dei file tracciati è stato modificato. Inoltre Git non ha trovato nessun file non ancora tracciato, altrimenti sarebbero elencati qui. In aggiunta il comando indica anche in quale ramo sei. Per ora, è sempre master, che è il predefinito; non preoccupartene per ora. Il prossimo capitolo tratterà in dettagli dei branch (ramificazioni) e dei riferimenti.

Immagina di aver aggiunto un nuovo file al tuo progetto, un semplice README. Se il file non esisteva e lanci git status, vedrai così il file non tracciato:

$ vim README
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        README

nothing added to commit but untracked files present (use "git add" to track)

Puoi vedere che il nuovo file README non è tracciato poiché nell'output è nella sezione dal titolo "Untracked files". Untracked significa che Git vede un file che non avevi nello snapshot precedente (commit); Git non lo includerà negli snapshot delle tuoe commit fino a quando non glielo dirai esplicitamente. Fa così per evitare che includa accidentalmente dei file binari generati o qualsiasi altro tipo di file che non intendi includere. Se vuoi includere il README, iniziamo a tracciarlo.

Tracciare Nuovi File

Per iniziare a tracciare un nuovo file, si usa il comando git add. Per tracciare il file README, usa questo comando:

$ git add README

Se lanci nuovamente il comando per lo stato, puoi vedere che il tuo file README ora è tracciato e nell'area di stage:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README

Sai che è nell'area di stage perché è nella sezione "Changes to be committed". Se a questo punto fai commit, la versione del file com'era quando hai lanciato git add sarà quella che troverai nella cronologia dello snapshot. Ricorderai che quando prima hai eseguito git init, poi hai dovuto lanciare git add (file), che era necessario per iniziare a tracciare i file nella tua directory. Il comando git add accetta il nome del percorso di un file o una directory; se è una directory, il comando aggiunge ricorsivamente tutti i file in quella directory.

Fare lo stage dei file modificati

Modifichiamo un file che è già tracciato. Se modifichi un file tracciato chiamato benchmarks.rb e poi esegui il comando status, otterrai qualcosa di simile a:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   benchmarks.rb

Il file benchmarks.rb appare nella sezione chiamata "Changes not staged for commit" — che significa che un file tracciato è stato modificato nella directory di lavoro ma non è ancora nello stage. Per farlo, esegui il comando git add (è un comando multifunzione — lo usi per iniziare a tracciare nuovi file, per fare lo stage dei file e per fare altre cose, ad esempio per segnare come risolti i conflitti causati da un merge). Esegui git add per mettere in stage il file benchmarks.rb, e riesegui git status:

$ git add benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

Entrambi i file sono nello stage e staranno nella prossima commit. A questo punto, immagina che ti sia ricordato di una piccola modifica da fare in 'benchmarks.rb' prima della commit. Riapri il file e fai la modifica: ora sei pronto per la commit. Come sempre, esegui git status di nuovo:

$ vim benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   benchmarks.rb

Cos'è successo? Ora benchmarks.rb è elencato sia dentro che fuori lo stage. Come è possibile? È saltato fuori che Git ha messo in stage il file esattamente com'era quando hai eseguito git add. Se committi ora, la versione di benchmarks.rb che verrà committata sarà quella che avevi quando hai eseguito il git add, non la versione del file che trovi nella directory di lavoro quando esegui git commit. Se modifichi un file dopo che hai eseguito git add, devi rieseguire git add per mettere nello stage l'ultima versione del file:

$ git add benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

Ignorare File

Spesso hai dei file che non vuoi che Git aggiunga automaticamente e nemmeno che te li mostri come tracciati. Generalmente si tratta di file generati automaticamente, come i log o quelli prodotti dal tuoi sistema di build. In questi casi puoi creare un file chiamato .gitignore con la lista di pattern dei file che vuoi ignorare. Questo è un .gitignore d'esempio:

$ cat .gitignore
*.[oa]
*~

La prima riga dice a Git di ignorare qualsiasi file che finisce in .o o .a — file di oggetti o archivi che possono essere il prodotto di una compilazione del tuo codice. La seconda riga dice a Git di ignorare tutti i file che finiscono con tilde (~), che è usata da alcuni editor di testo come Emacs per marcare i file temporanei. Puoi anche includere le directory log, tmp o pid, documenti generati automaticamente e così via. Definire un file .gitignore prima di iniziare generalmente è una buona idea, così eviti il rischio di committare accidentalmente dei file che non vuoi nel tuo repository Git.

Queste sono le regole per i pattern che puoi usare in .gitignore:

  • Le righe vuote o che inizino con # vengono ignorate.
  • Gli standard glob pattern funzionano (http://it.wikipedia.org/wiki/Glob_pattern, ndt).
  • Puoi terminare i pattern con uno slash (/) per indicare una directory.
  • Puoi negare un pattern facendolo iniziare con un punto esclamativo (!).

I glob pattern sono come espressioni regolari semplificate, usate dalla shell. L'asterisco (*) corrisponde a zero o più caratteri; [abc] corrisponde a ogni carattere all'interno delle parentesi (in questo caso a, b, o c); il punto interrogativo (?) corrisponden ad un carattere singolo; e i caratteri all'interno delle parentesi quadre separati dal segno meno ([0-9]) corrispondono ad ogni carattere all'interno dell'intervallo (in questo caso da 0 a 9).

Questo è un altro esempio di file .gitignore:

# un commento - questo è ignorato
# escludi i file .a
*.a
# ma traccia lib.a, sebbene su tu stia ignorando tutti i file `.a`
!lib.a
# ignora solo il TODO nella root, e non subdir/TODO
/TODO
# ignora tutti i file nella directory build/
build/
# ignora doc/note.txt, ma non doc/server/arch.txt
doc/*.txt
# ignora tutti i file .txt nella directory doc/
doc/**/*.txt

Il pattern **/ è disponibile in Git dalla version 1.8.2.

Mostra le modifiche dentro e fuori lo stage

Se git status è troppo vago per te - vuoi sapere cos'è stato effettivamente modificato e non solo quali file — puoi usare il comando git diff. Tratteremo più avanti git diff con maggior dettaglio, ma probabilmente lo userai molto spesso per rispondere a queste due domande: Cos'è che hai modificato ma non è ancora in stage? E cos'hai nello stage che non hai ancora committato? Sebbene git status risponda a queste domande in modo generico, git diff mostra le righe effettivamente aggiunte e rimosse — la patch così com'è.

Supponiamo che tu abbia modificato nuovamente README e benchmarks.rb ma messo nello stage solo il primo. Se esegui il comando status, vedrai qualcosa come questo:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   benchmarks.rb

Per vedere cosa hai modificato, ma non ancora nello stage, digita git diff senza altri argomenti:

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
           @commit.parents[0].parents[0].parents[0]
         end

+        run_code(x, 'commits 1') do
+          git.commits.size
+        end
+
         run_code(x, 'commits 2') do
           log = git.commits('master', 15)
           log.size

Questo comando confronta cosa c'è nella tua directory di lavoro con quello che c'è nella tua area di stage. Il risultato mostra le tue modifiche che ancora non hai messo nello stage.

Se vuoi vedere cosa c'è nello stage e che farà parte della prossima commit, puoi usare git diff --cached. (Da Git 1.6.1 in poi, puoi usare anche git diff --staged, che dovrebbe essere più facile da ricordare). Questo comando confronta le modifiche che hai nell'area di stage e la tua ultima commit:

$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README2
@@ -0,0 +1,5 @@
+grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+Grit is a Ruby library for extracting information from a Git repository

È importante notare che git diff di per se non visualizza tutte le modifiche fatte dall'ultima commit, ma solo quelle che non sono ancora in stage. Questo può confondere, perché se hai messo in stage tutte le tue modifiche, git diff non mostrereà nulla.

Ecco un altro esempio, se metti in stage il file benchmarks.rb e lo modifichi, puoi usare git diff per vedere quali modifiche al file sono in stage e i quali non ancora:

$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   benchmarks.rb

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   benchmarks.rb

Puoi quindi usare git diff per vedere cosa non è ancora in stage

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
 main()

 ##pp Grit::GitRuby.cache_client.stats
+# test line

e git diff --cached per vedere cos'è già in stage:

$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
          @commit.parents[0].parents[0].parents[0]
        end

+        run_code(x, 'commits 1') do
+          git.commits.size
+        end
+
        run_code(x, 'commits 2') do
          log = git.commits('master', 15)
          log.size

Committa le tue modifiche

Ora che la tua area di stage è configurata come vuoi, puoi fare la commit delle tue modifiche. Ricorda che tutto ciò che non è in stage — qualsiasi file che hai creato o modificato per cui non hai fatto git add — non sarà nella commit. Rimarranno come file modificati sul tuo disco. In questo caso, l'ultima volta che hai eseguito git status, hai visto che tutto era in stage, così sei pronto a committare le tue modifiche. Il modo più semplice per farlo è eseguire git commit:

$ git commit

Facendolo lanci il tuo editor predefinito. (Questo è impostato nella tua shell con la variabile di ambiente $EDITOR — generalmente vim o emacs, sebbene tu possa configurarlo con qualsiasi altro editor, usando il comando git config --global core.editor come hai visto nel Capitolo 1).

L'editor visualizzerà il testo (questo è un esempio della schermata di Vim):

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#       new file:   README
#       modified:   benchmarks.rb
#
~
~
~
".git/COMMIT_EDITMSG" 10L, 283C

Come vedi, il messaggio predefinito della commit contiene l'ultimo output del comando git status, commentato, e la prima riga in alto è vuota. Puoi rimuovere questi commenti e inserire il tuo messaggio di commit, o puoi lasciarli così per aiutarti a ricordare cosa stai committando. (Per una nota ancora più esplicita puoi usare l'opzione -v a git commit. Facendo saranno nel commento saranno inserite anche le modifiche stesse, così che tu possa vedere esattamente cosa hai fatto). Quando esci dall'editor, Git crea la tuo commit con un messaggio (rimuovendo commenti ed eventuali diff).

In alternativa, puoi inserire il messaggio per la tua commit alla riga di comando della commit specificando l'opzione -m, come segue:

$ git commit -m "Storia 182: Corretti benchmarks per la velocità"
[master 463dc4f] Storia 182: Corretti benchmarks per la velocità
 2 files changed, 3 insertions(+)
 create mode 100644 README

Hai creato la tua prima commit! Puoi vedere che la commit restituisce alcune informazioni su se stessa: su quale branch (ramo, ndt) hai fatto la commit (master), quale checksum SHA-1 ha la commit (463dc4f), quanti file sono stati modificati e le statistiche sulle righe aggiunte e rimosse con la commit.

Ricorda che la commit registra lo snapshot che hai salvato nella tua area di stage. Qualsiasi cosa che non è nello stage rimarrà lì come modificata; puoi fare un'altra commit per aggiungerli alla cronologia del progetto. Ogni volta che fai una commit, stai salvando un'istantanea (snapshot) del tuo progetto che puoi ripristinare o confrontare in seguito.

Saltare l'area di stage

Sebbene sia estremamente utile per amministrare le commit come vuoi, l'area di stage è molto più complessa di quanto tu possa averne bisogno nel lavoro normale. Se vuoi saltare l'area di stage, Git fornisce una semplice accorciatoia. Con l'opzione -a al comando git commit, Git, committando, mette automaticamente nello stage tutti i file che erano già tracciati, permettendoti di saltare la parte git add:

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   benchmarks.rb

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
 1 files changed, 5 insertions(+)

Nota come in questo caso non hai bisogno di eseguire git add per benchmarks.rb prima della commit.

Rimuovere i file

Per rimuovere un file da Git devi rimuoverlo dai file tracciati (più precisamente, rimuoverlo dall'area di stage) e quindi committare. Il comando git rm fa questo e lo rimuove dalla tua directory di lavoro, così che la prossima volta non lo vedrai come un file non tracciato.

Se rimuovi semplicemente il file dalla directory di lavoro, apparirà nella sezione "Changes not staged for commit" (cioè, no in stage) dell'output git status:

$ rm grit.gemspec
$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    grit.gemspec

no changes added to commit (use "git add" and/or "git commit -a")

Se poi esegui git rm, la rimozione del file viene messa nello stage:

$ git rm grit.gemspec
rm 'grit.gemspec'
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        deleted:    grit.gemspec

La prossima volta che committerai, il file sparirà e non sarà più tracciato. Se avevi già modificato il file e lo avevi aggiunto all'indice, devi forzarne la rimozione con l'opzione -f. Questa è una misura di sicurezza per prevenire la rimozione accidentale dei dati che non sono ancora stati salvati in uno snapshot e che non possono essere recuperati con Git.

Un'altra cosa utile che potresti voler fare è mantenere il file nel tuo ambiente di di lavoro ma rimuoverlo dall'area di stage. In altre parole, vuoi mantenere il file sul tuo disco ma non vuoi che Git continui a tracciarlo. Questo è particolarmente utile se hai dimenticato di aggiungere qualcosa al tuo .gitignore e accidentalmente lo metti in stage, come un file di log molto grande o un gruppo di file compilati .a. Per farlo usa l'opzione --cached:

$ git rm --cached readme.txt

Puoi passare file, directory o pattern glob di file al comando git rm. Questo significa che puoi fare

$ git rm log/\*.log

Nota la barra inversa (\) prima di *. Questo è necessario perché Git ha un'espansione propria dei nomi di file oltre a quella della tua shell. Questo comando rimuove tutti i file che hanno l'estensione .log nella directory log/. O puoi eseguire:

$ git rm \*~

Per rimuovere tutti i file che finiscono con ~.

Spostare i file

A differenza di altri sistemi VCS, Git non traccia esplicitamente gli spostamenti dei file. Se rinomini un file in Git, nessun metadato viene salvato per dirgli che lo hai rinominato. Tuttavia, Git è abbastanza intelligente da capirlo dopo che l'hai fatto — più in la ci occuperemo di rilevare il movimento dei file.

Può perciò creare un po' di confusione il fatto che Git abbia un comando mv. Se vuoi rinominare un file in Git puoi eseguire qualcosa come

$ git mv file_from file_to

e funziona. Se, infatti, lanci un comando del genere e controlli lo stato, vedrai che Git considera il file rinominato:

$ git mv README README.txt
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        renamed:    README -> README.txt

Ovviamente, questo è equivalente a eseguire:

$ mv README README.txt
$ git rm README
$ git add README.txt

Git capisce che, implicitamente stai rinominando il file, così che non c'è differenza se rinominare un file in questo modo o con il comando mv. L'unica differenza reale è che mv è un solo comando invece di tre: è un questione di convenienza. La cosa più importante è che puoi usare qualsiasi strumento per rinominare un file, e gestire l'aggiunta/rimozione più tardi, prima della commit.