Projet git

Bases de l'utilisation de git. On va utiliser git/github grâce à un petit projet web et en profiter pour expliquer le fonctionnement interne de git.

Création du projet git

Munissez vous d'un terminal et placez vous dans un dossier vierge :

mkdir mon_projet_web
cd mon_projet_web 

On peut maintenant initialiser un projet git :

git init

le dossier .git

La commande précédente a initialisé git en créant un dossier caché .git. Git ne fonctionne que comme ça, tout est mis dans ce dossier. Chez moi, il ressemble à ça :

.
└── .git
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── fsmonitor-watchman.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-merge-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

9 directories, 16 files

J'ai utilisé la commande tree -a pour afficher l'arborescence et les fichiers cachés.

Le fichier de configuration est également décrit dans ce post. En deux mots :

Fonctionnement de git

Le but de git est de garder un historique d'un projet, en conservant les fichiers du projets et leurs modifications au cours du temps. Cet historique est composé de commit qui stock l'état d'un projet à un instant donné. Plus précisément il stocke la différence en l'état précédent et le nouvel état, formant un graphe direct et acyclique (si je jargonne)

Pour démarrer cet historique faisons notre premier commit.

Premier commit

Pour cela, il faut que l'on ait quelque chose à stocker. Créons un petit fichier index.html à la racine de notre projet (donc ne soyez plus dans le dossier .git) :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>My homepage</title>
</head>
<body>
<h1>Hello World !</h1>
</body>
</html>

Pour faire un commit (stocker l'état d'un projet) il faut commencer par dire à git ce que l'on veut "commiter", ici notre fichier index.html :

git add index.html

On place le fichier index.html courant dans le stage c'est à dire l'endroit où son mis les fichier qui seront inclus dans le commit. Pour voir tout ça, la commande status est super utile :

git  status

Cette commande dit chez moi (et en couleur) :

Sur la branche main

Aucun commit

Modifications qui seront validées :
  (utilisez "git rm --cached <fichier>..." pour désindexer)
  nouveau fichier : index.html

On a bien un nouveau fichier que l'on veut garder. On peut fait notre premier commit :

git  commit

Cette commande va lancer l'éditeur que vous avez configuré (vim si vous avez suivi mes recommandations) et va vous demander de décrire votre commit. Cette étape est obligatoire et très importante, ne mettez donc pas de message fantaisiste : décrivez en une ligne ce que vous avez fait. Ici, par exemple : "first commit". Sauvez et sortez de vim, vous avez fait votre premier commit. On le voit en tapant la commande git status (qui dit que tout est ok, que le stage est vide et que l'on a aucun fichier non suivi par git).

La commande git log vous donne un historique des commit avec le nom, l'heure et le message. Chez moi ça donne :

commit 3b8c0a8836050e58ec8cf8bd24f3d06b0bf39613 (HEAD ->  main)
Author: François Brucker <francois.brucker@gmail.com>
Date:   Sat Sep 19 14:35:41 2020 +0200

    First commit !

Les objets

Git va tout (oui tout) stocker dans le dossier .git/objects sous la forme d'un fichier compressé de nom égal à sa valeur de hash SHA-1 sur 40 octets.

Il est possible mais hautement improbable que 2 fichiers différents aient la même valeur de hash SHA-1. Dans ce cas là git sera perdu. Mais bon, que cela arrive c'st rare, et dans un même projet c'est encore plus rare.... Voir n'est pas encore arrivé.

Chez moi j'ai, avec la commande tree objects exécutée dans le dossier git :

objects
├── 3b
│   └── 8c0a8836050e58ec8cf8bd24f3d06b0bf39613
├── 4b
│   └── 825dc642cb6eb9a060e54bf8d69288fbee4904
├── aa
│   └── e16b7870ad8867b12184215adf9063afc800c1
├── ab
│   └── 3401bd309f7e474cba48b2ecc06c09543a1e0d
├── info
└── pack

Ce dossier est la base de donnée de notre projet. Chaque objet est stocké avec une signature SHA-1 de 40 octets, les deux premiers étant le nom du dossier, les 38 autres le nom du fichier.

