Git Advanced by Tim Burglund

Présentation

Notes prises à partir de la présentation de Tim Berglund (@tlberglund/tlberglund@github.com / Github).

Les thèmes abordés lors de la présentation

  • commit à la main (repo internals)
  • Rebase interactif
  • Pull rebase
  • External diff tools
  • Reflog
  • Reset

Ceux qui ne l'ont pas été par manque de temps

  • Pull request
  • Log avancés

Un petit tour dans les entrailles de Git

3 Types d'objets gérés par Git

Un dépôt Git est un ensemble de fichiers de type blob, tree et Commit. Si on se représente un arbre, les blob sont les feuilles et les tree sont les nœuds. Les commits sont alors des pointeurs vers un nœud, ce qui est alors un point d'entrée sur une branche.

Chaque fichier est stocké dans un fichier ayant pour nom, l'empreinte sha256 du contenu de l'objet. Cela permet l'unicité des fichiers et des références associées. On peut comme cela stocké plusieurs versions d'un même fichier et référencer chaque version sans risque d'écrasement.

  • le blob est le contenu d'un fichier.
  • le tree est un élément qui contient une liste de blob et des empreintes associées. Cela correspond au principe de répertoire
  • le commit est un pointeur sur un objet de type 'tree' (empreinte du tree). Il contient aussi l'auteur, le commiter et le message du commit ainsi qu'un pointeur sur le _commit_parent

Chaque commit_donne lieu a de nouveaux _blob et tree correspondant à l'ensemble des éléments qui composent le commit. Un fichier qui n'a pas été modifié est référencé dans plusieurs tree au travers de plusieurs commits. Il y va de même pour les tree qui n'ont pas changé dans un commit.

Bonne idée d'illustration en live

Pour faire la démonstration, Tim crée un dépôt vide

$ git init bits && cd bits

Il ouvre deux terminaux

Commandes Git while true; do _tree_ .git; sleep 1; done;
$ git status

Sur la branche master

Aucun commit

rien à valider (créez/copiez des fichiers et utilisez "git add" pour les suivre)

  .git
  ├── config
  ├── HEAD
  ├── objects
  │   ├── info
  │   └── pack
  └── refs
      ├── heads
      └── tags
      

Suppression des fichiers inutiles pour la démonstration

$ rm -rf .git/hooks

État initial du référentiel Git

$ git status

Création d'un objet de type blob

$ echo "on cree un _blob_ qui contient ce texte" | git empreinte-object -w --stdin
128e7392089cd1fc22e3905214325da95a7d96e7

Information sur l'objet référencé par le empreinte * Son type

    $ git cat-file -t 128e7392089cd1fc22e3905214325da95a7d96e7
    _tree_
  • Son contenu
    $ git cat-file -p 128e7392089cd1fc22e3905214325da95a7d96e7
    on cree un _blob_ qui contient ce texte
    

État du référentiel Git est toujours l'état initial $ git status

# FIXME: contrairemment à la démo, lors de ce git status, un objet _tree_
vide est créé

On va déclaré et nommé l'objet créé dans le cache.Cela impacte le contenu du fichier .git/index

$ git update-index --add --cacheinfo 100644 \
       825dc642cb6eb9a060e54bf8d69288fbee4904 README.txt

On le vérifie de la façon suivante

$ git status
...
new file: README.txt
...
deleted: README.txt
...

On créé maintenant l'objet tree qui correspond à l'état du répertoire contenant README.txt

$ git write-_tree_
0055d7f6ee8c48e021848b27a13cd493449333b0

$ git cat-file -t 0055d7f
_tree_

$ git cat-file -p 0055d7f
100644 _blob_ 128e7392089cd1fc22e3905214325da95a7d96e7    README.txt

On commit_l'objet _tree

$ git commit-_tree_ 0055d7f6ee8c48e021848b27a13cd493449333b0 \
    -m "Crazy crazy commit"
b610c2b4fc8b98a7a58e1657a18087c2d27ba6ff

L'état du référentiel est toujours sale git status car la référence pointée par HEAD n'existe pas

$ cat .git/HEAD
ref: refs/heads/master

On met à jour cette référence

$ git update-ref refs/heads/master b610c2b4fc8b98a7a58e1657a18087c2d27ba6ff
$ cat .git/refs/heads/master
b610c2b4fc8b98a7a58e1657a18087c2d27ba6ff

On est peut enfin avoir des choses connues

$ git log -p
_commit_b610c2b4fc8b98a7a58e1657a18087c2d27ba6ff (HEAD -> master)
Author: fccagou <fccagou@gmail.com>
Date:   Sun May 19 11:29:00 2019 +0200

    Crazy crazy commit

diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..128e739
--- /dev/null
+++ b/README.txt
@@ -0,0 +1 @@
+on cree un _blob_ qui contient ce texte

Mais l'état du répertoire courant n'est pas en phase avec l'index puisqu'on n'a jamais créé de fichier README.txt.

$ 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:    README.txt

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

Il ne nous reste plus qu'à faire un checkout

$ git checkout HEAD -- README.txt
$ $ cat README.txt 
on cree un _blob_ qui contient ce texte
$ git status
On branch master
nothing to commit, working _tree_ clean

Jouons avec les branches, les rebase et les logs

Nouvelle présentation pour l'illustration

Commandes Git while true; do clear; git log --color --decorate --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)' --abbrev-commit --all; done
$ git ...
  * 61f6e0a - (HEAD -> master) add samples files (il y a 4 heures)
  * e96b81c -  Un changement dans master (il y a 5 heures)
  * db3aefb - une modif (il y a 5 heures)
  * b610c2b - Crazy crazy commit (il y a 5 heures)

Dans le second terminal, changer de commande pour l'illustration. Elle permet de voir évoluer les références de HEAD et des branches.

Création d'un ensemble de commits et branches

Dans le terminal des commandes :

$ git branch
* master

$ git branch feature

$ echo "modif pour illustrer le rebase" >> README.txt

$ git _commit_-a -m "une modif"

$ git checkout master

On créé un ensemble de fichiers

$ mkdir files && ce files
$ for i in {1..5}; do echo $RANDOM > file$i.txt; git add file$i.txt; git _commit_-m "add file$i.txt" file$i.txt;  done
[feature 948fdba] add file1.txt
 1 file changed, 1 insertion(+)
 create mode 100644 files/file1.txt
[feature 36cf263] add file2.txt
 1 file changed, 1 insertion(+)
 create mode 100644 files/file2.txt
[feature 74e45f8] add file3.txt
 1 file changed, 1 insertion(+)
 create mode 100644 files/file3.txt
[feature 217e179] add file4.txt
 1 file changed, 1 insertion(+)
 create mode 100644 files/file4.txt
[feature 96edd19] add file5.txt
 1 file changed, 1 insertion(+)
 create mode 100644 files/file5.txt

On voit le graphe sur le terminal de démonstration.

Un merge est un commit_avec 2 parents mais ce n'est pas ce qu'on veut. On veut déplacer l'ensemble des _commit_d'une branche sur la branche _master. C'est le rebase en fast forward

$ git checkout master
$ echo "nouveau changement dans la version master" >> README.txt
$ git _commit_-a -m "Un changement dans master" 
[master e96b81c]  Un changement dans master
 1 file changed, 1 insertion(+)

$ git merge featue
suggestion : en attente de la fermeture duMerge made by the 'recursive' strategy.
 files/file1.txt | 1 +
 files/file2.txt | 1 +
 files/file3.txt | 1 +
 files/file4.txt | 1 +
 files/file5.txt | 1 +
 5 files changed, 5 insertions(+)
 create mode 100644 files/file1.txt
 create mode 100644 files/file2.txt
 create mode 100644 files/file3.txt
 create mode 100644 files/file4.txt
 create mode 100644 files/file5.txt

Oups, je ne voulais pas faire un merge ... Mais temps que rien n'est poussé, je suis serein :)

$ git branch
feature
* master
# On garde le _commit_merge pour l'exerice
$ git rev-parse master
b2a894705506648be602f1a927da0d1faea224de

$ git reset --hard  e96b81c
HEAD est maintenant à e96b81c  Un changement dans master

Reflog pour se sauver des presque tout et trouver ce qui a été fait

$ git reflog
e96b81c (HEAD -> master) HEAD@{0}: reset: moving to e96b81c
b2a8947 HEAD@{1}: merge feature: Merge made by the 'recursive' strategy.
e96b81c (HEAD -> master) HEAD@{2}: commit: Un changement dans master
db3aefb HEAD@{3}: checkout: moving from feature to master
96edd19 (feature) HEAD@{4}: commit: add file5.txt
217e179 HEAD@{5}: commit: add file4.txt
74e45f8 HEAD@{6}: commit: add file3.txt
36cf263 HEAD@{7}: commit: add file2.txt
948fdba HEAD@{8}: commit: add file1.txt
b610c2b HEAD@{9}: checkout: moving from master to feature
db3aefb HEAD@{10}: checkout: moving from feature to master
b610c2b HEAD@{11}: checkout: moving from master to feature
db3aefb HEAD@{12}: commit: une modif
b610c2b HEAD@{13}:

On retrouve bien le merge indésirable et sa valeur d'empreinte

