Git - 1 - Concepts de base

git init, git status, git add, git commit, git log, git checkout, git tag

Dans cette partie, nous allons voir les concepts et les commandes de base : les dépôts, les commits, les branches. Lecture indispensable ! (ça devrait être enseigné dès le primaire)

Dépôt

git init, git status

Imaginons le projet avec l’arborescence suivante :

myproject/
└── toto.txt

Pour sauvegarder le contenu de dir, nous allons créer un dépôt (repository ou repo) git.
Nous obtenons l’arborescence suivante :

myproject/ => Arbre de travail
├── toto.txt
└── .git => Dépôt

Toutes les informations de versionnement seront stockées le dossier .git. Ce dossier est appelé dépôt. Il pourrait être stocké n’importe où.
Le dossier myproject est l’arbre de travail. C’est dans ce dossier qu’on peut apporter des modifications à nos fichiers et les versionner.

Le fichier toto.txt n’aura pas besoin de changer de nom.

cone TP Créer et initialiser son projet

Ouvrir un terminal

mkdir myproject # Créer le dossier
cd myproject # Aller dans le dossier
echo "bonjour" > toto.txt # Créer un fichier toto.txt
git init # Initialiser le projet en tant que dépôt
ls -a # Vérifier l'existence du dossier .git
# .  ..  .git  toto.txt

git status # Afficher le statut de l'arbre de travail

Sortie :

Sur la branche main

Aucun commit

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

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

Pas génial, ya du rouge. La suite dans le TP suivant.

Commit (révision/version)

git add, git commit, git log

Le repo est initialisé, mais rien n’a encore été versionné dans le repo.
Dans git, une version/révision est appelée un commit.

Pour créer un commit, il y a deux étapes :

  1. git add a deux fonctions :
    • ajouter des nouveaux fichiers à l’index des fichiers suivis par git
    • stocker les modifications dans une zone intermédiaire, la staging area (zone de transit) qui nous permet de faire de la vérification sur les fichiers qu’on intègre dans une révision.
  2. git commit : sauvegarde les modifications de tous les fichiers présents dans la staging area dans le dossier .git/ et produit un commit avec une clé SHA1.
 ┌────────────┐┌────────────┐┌──────────┐
 │Working Tree││Staging Area││Local Repo│
 └─────┬──────┘└─────┬──────┘└────┬─────┘
       │             │            │      
       │   git add   │            │      
       │────────────>│            │      
       │             │            │      
       │             │ git commit │      
       │             │───────────>│      
       │             │            │      
       │   git checkout/switch    │      
       │<─────────────────────────│      
       │             │            │      
       │        git merge         │      
       │<─────────────────────────│      

Un fichier peut être référencé dans 3 zones : l’arbre de travail, la zone de transit et le dépôt.
Pour le versionnement, il devra passer par ces trois zones successivement.

A chaque fois que vous versionner grâce aux commandes git add + git commit, vous créez un nouveau commit qui s’ajoute dans une chaîne, une lignée de commits.

┌────────┐
│commit C│ C hérite de B
└┬───────┘
┌▽───────┐
│commit B│ B hérite de A
└┬───────┘
┌▽───────┐
│commit A│
└────────┘

Exemple de graphe de lignée de commits. Le commit le plus récent est C, B est son commit parent. A est le commit le plus ancien.

Plus précisément, pour chaque commit, il se passe ceci :

  • Git versionne sur l’intégralité de l’arbre de travail. Lors de la création d’un commit, git fait une photographie de l’instant en stockant les modifications apportées dans le projet dans le dossier .git/ et en attribuant à cette photographie un identifiant cryptographique unique SHA1 (ex: 338b0b72fefb35d6e374127768aed10642aada0b, raccourci en 7 caractères 338b0b7)
  • Chaque commit est associé à un ou plusieurs commits parents ce qui nous permettra de retrouver l’historique.

🟢 Conseil :

C’est à vous de choisir à quel moment versionner votre travail. Il n’y a pas vraiment de convention. Considérez simplement un moment qui est un point de sauvegarde pour votre projet. Vous pouvez versionner autant que vous voulez, git est très robuste quant au nombre de commits.

cone TP Versionner son projet

git add toto.txt # Ajout à l'index et stockage des modifications dans la staging area 
git commit -m "Mon premier commit" # Création d'un commit avec un message
git log # Visualisation de l'historique de commits

Sortie :