Si je veux savoir ce qu'il y a dans le fichier ab/3401bd309f7e474cba48b2ecc06c09543a1e0d du dossier objet, je tape : git cat-file -p ab3401bd309f7e474cba48b2ecc06c09543a1e0d et j'obtiens :


<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>My homepage</title>
</head>
<body>
<h1>Hello World !</h1>
</body>
</html>

C'est notre fichier html ! Les autres objets correspondent à l'arborescence de projet (pour l'instant un unique nœud, notre commit) et le commit en lui même. Il aura un autre nom chez vous, mais chez moi c'est celui de nom 3b/8c0a8836050e58ec8cf8bd24f3d06b0bf39613 (c'est le numéro donné dans le commit).

Les références

Le dossier référence contient les références des commits de toutes les branches et ou tags de notre projet. Nous n'avons qu'une seule branche, nommée main commit des différentes branches. En regardant ce qu'il y a dans le fichier refs/heads/main je retrouve bien le numéro de commit.

On peut accéder à tout dans git en utilisant ces numéros (en entier ou les 4 ou plus premiers chiffres). Par exemple si jeu veux voir le log de mon commit de numéro 3b8c0a8836050e58ec8cf8bd24f3d06b0bf39613, je peux taper git log 3b8c0a8836050e58ec8cf8bd24f3d06b0bf39613, git log 3b8c ou encore git log 3b8c0a8

Avancer dans l'arbre

Modifions notre fichier index.html :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
</body>
</html>

La commande git status nous donne ce qui a changé :


Sur la branche main
Modifications qui ne seront pas validées :
  (utilisez "git add <fichier>..." pour mettre à jour ce qui sera validé)
  (utilisez "git restore <fichier>..." pour annuler les modifications dans le répertoire de travail)
  modifié :         index.html

aucune modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")

Un fichier a été modifié. Comme on a pas de nouveaux fichiers, et que l'on veut juste mettre à jour les fichiers déjà suivis, on peut utiliser l'argument -a de la commande git commit qui place dans le stage tous les fichiers suivi et modifiés. On ajoute un autre argument, -m, qui permet d'ajouter le message de commit directement dans la commande sans passer par l'éditeur.

git commit -a -m"add french"

On peut voir les logs (avec en prime deux nouvelles options, une de git pour ne pas avoir de pager une option de log pour juste afficher le message et le numéro du commit) : git log --pretty=oneline. J'obtiens :

11f5564cda69451538ff8036c1eb92834a585884 (HEAD -> main) add french
3b8c0a8836050e58ec8cf8bd24f3d06b0bf39613 First commit !

Ajout de fichiers

Ajoutons un fichier css à notre projet et faisons les liens avec le fichier html.

Fichier main.css :


h1 {
  color: olive;
}

et le fichier index.html modifié :


<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>
    
    <link href="main.css" rel="stylesheet">
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
</body>
</html>

La commande git status m'indique qu'il existe un fichier non suivi (main.css) et que le fichier index.html a été modifié :

Sur la branche main
Modifications qui ne seront pas validées :
  (utilisez "git add <fichier>..." pour mettre à jour ce qui sera validé)
  (utilisez "git restore <fichier>..." pour annuler les modifications dans le répertoire de travail)
  modifié :         index.html

Fichiers non suivis:
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
  main.css

aucune modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")

Faites un commit de tout ça (en n'oubliant pas d'ajouter main.css au stage avant le commit car l'option -a n'ajoute pas automatiquement au stage les fichiers non suivis) :

git add main.css
git add index.html

# on vérifie bien tout avant le commit
git status

# on commit
git commit -m"add css and link to html"

# on vérifie que tout s'est bien passé.
git status

Prenez l'habitude de faire un git status avant et après chaque commit, histoire d'être sur que l'on ne va rien oublier dans le commit.

Un git log --oneline nous montre que l'on a 3 commits (notez que l'option --oneline ne garde que les 7 premiers chiffres de chaque commit).

diff avant le commit pour voir la diff avec ce qui a été enregistré.

Historique

Vous pouvez voir ce qu'il y avait dans chaque commit, cette doc vous montre plein de chouettes exemples.

En particulier : git log -p qui montre ce que l'on a fait avec chaque commit. Le format de git diff est un format étonnamment lisible qui montre distinctement les différences entre les versions.

Diff

On peut également voir les différences entre le dernier commit et ce que l'on a fait en utilisant la commande git diff.

Commençons par modifiez nos fichiers main.css et index.html :

On ajoute une règle pur les <h2> dans main.css :

h2 {
  color: blue;
}
h1 {
  color: olive;
}

Et un petit paragraphe dans index.html :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>
    
    <link href="main.css" rel="stylesheet">
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
<p>Comment allez-vous ? </p>
</body>
</html>

La commande git diff nous indique les différences entre le dernier commit et ce qeu je n'ai pas encore mis en stage. Donc ici git diff nous donne les différences pour main.css et index.html :

diff --git a/index.html b/index.html
index b73fc14..17c8556 100644
--- a/index.html
+++ b/index.html
@@ -9,5 +9,6 @@
 <body>
 <h1>Hello World !</h1>
 <h2>et bonjour Monde !</h2>
+<p>Comment allez-vous ? </p>
 </body>
 </html>
diff --git a/main.css b/main.css
index 4a4e8ca..aca9293 100644
--- a/main.css
+++ b/main.css
@@ -1,3 +1,6 @@
+h2 {
+  color: blue;
+}
 h1 {
   color: olive;
 }

Mettons le fichier index.html en stage (git add index.html) et refaisons la commande git diff :

diff --git a/main.css b/main.css
index 4a4e8ca..aca9293 100644
--- a/main.css
+++ b/main.css
@@ -1,3 +1,6 @@
+h2 {
+  color: blue;
+}
 h1 {
   color: olive;
 }

Nous n'avons bien plus que les différences avec main.css.

Allez commitons tout ça : git commit -a -m"h2 rule in css and p in html". Notez que le fichier main.css a bien été ajouté au stage avant le commit grâce à l'argument -a. Vérifiez le avec git status voir même un git diff.

La commande git diff --cached permet de faire le diff en prenant en compte le stage. Vous avez donc un diff entre le dernier commit et ce que vous avez fait depuis.

Modifier le dernier commit

Il arrive parfois (souvent) de se rendre compte juste après un commit que l'on a pas tout envoyé (le git status n'est pas clean) ou que l'on a fait une faute dans le message accompagnant le commit. C'est pour ça qu'il existe l'argument --amend à commit qui vous permet de modifier le dernier commit que vous avez fait.