$ git rev-parse HEAD@{1}
b2a894705506648be602f1a927da0d1faea224de

Et on peux y revenir finalement

$ git reset --hard HEAD@{1}
HEAD est maintenant à b2a8947 Merge branch 'feature'

Mais ce n'est pas ce qu'on veut, on veut bien ne pas faire ce merge

$ git reflog
b2a8947 (HEAD -> master) HEAD@{0}: reset: moving to HEAD@{1}
e96b81c HEAD@{1}: reset: moving to e96b81c
b2a8947 (HEAD -> master) HEAD@{2}: merge feature: Merge made by the 'recursive' strategy.
e96b81c HEAD@{3}: commit: Un changement dans master
db3aefb HEAD@{4}: checkout: moving from feature to master
96edd19 (feature) HEAD@{5}: commit: add file5.txt
217e179 HEAD@{6}: commit: add file4.txt
74e45f8 HEAD@{7}: commit: add file3.txt
36cf263 HEAD@{8}: commit: add file2.txt
948fdba HEAD@{9}: commit: add file1.txt
b610c2b HEAD@{10}: checkout: moving from master to feature
db3aefb HEAD@{11}: checkout: moving from feature to master
b610c2b HEAD@{12}: checkout: moving from master to feature
db3aefb HEAD@{13}: commit: une modif
b610c2b HEAD@{14}:

$ git reset --hard HEAD@{3}
HEAD est maintenant à e96b81c  Un changement dans master

C'est parti pour un rebase

Revenons en au fait pour mettre nos modification de la branche feature dans la branche master.

$ git checkout feature
Basculement sur la branche 'feature'

$ git rebase master
Rembobinage préalable de head pour pouvoir rejouer votre travail par-dessus...
Application de  add file1.txt
Application de  add file2.txt
Application de  add file3.txt
Application de  add file4.txt
Application de  add file5.txt

$ git checkour master
Basculement sur la branche 'master'

$ git merge feature
Mise à jour e96b81c..9c7c907
Fast-forward
 files/file1.txt | 1 +
 files/file2.txt | 1 +
 files/file3.txt | 1 +
 files/file4.txt | 1 +
 files/file5.txt | 1 +
 5 files changed, 5 insertions(+)
 create mode 100644 files/file1.txt
 create mode 100644 files/file2.txt
 create mode 100644 files/file3.txt
 create mode 100644 files/file4.txt
 create mode 100644 files/file5.txt

Rebase interactif

Petit rappel sue la syntaxe des références

$ git rev-parse HEAD~5
$ git rev-parse "HEAD^^^^^"

On squash tous les commits de création des fichiers dans files pour ne faire qu'un seul commit.

$ git rebase -i HEAD~5
...
=> on remplace les 4 derniers _pick_ par squash, on sauvegarde et on quitte
...
=> on fait un joli message de commit, on sauvegarde, on quitte et hop 
...
suggestion : en attente de la fermeture du[HEAD détachée 61f6e0a] add samples files
 Date: Sun May 19 12:06:37 2019 +0200
 5 files changed, 5 insertions(+)
 create mode 100644 files/file1.txt
 create mode 100644 files/file2.txt
 create mode 100644 files/file3.txt
 create mode 100644 files/file4.txt
 create mode 100644 files/file5.txt
Successfully rebased and updated refs/heads/master.

On obtient un nouveau commit sur la branche master et la branche feature référence toujours les 5 commits faits pour créer les 5 fichiers. On n'a plus besoin de la branche feature donc on a supprime

$ git branch -D feature
Branche feature supprimée (précédemment 9c7c907).

A ce stade, il n'y a plus de référence au commit pointé par le branche feature MAIS le commit existe toujours. Pour s'en persuader, il suffit d'interroger Git.

$ git cat -t 9c7c907
commit

External diff tools

  • Local conf : .gitconfig
  • Global conf :

    $ git config --global merge.tool vimdiff $ git config --global mergetool.prompt false $ git difftool

From scratch

L'équivalent de git init scratch

$ mkdir scratch && cd scratch
$ mkdir -p .git/objects
$ mkdir -p .git/refs/heads
$ echo 'ref: refs/heads/master' > .git/HEAD

Création d'un premier fichier

$ git status
$ echo "first file" | git hash-object --stdin -w                                                                  
$ git update-index --add --cacheinfo 100644 303ff981c488b812b6215f7db7920dedb3b59d9a  README.txt                  
$ git write-tree                                                                                                  
$ git commit-tree f9f8cb0e34417c41edafe889c303d1acc9a858b7 -m "Crazy crazy commit"
$ git update-ref refs/heads/master be8c65c58aa5ab7ef8737079da858486fd102bf8
$ git checkout master -- README.txt