Travaux pratiques -- Git

Avertissement : pour ce TP, nous vous demanderons de travailler en groupes de deux personnes (une par ordinateur). En effet, les exercices deux et quatre demanderont de travailler à plusieurs sur un dépôt distant collaboratif commun.

L'objectif de ce TP est de vous apprendre à manipuler Git, un logiciel libre et open-source de gestion de versions décentralisé. En cas de besoin, vous pourrez vous référer tout au long de ce TP aux notes de cours associées.

Le TP est composé de quatre exercices. Le premier vous introduira à la création de vos premiers commits, ainsi qu'à la lecture et navigation de l'historique ; puis vous fera créer et manipuler vos premières branches, et vous apprendra la résolution de conflits lors d'une fusion. Le second consistera à collaborer sur un dépôt distant.

Les deux derniers exercices sont des exercices avancés facultatifs. Les étudiant·e·s connaissant déjà les manipulations de Git évoquées dans les deux premiers exercices sont vivement encouragé·e·s à aller directement aux troisième et quatrième exercices : ceux-ci permettent de développer une compréhension plus profonde des manipulations de Git, et peuvent aborder des notions plus complexes (comme « rebase »).

L'archive associée à cet énoncé est disponible ici.

Exercice 1 : Apprendre à utiliser git

Incantations préliminaires

Avant de lancer Git pour la première fois, un peu de configuration est requise. Tout d'abord, Git demande aux commits d'être signés par le nom de l'auteur et un email. Pour configurer ces deux paramètres, vous pouvez entrer les commandes suivantes (en remplaçant évidemment <name> et <email> par les données vous concernant) :

$ git config --global user.name "<name>"
$ git config --global user.email "<email>"

Certaines commandes utilisées durant le TP, comme git commit ou git merge, demandent la saisie d'un message à l'utilisateur. Git vous laisse la possibilité de choisir l'éditeur de texte qui s'ouvrira (emacs, vim, nano, gedit...) :

$ git config --global core.editor emacs

Attention, pour les utilisateurs de VSCodium, la commande à utiliser est :

$ git config --global core.editor "codium --wait"

Enfin, pour des raisons de politesse élémentaire, on va entrer la dernière commande pour définir le nom de la branche par défaut comme main:

$ git config --global init.defaultBranch main

Initialiser un dépôt

Téléchargez l'archive du TP et décompressez-là quelque-part. Déplacez-vous ensuite dans le dossier tp-git-exo1.

Le dossier tp-git-exo1 contient un fichier integers.py que vous allez éditer, et vous allez progressivement transformer ces modifications en commits. Mais avant toute chose, il faut initialiser un dépôt git dans le répertoire. Pour cela, à la racine de tp-git-exo1, vous pouvez entrer :

$ git init

Ce à quoi git vous répond :

Initialized empty Git repository in [...]

Félicitations, vous venez de créer votre premier dépôt git !

Prendre connaissance de l'exercice

L'objectif de l'exercice est de compléter le développement du fichier integers.py pour être capable d'effectuer quelques opérations basiques sur des listes d'entiers : afficher le minimum ou le maximum/minimum d'une liste d'entiers, ou encore renvoyer un élément choisi aléatoirement.

Un exemple typique d'utilisation est :

$ ./integers.py max 1 2 5 8 3 2
8
$ ./integers.py min 1 5 0 3 9 1
0
$ ./integers.py random 0 8 2 91 2 37
2

add et commit

Complétez le code de la fonction max_int(l), qui prend en entrée une liste d'entiers et retourne l'entier maximal apparaissant dans la liste. Vous pourrez pour cela utiliser la fonction Python max(l).

Utilisez la commande git status pour afficher l'état de votre dépôt. Git vous signale que le fichier integers.py n'est pas encore suivi. Utilisez git add pour ajouter le fichier integers.py à l'index. Refaites un git status pour regarder comment l'état de votre dépôt a changé.

Utilisez git commit pour transformer l'index en commit. Enregistrez ce premier commit avec un message descriptif. Utilisez git status pour vérifier l'état de votre dépôt.

Félicitations, vous venez de créer votre premier commit !

« Au secours, j'ai entré git config --global core.editor emacs lors de la configuration, mais en fait je ne sais pas quitter Emacs ! »

Pour enregistrer et quitter sous Emacs, vous pouvez successivement taper "ctrl+x ctrl+s ; ctrl+x ctrl+c".

log

Maintenant que le commit a été créé, la commande git status ne permet plus de visualiser les modifications de la fonction max_int(l). Pour afficher l'historique des commits créés (et donc visualiser celui que vous venez d'effectuer), utilisez la commande git log.

