Cos’è un sistema di controllo di versione?
Un sistema di controllo di versione è uno strumento che tiene traccia delle modifiche ai file, di fatto creando diverse versioni dei nostri file e mantenendo dei metadati utili su di essi. La storia completa dei nostri commit, che possiamo considerare come degli snapshot, e i loro metadati compongono un repository. I repository possono essere sincronizzati tra diversi computer, facilitando la collaborazione tra più persone.
Storia
I sistemi di controllo di versione non sono una novità. Strumenti come RCS, CVS o Subversion esistono fin dagli anni ‘80 e sono utilizzati da molte grandi aziende. Tuttavia, molti di questi sono ormai considerati sistemi legacy (ossia obsoleti) a causa di varie limitazioni nelle loro capacità. Sistemi più moderni, come Git e Mercurial, sono distribuiti, il che significa che non è necessario un server centralizzato per ospitare il repository. Includono anche potenti strumenti di merging che rendono possibile per più autori lavorare sugli stessi file contemporaneamente.
Git è diventato lo standard de-facto quando si tratta di questo tema. È stato creato da Linus Torvalds durante lo sviluppo del sistema operativo Linux. Non è da confondere con GitHub, che è un sito web commerciale che ospita repository git.
Fondamentali
La caratteristica fondamentale offerta da git è la capacità di tracciare lo stato di tutti i file in una cartella, permettendoti di muoverti liberamente tra tutti gli snapshot salvati.
Staging
Dal punto di vista di git, i file possono trovarsi in uno dei tre stati: working directory, staging e commit. La working directory rappresenta lo stato attuale dei tuoi file, come li vedi nel momento in cui li apri con un qualsiasi software. I file possono essere spostati nell’area di staging. Questo indica che sono pronti per essere committati. Infine, creare un commit prenderà uno snapshot dell’area di staging e memorizzerà tutti i cambiamenti in un nuovo checkpoint che può essere ripristinato in qualsiasi momento in futuro.
Loading diagram...
Tip
Puoi pensare all’area di staging come a uno “snapshot in progress”. Quando sei soddisfatto del tuo lavoro, finalizzi lo snapshot creando un commit permanente e immutabile.
Note
Se modifichi nuovamente un file che avevi precedentemente aggiunto all’area di staging, le nuove modifiche non saranno presenti: lo “snapshot in progress” è stato preso prima dei cambiamenti. Se vuoi tener traccia di essi, devi aggiungere nuovamente il file, aggiornando lo “snapshot in progress” prima del commit.
Branch di git
Branch early, branch often
Comandi base
Configurazione globale
È una buona idea iniziare creando una configurazione globale per git. Le informazioni minime richieste sono il nome utente e l’email. Possono essere configurate tramite la riga di comando con i seguenti comandi:
git config --global <config-key> <config-value>
# Configura il nome utente
git config --global user.name "Il mio nome"
# Configura l'email
git config --global user.email "myemail@myemail.com"
# Configura le fine righe
git config --global core.autocrlf input
# Configura l'editor
git config --global core.editor "nano -w"
# Lista tutte le configurazioni
git config --list
Editor disponibili
Editor | Comando di configurazione |
---|---|
Atom | git config --global core.editor "atom --wait" |
nano | git config --global core.editor "nano -w" |
BBEdit (Mac, with command line tools) | git config --global core.editor "bbedit -w" |
Sublime Text (Mac) | git config --global core.editor "/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl -n -w" |
Sublime Text | git config --global core.editor "'c:/program files/sublime text 3/sublime_text.exe' -w" |
Notepad++ | git config --global core.editor "'c:/program files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin" |
Kate (Linux) | git config --global core.editor "kate" |
Gedit (Linux) | git config --global core.editor "gedit --wait --new-window" |
Scratch (Linux) | git config --global core.editor "scratch-text-editor" |
Emacs | git config --global core.editor "emacs" |
Vim | git config --global core.editor "vim" |
VS Code | git config --global core.editor "code --wait" |
Inizializzare il repository
Per poter utilizzare git in un progetto, dobbiamo prima creare una nuova cartella e inizializzare il repository con il comando git init
.
# Crea una nuova directory chiamata "recipes" (linux)
mkdir recipes
# Spostati in quella directory
cd recipes
# Inizializza un repository git
git init
#> Initialized empty Git repository in /home/user/recipes/.git/
Verrà creata una cartella nascosta .git
.
Al suo interno, git memorizzerà tutti i dati necessari per tenere traccia dello stato del progetto (ad es. le modifiche ai file).
# Provando a elencare i file nella cartella non verrà mostrato nulla
ls
#>
# Aggiungendo il flag -a, la cartella .git apparirà
ls -a
#> . .. .git
Warning
Disinizializzare un repository git è semplice come eliminare la cartella .git
.
Lo stato di tutti gli altri file rimarrà invariato, ma perderai tutti i tuoi commit.
Git creerà anche un branch predefinito, chiamato main (o, storicamente, master).
Puoi controllare lo stato del repository con git status
.
# Visualizza lo stato del repository
git status
#> On branch main
#>
#> No commits yet
#>
#> nothing to commit (create/copy files and use "git add" to track
Loading diagram...
Tracciare le modifiche
Git terrà traccia di qualsiasi nuovo file aggiunto alla cartella.
# Crea un nuovo file di testo contenente "Nuovo file"
echo "Nuovo file" > new_file.txt
# Controlla lo stato del repository
git status
#> On branch main
#>
#> No commits yet
#>
#> Untracked files:
#> (use "git add <file>..." to include in what will be committed)
#> new_file.txt
#>
#> nothing added to commit but untracked files present (use "git add" to track)
Se vogliamo fare uno snapshot di questa modifica, dobbiamo prima aggiungerla all’area di staging e poi creare un nuovo commit con un messaggio che descriva la nostra modifica.
# Aggiungi il file all'area di staging
git add new_file.txt
# Controlla lo stato del repository
git status
#> On branch main
#>
#> No commits yet
#>
#> Changes to be committed:
#> (use "git rm --cached <file>..." to unstage)
#> new file: new_file.txt
#>
# Crea un commit (snapshot)
git commit -m "Aggiunto nuovo file new_file.txt"
#> [main (root-commit) ce52f8c] Aggiunto nuovo file new_file.txt
#> 1 file changed, 1 insertion(+)
#> create mode 100644 new_file.txt
# Controlla lo stato del repository
git status
#> On branch main
#> nothing to commit, working tree clean
Loading diagram...
Tip
Per rimuovere tutti i file dall’area di staging (unstage) usa il comando git reset
.
Se si vuole rimuovere solo un file, basta indicarne il nome: git reset new_file.txt
Se il file viene ulteriormente modificato (aggiungiamo delle righe), lo stato di git lo rifletterà.
# Aggiungi una riga a new_file.txt
echo "Un'altra riga" >> new_file.txt
# Controlla lo stato del repository
git status
#> On branch main
#> Changes not staged for commit:
#> (use "git add <file>..." to update what will be committed)
#> (use "git restore <file>..." to discard changes in working directory)
#> modified: new_file.txt
#>
#> no changes added to commit (use "git add" and/or "git commit -a")
# Provando a fare un commit con un'area di staging vuota solleverà un errore
git commit -m "Mi sono dimenticato di 'git add new_file.txt'"
#> On branch main
#> Changes not staged for commit:
#> (use "git add <file>..." to update what will be committed)
#> (use "git restore <file>..." to discard changes in working directory)
#> modified: new_file.txt
#>
#> no changes added to commit (use "git add" and/or "git commit -a")
# Invece, prima metti in staging tutte le modifiche che vuoi committare
git add new_file.txt
# E poi le committi
git commit -m "Aggiunta una riga a new_file.txt"
#> [main 3a25799] Aggiunta una riga a new_file.txt
#> 1 file changed, 1 insertion(+)
Loading diagram...
Tip
Git non tiene traccia delle cartelle vuote.
Se vuoi sovrascrivere questo comportamento, aggiungi un file vuoto alla cartella.
La convenzione è di chiamare il file vuoto .gitkeep
.
# Crea un file vuoto .gitkeep
echo "" > .gitkeep
Non tracciare le modifiche
Ci sono alcuni file che potresti non voler tracciare con un sistema di controllo di versione.
Questi includono tipicamente file binari, file di cache, video o immagini di grandi dimensioni e log.
Per far sapere a git che dovrebbe ignorare questi file, crea un file .gitignore
e specifica tutto ciò che dovrebbe essere ignorato.
# File .gitignore
# Ignora il file "eseguibile.exe"
eseguibile.exe
# Ignora i file con le estensioni ".mp3", ".mp4" e ".avi"
*.mp3
*.mp4
*.avi
# Ignora la cartella "cache"
cache/
# Ignora tutti i file ".png" sotto la cartella "docs"
docs/**/*.png
# Traccia sempre ".gitkeep" a prescindere da qualsiasi regola precedente
!.gitkeep
Storia del repository
La storia del repository, con tutti i suoi commit, è disponibile tramite il comando git log
.
# Mostra la storia del repository
git log
#> commit 3a2579938cc2d2934e24d971f9738894a60c18ad (HEAD -> main)
#> Author: Ernesto Casablanca <
#> Date: Tue Oct 22 11:29:05 2024 +0100
#>
#> Aggiunta una riga a new_file.txt
#>
#> commit ce52f8cef8a35df3bc1e848a579668de263d5a6a
# Mostra la storia in modo più compatto
git log --oneline
#> 3a25799 (HEAD -> main) Aggiunta una riga a new_file.txt
#> ce52f8c Aggiunto nuovo file chiamato new_file.txt
# Mostra la storia tenendo traccia dei branch
git log --oneline --graph
#> * 3a25799 (HEAD -> main) Aggiunta una riga a new_file.txt
#> * ce52f8c Aggiunto nuovo file chiamato new_file.txt
# Mostra informazioni esaustive ma compatte sulla storia del repository
git log --all --decorate --oneline --graph --date=relative --pretty=tformat:'%C(auto)%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'
#> * 3a25799 - (HEAD -> main) Aggiunta una riga a new_file.txt (Ernesto Casablanca 28 minutes ago)
#> * ce52f8c - Aggiunto nuovo file chiamato new_file.txt (Ernesto Casablanca 38 minutes ago)
Confrontare le differenze
Git può produrre un confronto compatto e informativo tra lo stato attuale del repository con il comando git diff
.
# Aggiungi un'altra riga a new_file.txt
echo "E un'altra ancora" >> new_file.txt
# Mostra le differenze
git diff
#> diff --git a/new_file.txt b/new_file.txt
#> index 20a8ff1..7785ace 100644
#> --- a/new_file.txt
#> +++ b/new_file.txt
#> @@ -1,2 +1,3 @@
#> Nuovo file
#> Un'altra riga
#> +E un'altra ancora
# Possiamo anche essere selettivi con i file di cui vogliamo vedere la differenza
git diff new_file.txt
#> diff --git a/new_file.txt b/new_file.txt
#> index 20a8ff1..7785ace 100644
#> --- a/new_file.txt
#> +++ b/new_file.txt
#> @@ -1,2 +1,3 @@
#> Nuovo file
#> Un'altra riga
#> +E un'altra ancora
# I file in staging non appariranno più nella differenza
git add new_file.txt
git diff
#>
# Per vedere le differenze tra l'area di staging e l'ultimo commit,
# aggiungi il flag --staged
git diff --staged
#> diff --git a/new_file.txt b/new_file.txt
#> index 20a8ff1..7785ace 100644
#> --- a/new_file.txt
#> +++ b/new_file.txt
#> @@ -1,2 +1,3 @@
#> Nuovo file
#> Un'altra riga
#> +E un'altra ancora
# Committando le modifiche aggiornerai il riferimento per i futuri diff
git commit -m "Aggiunta ancora un'altra riga a new_file.txt"
#> [main 1b7d1e4] Aggiunta ancora un'altra riga a new_file.txt
#> 1 file changed, 1 insertion(+)
Loading diagram...
Working with a remote
One of git’s most appreciated features is its ability of synchronising the local repository with a remote git server. You could host your own git server, or use a public one, such as GitHub or GitLab.
Adding a remote
To let the local repository know of the remote one, we use the git remote add
command.
The url we use should match the one provided by the remote git server.
# Add a new remote called "origin" at the provided url
git remote add origin git@github.com:TendTo/my-repo.git
# Check the remote was correctly added
git remote -v
#> origin git@github.com:TendTo/my-repo.git (fetch)
#> origin git@github.com:TendTo/my-repo.git (push)
Remote authentication
Most services will require some form of authentication before allowing you to push any changes to the remote git repository. Using GitHub as a reference, the two available protocols are https and ssh. ssh is usually preferable because, while it requires some additional configuration, it is a security protocol widely used by many applications. GitHub does not permit HTTPS access to repositories using your account login and password in any event, instead requiring the creation of a personal access token (PAT) which also needs additional configuration.
Generating ssh keys
To generate a pair of ssh keys, use the ssh-keygen
command.
You will be asked for a path where to save the keys.
By default they will be saved under the .ssh folder in your home directory.
If you choose the default path, git will be able to find the key without any further indication.
You will also be given the option to provide a password you will need to input every time you need to use the key.
Press enter without writing anything to avoid setting the password.
# Generate a new key pair.
# ed25519 is the encryption algorithm.
# You can put a comment, usually your email, to identify the key
ssh-keygen -t ed25519 -C "myemail@myemail.com"
#> Generating public/private ed25519 key pair.
#> Enter file in which to save the key (/home/user/.ssh/id_ed25519): ./my_key
#> Enter passphrase (empty for no passphrase):
#> Enter same passphrase again:
#> Your identification has been saved in ~/.ssh/id_ed25519
#> Your public key has been saved in ~/.ssh/id_ed25519
#> The key fingerprint is:
#> SHA256:1DYSxuTTogM8nrNkWeXXbpd9MAe6fiUDd6vlB0y0gHg myemail@myemail.com
#> The key's randomart image is:
#> +--[ED25519 256]--+
#> | o=. .. o |
#> | . =o+E. + o |
#> | + . B.* + * o|
#> | . * o * o * B.|
#> | B o S + Bo+|
#> | o o . o .+=.|
#> | . .....|
#> | . .|
#> | |
#> +----[SHA256]-----+
Publishing the ssh keys
Now we need publish our public key on the remote repository.
Read the public key (the file with the .pub
extension) and paste the whole output on your remote of choice.
# Read the public key
cat ~/.ssh/id_ed25519.pub
#> ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDmRA3d51X0uu9wXek559gfn6UFNF69yZjChyBIU2qKI myemail@myemail.com
For instance, on GitHub, click on your profile icon in the top right corner to get the drop-down menu. Click Settings, then on the settings page, click SSH and GPG keys, on the left side Account settings menu. Click the New SSH key button on the right side. Now, you can add a title to help you remember the key, paste your SSH key into the field, and click the Add SSH key to complete the setup.
Pushing changes
Git workflows
Branches
It is generally a good practice to keep the main work safe from experimental changes we are working on. To do this we can use branches to work on separate tasks in parallel without changing our current branch, main.
We can list all branches and create new ones with the git branch
command.
# List all branches
git branch
># * main
# Create a new branch called experiment
git branch experiment
# List all branches
# The '*' indicates the active branch
git branch
#> experiment
#> * main
Loading diagram...
Creating a branch just adds a new pointer to the last commit. In order to ensure that future commits are appended to the new branch, we need to checkout to it.
# Switch to the experiment branch
git checkout experiment
#> Switched to branch 'experiment'
# We can double check that is the case with git log
# HEAD indicates the active branch
git log --oneline
#> 1b7d1e4 (HEAD -> experiment, main) Added yet another line to new_file.txt
#> 3a25799 Added a line to new_file.txt
#> ce52f8c Added new file called new_file.txt
We can create and switch to a new branch with the checkout
command or with the newer switch
command.
# Create and checkout to the new branch 'new'
git checkout -b new
#> Switched to a new branch 'new'
# or, equivantely
# Create and checkout to the new branch 'new'
git switch -c new
#> Switched to a new branch 'new'
Loading diagram...
We can delete a branch we don’t need anymore by using the -d
flag in the branch
command.
We cannot delete a currently checked out branch.
# Delete the branch "experiment"
git branch -d experiment
#> Deleted branch experiment (was 1b7d1e4).
Loading diagram...
Warning
All commits related to that branch will be lost, unless there is another branch keeping track of them.
We can makes some changes and commit them on the new branch.
# Make some changes
echo "Branch new" > branch_file.txt
# Add and commit them
git add branch_file.txt
git commit -m "Add branch_file.txt"
#> [new 091a043] Add branch_file.txt
#> 1 file changed, 1 insertion(+)
#> create mode 100644 branch_file.txt
Loading diagram...
When we are satisfied with the changes, we can merge them back in the main branch.
# Checkout back to the main branch
git checkout main # or git switch main
#> Switched to branch 'main'
# Merge the changes from the "new" branch
git merge new
#> Updating 1b7d1e4..091a043
#> Fast-forward
#> branch_file.txt | 1 +
#> 1 file changed, 1 insertion(+)
#> create mode 100644 branch_file.txt
Loading diagram...
Merge conflicts
Git employs a lot of heuristics to automatically handle merges between different branches in a non disruptive way. That being said, there are some cases where there is no obvious solution. Hence git will trust you to know which changes should be given higher priority and override the others.
# Switch to the new branch "left" and make some changes
git checkout -b left
echo "left" >> branch_file.txt
git add branch_file.txt
git commit -m "Left changes"
# Go back to the main branch
git checkout main
# Repeat the same procedure for the "right" branch
git checkout -b right
echo "right" >> branch_file.txt
git add branch_file.txt
git commit -m "Right changes"
# Go back to the main branch
git checkout main
# Show the state of the repository
git log --oneline --graph --all
#> * c5c0519 (right) Right changes
#> | * 9ba1fdc (left) Left changes
#> |/
#> * 091a043 (HEAD -> main, new) Add branch_file.txt
#> * 1b7d1e4 Added yet another line to new_file.txt
#> * 3a25799 Added a line to new_file.txt
#> * ce52f8c Added new file called new_file.txt
Loading diagram...
Trying to merge both branches into the main will raise a merge conflict.
# Merge the left branch into main; no problems
git checkout main
git merge left
#> Updating 091a043..9ba1fdc
#> Fast-forward
#> branch_file.txt | 1 +
#> 1 file changed, 1 insertion(+)
# Merge the right branch; merge conflict
git merge right
#> Auto-merging branch_file.txt
#> CONFLICT (content): Merge conflict in branch_file.txt
#> Automatic merge failed; fix conflicts and then commit the result.
Loading diagram...
In other words, git does not know how to handle the divergence of the file automatically, hence it needs your guidance.
If you open the branch_file.txt
, you will notice git has modified it.
# Read "branch_file.txt"
cat branch_file.txt
#> Branch new
#> <<<<<<< HEAD
#> left
#> =======
#> right
#> >>>>>>> right
The section between <<<<<<< HEAD
and =======
is the content of the file in the current commit, while the other section indicates what the branch we are merging would have it changed to.
It is up to us to edit the file and solve the conflict, according to our needs.
When we are satisfied, we should delete the lines added by git and commit the result.
# Commit the solved merge
git add branch_file.txt
git commit -m "Merge branch 'right'"
#> [main d9a26e8] Merge branch 'right'
Loading diagram...