Fichier .gitignore

Il est impératif qu'avant chaque commit il ne reste aucun fichier non suivi. Cependant, certains fichiers sont pré&sent dans le dossier mais on ne veut pas les inclure dans le projet git. On peut citer :

Pour que git ignore ces fichiers on utilise un fichier .gitignore) qui liste ces fichiers. Son format est à la fois simple et efficace. On peut même avoir un fichier .gitignore par dossier du projet, donc utilisez le !

Vous trouverez plein d'exemples de .gitignore, je vous conseille ne pas mettre plein de choses dont vous n'avez pas besoin. Ajoutez des lignes au .gitignore uniquement lorsque vous en avez besoin.

Par exemple, après avoir supprimé un fichier via le finder sur mon mac, git status me donne ça :

Sur la branche main
Fichiers non suivis:
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
  .DS_Store

aucune modification ajoutée à la validation mais des fichiers non suivis sont présents (utilisez "git add" pour les suivre)

On va donc créer un fichier .gitignore contenant uniquement la ligne .DS_STORE, l'ajouter au stage et refaire un git status pour obtenir :

Sur la branche main
Modifications qui seront validées :
  (utilisez "git restore --staged <fichier>..." pour désindexer)
  nouveau fichier : .gitignore

On fini par l'ajouter au projet par un commit : git commit -m"add .gitignore".