En cas d'utilisation de ./integers.py max sur une liste vide, la fonction max_int(l) va renvoyer None: il serait plus informatif d'afficher une erreur sur la sortie d'erreur. Dans la fonction main(), modifiez l'appel à la fonction max_int(l) pour renvoyer une erreur si l est la liste vide. Pour afficher un message sur la sortie d'erreur, vous pourrez utiliser print("My error message", file=sys.stderr).

Créez un commit contenant cette nouvelle modification.

Créer mes premières branches

Dans cette section, nous allons ajouter des fonctionnalités à notre script integers.py. Pour cela, comme souvent en git, nous allons utiliser des branches !

Créez une branche nommée min et une branche nommée random.

Déplacez-vous dans la branche min. Utilisez git status pour constater que vous avez bien changé de branche.

Complétez le code de la fonction min_int(l), qui prend en entrée une liste d'entiers et retourne l'entier minimal apparaissant dans la liste. Vous pourrez pour cela utiliser la fonction Python min(l).

Complétez le code de la fonction main() pour que le script, lorsqu'appelé sous la forme ./integers.py min, appelle la fonction min_int(l).

En utilisant la commande git status à chaque étape, ajoutez le fichier modifié integers.py à l'index et créez un nouveau commit. Visualisez le nouveau commit avec la commande git log.

Déplacez-vous dans la branche random. Utilisez git status pour constater que vous avez bien changé de branche. Constatez que les modifications de la fonction min_int(l) ont disparu du fichier integers.py.

Complétez le code de la fonction random_int(l), qui devra renvoyer un entier aléatoire choisi dans la liste l. Pour cela, vous pourrez utiliser la fonction random.choice(l), qui choisit aléatoirement un élément dans une liste donnée.

Complétez le code de la fonction main() pour que le script, lorsqu'appelé sous la forme ./integers.py random, appelle la fonction random_int(l).

Ajouter le fichier integers.py à l'index et créez un nouveau commit. Visualisez ce nouveau commit avec git log. Constatez que le commit créé pour la fonction min(l) n'apparaît pas.

Pour visualiser l'ensemble de l'historique, y compris les branches différentes de la branche courante, utilisez la commande git log --graph --all. L'option --oneline permet d'en afficher une version abrégée plus lisible.

Fusionner des branches

Nous allons maintenant ajoutez les deux fonctionnalités min et random à la branche principale main.

Déplacez-vous dans la branche main. Utilisez git status pour constatez que vous avez bien changé de branche.

Fusionnez la branche main et la branche min en utilisant git merge. Constatez que le merge est "fast-forward" : comme la branche main était incluse dans la branche min, la fusion est automatique ! Constatez que la fusion a bien fonctionné en affichant le graphe Git avec la commande git log --graph --all --oneline.

Fusionnez la branche main et la branche random. En cas de conflit, résolvez-les, ajoutez les fichiers modifiés à l'index, puis reprenez la fusion avec git merge --continue.

Affichez le graphe git en utilisant la commande git log --graph --all --oneline.

Et voilà : les fonctionnalités min et random apparaissent maintenant dans la branche principale main du projet. Félicitations : vous avez résolu vos premiers conflits en Git ! (Et maintenant, vous avez les bases pour presque tout faire !)

Quelques manipulations supplémentaires

Effacez le contenu du fichier integers.py (par exemple, echo "Bonjour!" > integers.py).

Annulez la modification de la question précédente en utilisant la commande git restore <file>.

A l'heure actuelle, l'utilisation de ./integers.py random sur une liste vide déclenche une exception Python. Modifiez votre fonction random_int(l) pour tester si la liste l est vide, auquel cas, vous afficherez un message sur la sortie d'erreur avec print("My error message", file=sys.stderr). Ajoutez le fichier à l'index et créez un commit.

(Exercice difficile) Ce commit aurait évidemment dû être effectué dans la branche random ! Annulez le dernier commit de la branche main pour le « déplacer » dans la branche random. Fusionnez ensuite votre branche random dans votre branche main. Attention : il vous est interdit de modifier le fichier integers.py avec un éditeur de texte ! Vous pourrez plutôt utiliser git stash pour enregistrer temporairement des données !

(Machine à voyager dans le temps) Saisi d'un doute, vous souhaitez vérifier l'état des fichiers à différents temps dans le passé. Déplacez-vous dans l'historique en utilisant git checkout et vérifiez que le fichier integers.py est bien modifié en conséquence.

Exercice 2 : Travail collaboratif

Cet exercice est un exercice collaboratif par groupe de deux étudiant·e·s.

Certaines questions de cet exercice sont à réaliser par l'un·e ou l'autre membre du groupe.