commit 34c9d3c50746bf01df0e3e7a9ec9c59f2803aa17 (HEAD -> main)
Author: Léo Le Lonquer <leo.le.lonquer@bidulon.fr>
Date:   Thu Apr 4 09:50:48 2024 +0200

    Mon premier commit

Amusez-vous à modifier le fichier toto.txt, à le versionner, à vérifier l’état de votre repo avec git status et à regarder les logs avec git log.

Branches et arborescence 🌿

Branchage et merge

git branch, git switch, git merge

Pour l’instant nous étions sur un repo très simple. Imaginons que nous voulons ajouter une fonctionnalité sans casser l’état actuel de notre projet. Nous pouvons créer une branche qui permettra de travailler à côté sur cette nouvelle fonctionnalité tranquillement.

  • Nous pouvons créer autant de branches que souhaitées, à partir de n’importe quelle branche. Vous allez donc créer une arborescence, qui s’apparente à un arbre généalogique.
  • Par défaut, nous sommes la branche principale main (master pour des projets plus anciens).
      E---F---G topic
     /
A---B---C---D main

Graphe d’un repo présentant une branche topic issue de la branche main

Une fois que les fonctionnalités sont développées et matures, nous pouvons ramener les modifications d’une branche dans une autre. C’est ce qu’on appelle un merge.

      E---F---G topic
     /         \
A---B---C---D---H main

Exemple de graphe après un merge

cone TP 1 Créer une branche, basculer dessus et observer les effets du basculement

Toujours avec le même projet

git branch topic # Créer la branche à partir du dernier commit
git switch topic # changer de branche
# Ou les deux commandes en une
git switch -c topic

git branch # Afficher les branches existantes
#   main
# * topic

Vous ne verrez pas de différences au premier abord. Ajoutons un fichier et versionnons l’état.

echo "coucou" > tutu.py # Créer un nouveau fichier
git add . && git commit -m "Premier commit de la branche topic"
ls 
# toto.txt  tutu.py

Une fois commité revenons à main.

git switch main
ls # Où est passé le fichier `tutu.py` ?
# toto.txt

git log

Vous ne verrez pas le commit avec le message “Premier commit de la branche topic” car vous êtes sur la branche main qui n’a pas encore intégré les modifications faites sur topic (heureusement !).

Rassurez-vous le fichier tutu.py n’est pas perdu, il est simplement dans le dossier .git/ et attend que vous reveniez le voir. Git s’est chargé de modifier votre espace de travail courant pour revenir à un espace de travail correspondant à une autre branche.

git switch topic # Revenons à la branche topic
ls # Le fichier tutu.py réapparaît
# toto.txt  tutu.py

coneTP 2 Faire un merge

Maintenant imaginons que nous avons terminé le développement de notre fonctionnalité, nous voulons réintégrer les développement dans la branche principale.

Le graphe de commit est dans l’état suivant :

      B topic
     /
0---A main

Exécutons le merge

git switch main 
git merge topic --no-ff # l'option --no-ff est pour la cosmétique du graphe de commits, elle n'est pas utilisée usuellement
ls # tutu.py est présent

git crée un nouveau commit appelé merge commit pour signaler que vous avez intégré les modifications du dernier commit de la branche topic.

Le graphe de commits se retrouve dans l’état suivant :

      B topic
     / \
0---A---C main

Les références (branche-HEAD-tag)

git log –graph, git checkout, git tag

Les références sont des pointeurs qui identifient un commit dans un graphe de commits. Il existe trois types de référence :

  • HEAD : c’est la référence au dernier commit. A chaque commit, il est mis à jour. Il peut avoir deux états :
    • attaché : il pointe vers une branche qui elle-même pointe vers un commit
    • détaché : il ne pointe vers aucune branche, mais vers n’importe quel commit du graphe
  • les branches : les branches ne sont en réalité que des pointeurs. Elles identifient un dernier commit d’une chaîne de commits. L’héritage se fait de proche en proche (rappelez-vous les commits stockent leur commit parent.). Si HEAD est associé à la branche, alors la référence de la branche est mise à jour lors d’un commit.
  • tag : référence immutable (qui n’est jamais mis à jour) permettant de labeliser des commits plus importants que d’autres (ex : une version livrable du projet).
                 ┌────────────────────────┐
                 │commit W (topic <= HEAD)│
                 └┬───────────────────────┘
┌─────────────────▽┐                                 
│commit X (main)   │                                 
└┬─────────────────┘                                 
┌▽───────┐                                           
│commit Y│                                           
└┬───────┘                                           
┌▽───────┐                                           
│commit Z│                                           
└────────┘                                           