Branches

Les branches de git permettent d'avoir plusieurs histoires possible de mon projet.

La principale utilisation des branches en développement est :

Ceci nous assure que la branche main est TOUJOURS un projet fonctionnel.

A part ces branches temporaires, on a parfois besoin de branches plus pérennes comme :

La commande git branch nous indique les branches que nous avons. Pour l'instant nous n'avons que la branche par défaut : main.

Créer une branche

Nous allons créer une branche pour voir s'il est possible d'ajouter du javascript à notre projet : git branch js.

Si l'on refait la commande git branch, on voit qu'on a deux branches et qu'on est toujours sur la branche main. Allons vers la branche js avec la commande : git checkout js

On peut maintenant tranquillement ajouter du js à notre projet :

On peut maintenant commiter le tout (en commençant pas ajouter les fichiers main.js et index.html au stage bien sur avec la commande git add main.js index.html) : git commit m"add js".

Un git log --oneline nous montre bien que l'on est maintenant sur une nouvelle branche :

f7907be (HEAD -> js) add js
35609dd (main) add .gitignore
7d2fd72 h2 rule in css and p in html
a3c3fdc add css and link to html
11f5564 add french
3b8c0a8 First commit !

Voir des branches

Si l'on revient à la branche main avec la commande git checkout main on voit que le fichier main.js a disparu et que le fichier index.html est remis à sa position sans le js.

Faisons une modification du fichier index.html de la branche main en ajoutant une phrase au paragraphe :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>

    <link href="main.css" rel="stylesheet">
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
<p>Comment allez-vous ?  Bien ou quoi ?</p>
</body>
</html>

Et on commit le tout : git commit -am"parlons jeune"

On remarque que la commande git log --oneline ne montre que l'histoire du dernier commit, on ne montre donc pas la modification de la branche js qui est inutile pour notre dernier commit.

Pour voir tous les log, on peut ajouter l'argument --all. Du coup : git log --oneline --all donne :

a2ac886 (HEAD -> main) parlons jeune
f7907be (js) add js
35609dd add .gitignore
7d2fd72 h2 rule in css and p in html
a3c3fdc add css and link to html
11f5564 add french
3b8c0a8 First commit !

Et si l'on veut voir le graphe des dépendances on peut ajouter l'argument --graph : git log --oneline --all --graph :

* a2ac886 (HEAD -> main) parlons jeune
| * f7907be (js) add js
|/  
* 35609dd add .gitignore
* 7d2fd72 h2 rule in css and p in html
* a3c3fdc add css and link to html
* 11f5564 add french
* 3b8c0a8 First commit !

Réconcilier les branches

Si l'on veut maintenant mettre notre branche expérimentale (js) dans la branche main, il va falloir réconcilier les branches. Cela peut se faire de nombreuses manière mais la méthode couramment utilisée actuellement est celle du rebase

Nous allons donc procéder comme suit :

  1. depuis la branche js, on va la faire commencer à la fin de main
  2. on va "merger" la branche js à la suite de la branche main.

On aura donc à la fin un joli historique qui fait comme si j'avais ajouter mon js à la suite du main sans autres branches.

Git rebase

Sur la branche js on exécute la commande : git rebase main et on obtient le résultat :

Fusion automatique de index.html
CONFLIT (contenu) : Conflit de fusion dans index.html
error: impossible d'appliquer f7907be... add js
Resolve all conflicts manually, mark them as resolved with
"git add/rm ", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
impossible d'appliquer f7907be... add js

En essayant d'ajouter les derniers commits de main au début de js, git a un soucis. Il n'arrive pas à le faire tout seul. Il va falloir l'aider.

Son soucis est dans le fichier index.html. Regardons le :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>

    <link href="main.css" rel="stylesheet">
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
<<<<<<< HEAD
<p>Comment allez-vous ?  Bien ou quoi ?</p>
=======
<p id="couleur">Comment allez-vous ? </p>

<script src="main.js"></script>
>>>>>>> f7907be... add js
</body>
</html>

