Comment maintenir un dépôt correctement ? Résoudre des conflits de branches, explorer l’historique, restaurer des versions antérieures ? Tous mes secrets présentés ici.
Git possède un panel de commandes très larges et il est facile de s’y perdre. Prenons un peu de recul et associons une fonctionnalité à un lot de commandes git. Voyons donc ici les fonctionnalités les plus utilisées pour maintenir un dépôt et les commandes associées.
Récupérer des modifications
git switch, git pull, git fetch, git merge
Imaginons que quelqu’un·e d’autre ait terminé un développement et que nous souhaitions récupérer son travail déjà mergé dans la branche develop (la branche qui doit être la plus à jour si on suit le git workflow). Dans cette partie, j’anticipe un peu sur l’article suivant qui parlera du repo distant mais c’est une action très courante.
Pour créer une nouvelle branche à partir des dernières mises à jour de develop :
git switch develop
git pull # Récupérer les modifications distantes
git switch -c manouvellebranche
Si vous êtes en train d’éditer une branche et que vous voulez récupérer les mises à jour de develop, il va être nécessaire de committer vos modifications pour qu’un merge puisse avoir lieu :
git add . && git commit -m "Mon message"
git fetch --all
git merge develop
Merge conflicts
git merge
Dans la section précédente, nous effectuons un merge avec la branche develop. Parfois ça fonctionne tout seul, parfois on tombe sur un merge conflict et on panique. Voyons quand et comment se crée un merge conflict et comment le résoudre.
=> Mettre une image d’une ombre terrifiante projetée depuis un lapin tout mignon.
Un merge conflict est un conflit de versions qui se déclare au moment d’un merge entre deux branches ou d’une Merge Request et lorsque les branches ont divergé.
Les branches divergent à deux conditions :
- Les deux branches ne partagent pas certains commits dans leur historique
- Dans ces commits non partagés, des modifications ont été apportées sur les mêmes fichiers et mêmes lignes
Un arbitrage va être nécessaire pour décider de la version à garder. Une fois cet arbitrage effectué les historiques seront resynchronisés.
Exemple :
Imaginons le fichier initial hello.py
suivant dans un repo quelconque :
def say_hello():
print("Bonjour")
Miguel modifie sur la branche main
tandis que Maria crée sa branche maria
. Tout deux commitent leurs modifications.
Modifications de Miguel | Modifications de Maria |
---|---|
print("Buenos dias") |
print("Buongiorno") |
Avant le merge nous sommes donc dans cette situation.
C maria
/
0---A---B main
Les conditions sont réunies pour créer un merge conflict car les branches ont divergé :
B
etC
sont des commits qui ne sont pas partagés parmaria
etmain
B
etC
présentent chacun des modifications du même fichierhello.py
et des mêmes lignes
Miguel récupère la branche de Maria et tente de faire un merge :
=> Conséquence : Merge Conflict !!!
TP Créer et résoudre un merge conflict
Créer un nouveau repo et préparer un merge conflict
mkdir hello_project
cd hello_project
# Création commit initial
git init
echo -e 'def say_hello():\n print("Bonjour")' > hello.py
git add hello.py && git commit -m "First commit"
# Modifications de Maria
git switch -c maria
echo -e 'def say_hello():\n print("Buongiorno")' > hello.py
git add hello.py && git commit -m "Maria commit"
# Modifications de Miguel
git switch main
echo -e 'def say_hello():\n print("Buenos dias")' > hello.py
git add hello.py && git commit -m "Miguel commit"
# Affichage du graphe des commits
git log --oneline --decorate --all --graph
Sortie :
* 11741dd (HEAD -> main) Miguel commit | * 58808c5 (maria) Maria commit |/ * 9c4b795 First commit
Notre projet est mature pour le merge. Allons-y :
# Fusion de maria dans main
git merge --no-ff maria # L'option --no-ff n'est pas nécessaire, ici ce n'est que cosmétique pour le graphe de commits
# Fusion automatique de hello.py
# CONFLIT (contenu) : Conflit de fusion dans hello.py
# La fusion automatique a échoué ; réglez les conflits et validez le résultat.
Mince un conflit apparaît regardons-ça de plus près :
# Affichage du fichier hello.py
vim hello.py
Sortie :
def say_hello():
<<<<<<< HEAD
print("Buenos dias")
=======
print("Buongiorno")
>>>>>>> maria
Git a écrit directement dans le fichier et a indiqué les conflits de versions. Il va falloir arbitrer. Pour cela il faut ouvrir le fichier et supprimer à la main les lignes que l’on ne souhaite pas garder ou faire les modifications qui nous conviennent.
# Au choix :
# Ah mince la boulette ! Revenir au stade avant le merge
git merge --abort
# Ou faire un arbitrage et supprimer la section qui ne nous convient pas
nano hello.py
vim hello.py
gedit hello.py
# Faire un commit de merge
git add hello.py && git commit -m "Merge conflict solved"
git log --oneline --decorate --all --graph
Sortie :
* 6f128c4 (HEAD -> main) Merge conflict solved |\ | * 58808c5 (maria) Maria commit * | 11741dd Miguel commit |/ * 9c4b795 First commit
Nous sommes carrés !
Bonnes pratiques
- On peut également utiliser des outils graphiques pour la résolutions de conflits comme meld ou des éditeurs comme VSCode (ou VSCodium), Intellij, PyCharm …
- Je vous conseille également fortement d’utiliser
git worktree
pour la gestion des branches et éviter des merge conflicts tout à fait inutile du à des switch entre branches. En savoir plus sur cet article Git worktree : se dénouer les branches
Par rapport aux branches
On résout les merge conflict dans les branches feature plutôt que dans la branche main pour ne pas pourrir la branche main.
L’idée est de rapatrier toutes les modifications qui ont eu lieu dans la branche principale et de résoudre les problèmes dans un endroit qui n’impacte pas la production.
Donc de manière générale, avant de merge une feature dans un main, faire le merge de main dans feature puis feature dans main.
Exemple :
git switch main
git merge maria
# CONFLIT
git merge --abort
git switch maria
git merge main
# CONFLIT
vim hello.py
git add hello.py && git commit -m "Merge conflict solved"
git switch main
git merge maria
Explorer une version précédente
git checkout
Si vous souhaitez juste explorer une version précédente sans créer de commit
git log --oneline
git checkout SHA
git checkout - # Revenir à la version courante
# ou
git checkout -b revert/mybranch # Créer une branche à partir de la version précédente
Afficher des différences
git diff
Un besoin général de tous les gestionnaires de version est de regarder les différences qui ont été apportées au projet. On passe pour cela par la commande git diff
. Les commandes git diff
, git log
, git grep
vont souvent être utilisées de pairs
La syntaxe de la commande est la suivante, on la retrouve pour plusieurs autres commandes (git log
, git reset
) :
git diff (--options) mes_refs_de_branches (-- <mes_chemins> )
où :
()
: signifie que le contenu est optionnel--options
désigne toutes les options disponibles degit diff
mes_refs_de_branches
désigne des références à des commits (SHA de commits, branches, tags,HEAD~3
,HEAD@{3 months|weeks|days|hours|seconds ago}
indistinctement. On peut en spécifier de 0 à l’infini.--
permet d’indiquer que les prochains arguments doivent être interprétés comme des chemins (et non des arguments ou options)mes_chemins
désigne des dossiers ou des fichiers, on peut en spécifier de 0 à l’infini.
Je vous mets plein d’exemples, une fois qu’on a compris la logique c’est simple !
# Soient X et Y des références (commits, branches, tags, HEAD~3, HEAD@{3 months|weeks|days|hours|seconds ago} indistinctement)
# Soient cheminA et cheminB des chemins vers des dossiers ou fichiers
git diff # Différences entre les fichiers dans le working tree qui n'ont pas subi git add et HEAD (le dernier commit)
git diff --cached # Différences entre les fichiers dans la staging area (qui ont subi git add) et HEAD (le dernier commit)
git diff HEAD # Différences locales (càd de tous les fichiers modifiés càd ceux qui ont ou pas subi `git add`) par rapport à HEAD (le dernier commit)
git diff X # Différences locales par rapport à X
git diff --name-only X # Uniquement les noms des fichiers différents par rapport à X
git diff --name-status X # Uniquement les noms des fichiers différents par rapport à X avec leur statut (D=deleted, U=Updated, A=Added)
git diff --summary X # Créations, renommages et changements de droits de fichiers par rapport à X
git diff X -- chemin # Différences par rapport à X pour un chemin spécifiquement (dossier ou fichier)
git diff HEAD Y # Différences du dernier commit par rapport à Y
git diff X Y # Différences de X par rapport à Y
git diff X Y -- chemin # Différences de X par rapport à Y pour un chemin spécifiquement (dossier ou fichier)
git diff X Y -- cheminA cheminB # Différences de X par rapport à Y pour deux chemins spécifiquement (dossiers ou fichiers)
git diff X:cheminA Y:cheminB # Différences entre le cheminA de X et cheminB de Y (dossiers ou fichiers)
# Git diff en dehors d'un repo (parce que la coloration est jolie)
git diff --no-index --word-diff cheminA cheminB
Pour --name-status
, voir la liste des symboles correspondants sur la documentation git short-format.
TIP : utiliser les tags lorsque vous mettez en production votre appli pour avoir une référence facilement retrouvable.
TP Tester
git diff
Créer un repo, et plusieurs commits
mkdir time_project
cd time_project
# Création commit initial
git init
echo -e 'bonjour' > poesie.txt
git add poesie.txt && git commit -m "First commit"
echo -e 'coucou' >> poesie.txt
git add poesie.txt && git commit -m "Second commit"
echo -e 'hello' >> poesie.txt
git add poesie.txt && git commit -m "Third commit"
cat poesie.txt
git log --oneline
# a9d4887 (HEAD -> main) Third commit
# 5c2c3f7 Second commit
# d9d039d First commit
Tester des différences
git diff HEAD~1 # Différences entre les fichiers locaux et l'avant dernier commit (5c2c3f7)
# diff --git a/poesie.txt b/poesie.txt
# index 680f2da..adaea46 100644
# --- a/poesie.txt
# +++ b/poesie.txt
# @@ -1,2 +1,3 @@
# bonjour
# coucou
# +hello
git diff --name-only 5c2c3f7 # Noms uniquement des fichiers différents entre les fichiers locaux et l'avant dernier commit
# poesie.txt
git diff --name-status HEAD~1 # Noms et statuts uniquement des fichiers différents entre les fichiers locaux et l'avant dernier commit
# M poesie.txt
git diff HEAD~2 HEAD~1 # Différences entre l'avant-avant dernier commit et l'avant-dernier
# diff --git a/poesie.txt b/poesie.txt
# index 1cd909e..680f2da 100644
# --- a/poesie.txt
# +++ b/poesie.txt
# @@ -1 +1,2 @@
# bonjour
# +coucou
git diff HEAD~2 HEAD~1 -- poesie.txt # Idem que ci-dessus, mais seulement pour le fichier poesie.txt (pas pour l'ensemble du projet)
# Même sortie que ci-dessus
Faire une modification pour tester git diff
de base :
echo -e "ni hao" >> poesie.txt
git diff
# index adaea46..6827c5d 100644
# --- a/poesie.txt
# +++ b/poesie.txt
# @@ -1,3 +1,4 @@
# bonjour
# coucou
# hello
# +ni hao
git add poesie.txt
git diff
# Aucune sortie
git diff --cached
# même print que ci-dessus
git diff HEAD
# même print que ci-dessus
Supprimer toutes les modifs courantes
git reset, git clean
Pour supprimer toutes les modifications de tous les fichiers du repo qui n’ont pas encore été commitées (que ce soit les fichiers staged, unstaged et untracked), il faut utiliser plusieurs commandes :
git reset --hard # Supprime toutes les modifs des fichiers suivis
git clean -fd # Supprime tous les nouveaux fichiers non suivis
Franchement c’est compliqué pour quelque chose de simple. Dans VsCode, il y a un bouton “Discard all changes” qui fait exactement le taff. On pourra créer un alias pour faire une seule commande à partir des deux. On verra ça dans le dernier article.
TP Tester les commandes
Depuis le repo précédent tester la commande.
git status
git reset --hard
git clean -fd
git status
Suivre l’historique
git log
De même que git diff
, on peut filtrer git log
sur des références de commits ou sur des fichiers ou des dossiers spécifiquement.
git log release..test # Afficher les commits de la branche test qui ne sont pas dans la branche release
git log --oneline -- etc/ # Lister les commits où sont modifiés des fichiers dans le dossier etc/
git log --name-status # Afficher les fichiers modifiés et leur statut pour chacun des logs
git log --stat # Un peu différent de --name-status avec le nombre d'additions, de modifications, ou de suppressions de lignes
git log -u/-p/--patch -- poesie.txt # Afficher toutes les modifs du fichier poesie.txt
TP Faites des tests wesh
Rechercher dans l’historique du repo
git log
La commande git log
est pleine de surprise ! Elle permet également de chercher dans l’historique des fichiers des chaînes de caractères spécifiques.
git log -p -G coucou # -p pour --patch permet d'afficher les différences et -G pour regex (-P pour des regex perl)
# diff --git a/poesie.txt b/poesie.txt
# index 1cd909e..680f2da 100644
# --- a/poesie.txt
# +++ b/poesie.txt
# @@ -1 +1,2 @@
# bonjour
# +coucou
git log -i/--regexp-ignore-case --grep Second # Lister les commits dont le message contient 'Second'
# 4766ce0 Second commit
La commande git grep
est assez décevante. Elle n’est pas faite vraiment faite pour chercher dans l’historique mais seulement dans les fichiers suivis.
TP Faites des tests wesh
Revenir à une version précédente
git revert, git checkout, git restore
Si git est un outil de versionnement, alors une de ses fonctionnalités principales est de pouvoir rétablir des versions précédentes (rollback). Cependant toutes les manières de visiter le passé ne sont pas bonnes à prendre.
=> mettre une image de paradoxe temporel
TP Faire un rollback de son projet
Créer un repo, créer plusieurs commits et revenir à une version plus ancienne
Si cette partie création du repo time_project dans la partie “Afficher l’historique” a déjà été faite, supprimer toutes les modifs en cours avec
git restore .
mkdir time_project
cd time_project
# Création commit initial
git init
echo -e 'bonjour' > poesie.txt
git add poesie.txt && git commit -m "First commit"
echo -e 'coucou' >> poesie.txt
git add poesie.txt && git commit -m "Second commit"
echo -e 'hello' >> poesie.txt
git add poesie.txt && git commit -m "Third commit"
cat poesie.txt
git log --oneline
# a9d4887 (HEAD -> main) Third commit
# 5c2c3f7 Second commit
# d9d039d First commit
Imaginons que nous souhaitons revenir au premier commit (d9d039d
).
Nous allons utiliser la méthode git revert
qui permet d’inverser les modifications d’un commit. En lui donnant une série de commits dans le bon ordre, on peut ainsi inverser une série de commit et revenir à un état précédent.
git revert d9d039d.. # d9d039d.. désigne un intervalle entre d9d039d et HEAD (d9d039d non compris)
cat poesie.txt
# First commit
git log --oneline
# 4e35ad9 (HEAD -> main) Revert "Second commit"
# b78f8c1 Revert "Third commit"
# a9d4887 Third commit
# 5c2c3f7 Second commit
# d9d039d First commit
Pour faire simple, voici ci-dessous la commande que je recommande pour 90% des usages qui permet :
- de créer un commit de rollback en intégrant tous les changements depuis (
..
) le commit5c2c3f7
inclu - sans créer automatiquement le message de commit (
--no-edit
)* - sans créer les commits pour chacun des commits inversés (
--no-commit
)
git revert --no-edit --no-commit 5c2c3f7..
git commit -m "Rollback to commit 5c2c3f7 (Second commit)"
# Commande générale
git revert --no-edit --no-commit <SHA>.. # <SHA> : numéro de commit spécifiant celui auquel vous souhaitez revenir
git commit -m "Rollback to commit <SHA>"
Il y a plein d’autres manières de faire, je vous ai fait un tableau. Vous pouvez les tester.
git revert |
git log --oneline |
Commentaire |
---|---|---|
d9d039d..HEAD d9d039d.. HEAD~2..HEAD HEAD~2.. a9d4887 5c2c3f7 HEAD HEAD~1 |
4e35ad9 (HEAD -> main) Revert "Second commit" b78f8c1 Revert "Third commit" a9d4887 Third commit 5c2c3f7 Second commit d9d039d First commit |
En entrée on peut donner - un intervalle (..) absolu avec numéro de commit ou relatif par rapport à HEAD - un ensemble de commit à inverser avec référence absolue ou relative. Dans les deux cas, pour chaque commit inversé, on obtient un nouveau commit. |
--no-commit d9d039d..HEAD -no-commit d9d039d.. … + git commit -m "All reverts in one commit" |
1f4f434 (HEAD -> main) All reverts in one commit a9d4887 Third commit 5c2c3f7 Second commit d9d039d First commit |
Idem sauf qu’ici on a pas un commit pour chaque revert |
TP Faire un rollback uniquement d’un fichier
J’aurais bien aimé que ce soit la même commande git revert
, mais il faut en utiliser une autre (deux commandes au choix)
git checkout d9d039d -- poesie.txt
git commit -m "Rollback of poesie.txt to d9d039d (First commit)"
# ou
git restore --source d9d039d poesie.txt
git add poesie.txt && git commit -m "Rollback of poesie.txt to d9d039d (First commit)"
La suite
La suite ici : Git - 3 - Gitlab
Ressources
- Présentation animée des principales commandes git : CS Visualized: Useful Git Commands
- Codes de triche Git : Gitlab Cheatsheet
- Liste des commandes Git : git reference