Utiliser le GitLab Unicaen pour la première fois

Rendez-vous sur https://git.unicaen.fr et authentifiez-vous via « Communauté Renater ».

Consultez votre profil. Vous y verrez votre nom, et votre identifiant (de la forme @username). Notez cet identifiant username de côté : il vous permettra de vous connecter à des dépôts distants git !

Dans « User settings », onglet « passwords », générez un nouveau mot de passe.

Votre identifiant et votre mot de passe vous permettront de vous authentifier lorsque vous utilisez Git en ligne de commande et que vous souhaiterez interagir avec un dépôt distant stocké sur https://git.unicaen.fr.

Vous pouvez aussi vous authentifier sur le portail GitLab en utilisant cet identifiant et ce nom d'utilisateur, au lieu de passer par l'authentification Renater.

Initialiser le dépôt distant

Placez-vous à la racine des exercices. Vous devez contenir trois dossiers : tp-git-exo1, tp-git-exo2 et tp-git-exo4.

(Membre A) Sur https://git.unicaen.fr, créez un nouveau « Blank project » (important : décochez « Initialize repository with a README »). Une fois le projet créé, dans les paramètres du projet (onglet « Manage »), invitez les autres membres de votre groupe et donnez-leur les permissions « Maintainer ».

(Tout le monde) Dans votre projet sur le portail GitLab, cliquez sur le bouton « Clone » et copiez l'url de « Clone with HTTPS ». (Par exemple, pour moi, il s'agit de https://git.unicaen.fr/antonin.callard/tp-git-exo2.git).

(Membre A) Déplacez-vous dans le dossier tp-git-exo2 et initialisez un nouveau dépôt Git. Ajoutez le fichier random_joke.py et faites un commit. Entrez ensuite les deux commandes suivantes (pour respectivement ajouter le nouveau dépôt distant au dépôt local, puis envoyer les modifications de la branche main):

git remote add origin <url-clone-with-https>
git push -u origin main

(Membre B) Supprimez le dossier tp-git-exo2, et clonez plutôt le dépôt distant en utilisant git clone <url-clone-with-https> tp-git-exo2.

Prendre connaissance du sujet

L'objectif de l'exercice est de compléter le développement du fichier random_joke.py, qui permet d'afficher une blague aléatoire. Un exemple typique d'utilisation sera :

$ # Afficher une blague aléatoire
$ ./random_joke.py random
I have got a ton of work done today.
 A skele-ton!
$ # Afficher deux blagues aléatoires
$ ./random_joke.py random -n2
I have got a ton of work done today.
 A skele-ton!
Why are graveyards so noisy?
 -> Because of all the coffin!
$ # Afficher une blague contenant le mot "Papyrus"
$ ./random_joke.py random "Papyrus"
Papyrus stood by the fire for too long.
 Now he's BONE-dry!

Pour bien commencer

(Membre A) Complétez le code de la fonction random_joke(), qui devra afficher sur la sortie standard une blague choisie aléatoirement dans la liste list_jokes. Vous pourrez utiliser la fonction random.choice(l), qui choisit aléatoirement un élément dans une liste donnée.

(Membre A) En compagnie de votre camarade, ajoutez le fichier random_joke.py à l'index et créez un nouveau commit. Utilisez la commande git log --oneline --all --graph pour remarquer que votre copie locale du dépôt git est plus avancée que celle du serveur. Pour mettre à jour le serveur, utilisez la commande git push.

(Membre B) En compagnie de votre camarade, utilisez la commande git log --all --graph pour comparer l'état de votre dépôt : une première fois maintenant ; puis après avoir utilisé la commande git fetch ; puis une dernière fois après avoir utilisé la commande git pull.

Travailler en parallèle

Dans cette section, le but est que les deux étudiant·e·s travaillent en parallèle afin d'implémenter des fonctionnalités différentes ! Ainsi, un·e étudiant·e réalisera les question marquées « Membre A », en même temps que l'autre réalisera les questions marquées « Membre B ».

Membre A :

(Membre A) Complétez la fonction __filter_joke(regex), qui renvoie la liste des blagues contenues dans list_jokes qui matchent l'expression rationnelle regex donnée en paramètre. Pour cela, décommentez les deux lignes de commentaires et supprimez l'instruction pass. Ajoutez les modifications à l'index et créez un commit.

(Membre A) Modifiez la fonction random_joke() en une fonction random_joke(regex), qui devra renvoyer une blague aléatoire qui matche l'expression régulière regex. Vous pourrez utiliser la fonction random.choice(l) sur le résultat renvoyé par __filter_joke(regex).