Horreur, c'est tout cassé. Mais au final c'est compréhensible. Le haut est HEAD (donc main) et le bas c'est ce que j'ai (la branche js). Pour que les deux soient cohérent on modifie le fichier pour qu'il intègre nos modifications conjointes :

<!doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Ma maison page</title>

    <link href="main.css" rel="stylesheet">
</head>
<body>
<h1>Hello World !</h1>
<h2>et bonjour Monde !</h2>
<p id="couleur">Comment allez-vous ?  Bien ou quoi ?</p>

<script src="main.js"></script>
</body>
</html>

On peut ensuite l'ajouter au stage pour signifier à git qu'on a résolu son problème : git add index.html et on continue jusqu'à la fin ou jusqu'au nouveau problème : git rebase --continue. Il n'y a plus d'erreur et on arrive dans vim pour donner le message de commit. On laisse celui par défaut et on obtient la jolie liste de commit suivant (git log --oneline --all --graph):

* fe850ac (HEAD -> js) add js
* a2ac886 (main) parlons jeune
* 35609dd add .gitignore
* 7d2fd72 h2 rule in css and p in html
* a3c3fdc add css and link to html
* 11f5564 add french
* 3b8c0a8 First commit !

On est passé de ça :

A---B---C---D ← main
         \
          F---G ← js

à ça :

A---B---C---D ← main
             \
               F'---G' ← js

Il ne nous reste plus qu'à fusionner js dans main (ce qui devrait se faire sans soucis puisqu'elles se suivent). Pour cela :

  1. on se place sur la branche main : git checkout main
  2. on fusionne la branche js sur main : git merge js

Un it log --oneline --all --graph montre que les deux branches sont identique, on peut maintenant supprimer la branche js : git branch -d js

Merge

"Merger" revient à fusionner une branche dans une autre. Si les deux branches ne sont pas linéairement dépendante, par exemple comme ça :

A---B---C---D ← main
         \
          F---G ← js

Le résultat du merge sera :

A---B---C---D---H ← main
         \     /
          F---G ← js

Ce qui induit des "boucle" et n'est pas pratique lorsque l'on veut connaître l'historique du projet. Une droite c'est mieux pour voir ce qu'il s'est passé. Dans l'exemple ci-dessus, un même fichier a pu être modifié en D et en G avant d'être fusionné.

On évitera donc au maximum un merge comme ça et on ne l'utilisera que si les branches sont linéairement dépendante comme çà :

A---B---C---D ← main
             \
              E---F ← js

Rendant l'historique lisible et le merge facile (c'est le rebase qui pourra être compliqué)

Revenir en arrière dans l'historique

Il arrive parfois qu'on a complètement raté un truc et que l'on veuille revenir en arrière dans le projet. C'est super car c'est justement là où git est fort.

Attention cependant, ces opérations modifient l'historique du projet, chose que l'on aime pas trop faire. Il est donc recommandé de ne faire ça que sur des commits qui n'ont pas été publiés sur l'origin.

Je ne vais ici que résumer les possibilités. Allez voir sur cette vidéo pour avoir un aperçu de ce que l'on peut faire en situation, ou encore cette doc, plus velue.

Pour l'instant l'historique de notre projet est (git log --oneline) :

fe850ac (HEAD -> main, origin/main) add js
a2ac886 parlons jeune
35609dd add .gitignore
7d2fd72 h2 rule in css and p in html
a3c3fdc add css and link to html
11f5564 add french
3b8c0a8 First commit !

C'est quand on veut faire ce genre de chose ou que l'on cherche quand un bug a été introduit dans le projet que l'on est bien content d'avoir mis des message de commit explicite.

Il y a 3 commandes git qui permettent de gérer l'historique reset, restore et revert, chacune a ses spécificités et ses utilités.

la commande git reflog montre tous les commits du projet. Cela peut être super utile si on a fait un git reset à un ancien commit et que l'on veut finalement revenir à un endroit dans le futur par rapport à ce commit.

On va voir plusieurs cas pratiques :