Exemple de graphe et des pointeurs

cone TP Se balader dans le graphe des commits

Vous pouvez afficher le graphe de commits ainsi que l’état des pointeurs

git log --oneline --decorate --all --graph

Sortie :

*   316d42d (HEAD -> main) Merge branch 'topic'
|\  
| * f6c8d25 (topic) Premier commit de la branche topic
|/  
* 65b6302 Mon premier commit

Nous observons le graphe résultant du merge précédent.
Déplaçons nous dans un commit :

git checkout 65b6302
ls
# toto.txt

Réexaminons la carte :

# Revoir la carte et regarder où est placé le point HEAD
git log --oneline --decorate --all --graph 

Sortie :

*   316d42d (main) Merge branch 'topic'
|\  
| * f6c8d25 (topic) Premier commit de la branche topic
|/  
* 65b6302 (HEAD) Mon premier commit

Nous observons que le pointeur HEAD s’est déplacé sur le commit 65b6302. Nous retrouvons uniquement les fichiers qui correspondent à ce commit.

Replaçons HEAD à son pointage précédent

git checkout - 
git log --oneline --decorate --all --graph 

Nous retrouvons le premier graphe.

Convention de nommage de branche

Les branches peuvent être utilisées et nommées très librement. Cependant quelques conventions ont été créées pour que les développeurs retrouvent leurs marques entre différents projets. Je vous présente ici la nomenclature git-flow :

  1. la branche main ou master : c’est la branche principale, de référence, qui reproduit la production. Elle doit toujours être fonctionnelle.
  2. la branche dev ou develop : c’est la branche de développement, celle qui va proposer une nouvelle version d’un logiciel et dans laquelle on va merger toutes les features.
  3. feature : ce sont les branches d’ajout de fonctionnalités. Elles sont nommées généralement feature/[ma_fonctionnalite] (ex : feature/add_refresh_button). Dans certaines organisations, on référence le numéro de ticket correspondant à la feature (ex: feature/DTL-92_add_refresh_button voire même simplement feature/DTL-92)
  4. hotfix : pour corriger un bug dans la branche de production main et la reporter dans dev

Pour des développements simples, si vous êtes le seul contributeur, vous pouvez utiliser seulement main ou main et dev par exemple.
Vous pouvez aussi bien sûr créer votre propre convention, faites en sorte qu’elle soit la plus collective possible ! Et rappelez-vous que le principal c’est de rester branché 😎.

Bonnes pratiques sur les branches

De manière générale :

  • une personne par branche (créer une autre branche à partir d’une autre si besoin)
  • une branche par sujet : ajout/modification d’une configuration, ajout/modification d’une fonctionnalité
  • Résoudre les merge conflicts dans une branche plutôt que dans la branche principale (ramener dans la branche les modifs de la branche principale avec git merge, expliqué plus bas).
  • Utiliser git merge et non git rebase

Fichiers spéciaux

Le fichier .gitignore

Le fichier .gitignore permet d’éviter l’indexation de certains documents sur le critère de patterns. Il se trouve à la racine de l’arbre de travail. On peut ignorer :

  • des fichiers spécifiques
  • des patterns de fichiers
  • des dossiers

Exemple de fichier .gitignore :

# Ignorer les fichiers de configuration de l'environnement de développement
.idea/
.vscode/
.classpath
.project

# Ignorer les fichiers de sauvegarde temporaires vim
*~
*.swp

# Ignorer les fichiers de logs
*.log

# Ignorer certains dossiers
lib/
var/
tmp/
tdsConfigGen/

Une fois que notre fichier est bien configuré, on peut indexer et commiter plus rapidement sans se soucier de l’indexation de fichiers inutiles :

git add . && git commit -m "Mon message"

Vous pouvez demander à ChatGPT ou MistralAI (pour défendre une solution française et open-source) de vous fournir un fichier .gitignore en précisant le langage, les librairies, les modules, le IDE utilisées.

Le fichier .gitattributes

Si vous avez des problème de fin de lignes, car un des contributeurs.rices du projet est sur Linux quand l’autre est sur Windows, alors ajouter le fichier .gitattributes à votre projet avec le contenu suivant :

# Adaptation automatique aux fins de ligne Windows ou Linux
* text=auto

Voir également cet exemple de fichier .gitattributes généré par Codestral.

La suite

La suite ici : Git - 2 - Actions fréquentes

Ressources