(Membre A) Modifiez votre fonction main() pour remplacer l'appel à random_joke() par random_joke("") (sur l'expression régulière vide), afin que votre programme soit toujours fonctionnel. Ajoutez les modifications à l'index et créez un commit.

(Membre A) Modifiez la fonction main() pour que l'appel ./random_joke.py random <regex> affiche une blague aléatoire sur la sortie standard qui matche l'expression régulière regex. Si <regex> est fourni, vous pourrez utiliser random_joke(regex); si <regex> n'est pas fourni, vous continuerez à utiliser random_joke(""). Vous pourrez consulter si <regex> est fournie en paramètre en consultant le contenu de la liste args.

(Membre A) Ajoutez les modifications à l'index et créez un nouveau commit.

(Membre A) Envoyez tout votre travail sur le serveur distant avec git push (attention : si votre camarade a été plus rapide que vous, cela ne fonctionnera pas !)

Membre B :

(Membre B) Modifiez la fonction random_joke() en une fonction random_joke(k), qui devra renvoyer k blagues aléatoires (potentiellement avec répétitions) parmi la liste list_jokes.

(Membre B) Modifiez votre fonction main() pour remplacer l'appel à random_joke() par random_joke(1), afin que votre programme soit toujours fonctionnel. Ajoutez les modifications à l'index et créez un commit.

(Membre B) Modifiez la fonction main() pour que l'appel ./random_joke.py random -nk affiche k blagues aléatoires sur la sortie standard. Vous pourrez évidemment faire appel à la fonction random_joke(k).

Indication : Si -nk est utilisé dans l'appel au script random_joke.py, cette option sera stockée dans la liste locale options de la fonction main. On rappelle qu'étant donné une chaîne de caractères s, il est possible d'en éliminer les deux premiers en utilisant s[2:].

(Membre B) Ajoutez les modifications à l'index et créez un commit.

(Membre B) Envoyez tout votre travail sur le serveur distant avec git push (attention : si votre camarade a été plus rapide que vous, cela ne fonctionnera pas !)

Résoudre les conflits lors d'un travail collaboratif

Après la fin de la partie précédente, une des deux modifications (ou bien l'ajout du paramètre -nk, ou bien l'ajout d'une expression régulière) n'a pas pu être envoyée sur le serveur. Vous allez maintenant vous placer à deux sur l'ordinateur contenant cette modification qui n'a pas été synchronisée et travailler à résoudre le conflit.

Utilisez git fetch puis git log --graph --oneline --all pour visualiser le problème.

Fusionnez la branche distante origin/main dans votre branche principale main. Résolvez les conflits dans le fichier random_joke.py, puis terminez la fusion avec git merge --continue.

Visualisez le graphe git avec git log --graph --oneline --all, puis envoyez votre travail sur le serveur distant avec git push. Constatez que, cette fois, cela a bien fonctionné. Pour cela, visualisez à nouveau le graphe git.

Sur l'autre ordinateur, visualisez le graphe git puis récupérez le contenu du serveur distant avec git pull. Constatez qu'il n'y a pas de conflits, puis visualisez à nouveau le graphe git.

Exercice 3 (Facultatif) : Learn Git Branching

Cet exercice est potentiellement vraiment très formateur pour les personnes qui souhaitent mieux visualiser les commandes Git en terme de manipulations du graphe associé !

Rendez-vous sur le site Learn Git Branching et travaillez sur les exercices proposés. Vous pouvez à la fois vous entraîner dans l'onglet « Main » sur les manipulations du graphe git, et dans l'onglet « Remote » au travail collaboratif ! Bonne chance !

Exercice 4 (Facultatif) : encore plus de parallélisme !

Cet exercice est un exercice volontairement plus ouvert et qui demande une meilleure maîtrise de Python : son objectif est de vous apprendre à travailler collaborativement dans des conditions un peu réalistes.

Dans le dossier tp-git-exo4, vous trouverez un fichier messaging.py dont il vous faudra terminer le développement. Pour cela, créez un nouveau dépôt git distant sur Gitlab, puis ajoutez les fichiers people.txt et messaging.py dans un premier commit. Une fois tout les deux membres du groupe synchronisé·e·s sur ce premier commit, vous aurez une liste des tâches que vous devrez vous répartir.

Chacune de ces tâches devra être implémentée dans sa propre branche. Vous pourrez effectuer autant de commits que souhaité. Une fois une tâche implémentée, vous pourrez fusionner la branche associée dans votre branche principale main. À la fin de chaque tâche, vous synchroniserez votre dépôt local avec le dépôt distant en utilisant git pull et git push.

Voici la liste des tâches à implémenter :