Chapitre 3 sur 13
Git collaboratif : les dépôts distants
Git fonctionne en local, c’est à dire que tout dépôt doit être sur la machine de l’utilisateur afin de pouvoir travailler. Cependant, la gestion de la synchronisation de dépôts distants est intégrée au cœur même du VCS.
Les dépôts distants, communéments appelées remotes, permettent de collaborer à plusieurs sur un même projet. De plus, comme une copie du dépôt est synchronisée sur une autre machine, cela permet de s’assurer d’avoir une sauvegarde du projet. En cas d’incident, il sera toujours possible de récupérer le dépôt depuis la copie distante. Découvrons comment utiliser Git avec les dépôts distants.
Les dépôts – ou repo, de l’anglais repository – distants permettent la collaboration sur des projets. Chaque projet Git peut être synchronisé avec un ou plusieurs dépôts distants. Sur chacun d’eux, vous pouvez avoir des droits en lectures, en écriture ou les deux. La collaboration consiste donc à récupérer le code depuis un dépôt distant, à effectuer des modifications puis à pousser – ou push – les commits de modifications sur le repo distant ; de manière à ce que tout le monde puisse y accéder.
Le bare repository
Lorsque l’on parle de dépôt distant, de l’autre côté du tuyau se trouve toujours un bare repository, ou dépôt nu. Ce type de dépôt a pour unique fonction d’être un réceptacle, le point central de synchronisation entre différents dépôts de travail.
Il s’agit d’un dépôt qui ne contient pas de répertoire de travail. Contrairement à un dépôt “normal”, il ne contient pas de répertoire .git
mais place tout le contenu de ce répertoire directement à la racine. Par ailleurs, par convention, ce type de répertoire possède l’extention .git
: projet-dingue.git
.
Ce type de dépôt ne possède pas de répertoire de travail car personne ne travaille directement dedans. Un dépôt bare a simplement vocation à être la source de git clone
et la cible de git push
.
Un repos de ce type se créer aussi facilement qu’un repo classique :
git init --bare
Il est également possible d’en créer un directement depuis un dépôt existant.
git clone --bare <repo-address>
Les protocoles de Git
On a parlé dans le chapitre précédent de deux protocoles : SSH et HTTPS. Ce sont en effet les deux plus courants mais pas les seuls.
HTTPS
Le HTTPS est le protocole de synchronisation le plus courant. Il offre en effet la gestion la plus souple de l’authentification. Elle est possible, mais non obligatoire. Ainsi, il est possible pour quiconque possède l’url de cloner le dépôt sans avoir à s’authentifier, tout en requérant une authentification par login et mot de passe pour ajouter des modifications.
C’est la méthode par défaut de toutes les platformes collaboratives. Lorsque l’on collabore activement à un projet, il devient vite fastidieux d’avoir à entrer un couple login/mot de passe pour chaque push. Comme nous l’avons vu lors de la configuration de Git, il devient alors pertinent de mettre en place une gestion automatique de l’authentification.
Git supporte aussi le HTTP, son fonctionnement est le même que le HTTPS, la sécurité en moins. Il est également bon de savoir que dans les versions plus anciennes de Git (antiérieures à 1.6.6), le protocole HTTP – aujourd’hui dénommé Dumb HTTP, par opposition à la gestion actuelle nommée Smart – ne permettait pas l’authentification. Le dépôt du projet était simplement servi tel quel par un serveur web et récupéré par Git à travers ce protocole.
SSH
Le SSH a comme atout une gestion intégrée de l’authentification, une sécuritée via un chiffrement natif des communications sans recours à des certificats SSL et une présence quasi systématique sur les serveurs.
Lorsqu’un dépôt distant est configuré sur un serveur, ce dernier a 99% de chance d’avoir déjà SSH de configuré. Le principal inconvénient du SSH est qu’il nécessite la configuration d’une clef SSH pout tous les utilisateurs qui voudraient accéder au dépôt, même en lecture.
Cela ne présente aucun problème pour les dépôts privés, mais pour les dépôts publics et les projets open source, c’est plus embêtant. En SSH, vous n’auriez ainsi pas été en mesure de cloner le projet Git emojis hook sans d’abord enregistrer votre clef SSH sur GitHub. D’ailleurs, GitHub ne vous propose l’option que si vous êtes connectés et qu’une clef SSH est associée à votre compte.
La clef possède en revanche l’énorme avantage d’unifier l’authentification entre les différents systèmes. Il n’y a pas de mot de passe à taper et ce n’est pas à Git de se souvenir d’un mot de passe. Qu’il s’agisse de Windows, Linux ou macOS, on utilise la même stratégie et on peut même partager une clef entre plusieurs systèmes pour s’authentifier de manière transparente. C’est ce que je fais entre mes différentes machines.
Les clefs SSH sont supportées par tous les systèmes. Pour les créer sous macOS, Linux et Unix, vous pouvez vous référer à mon article sur SSH. Pour Windows, cet article vous explique la démarche pas à pas.
Le protocole local
Le protocle local est le plus basique de tous. Il permet de cloner un dépôt accessible localement, c’est à dire sur un système de fichiers local. Il est la plupart du temps utilisé lorsque tous les collaborateurs ont accès à un système de fichiers partagé, tel que NFS.
# On pécise simplement la chemin d'accès
git clone /chemin/vers/repo
Dans cette configuration, Git active automatiquement l’option --local
. Ainsi, Git gagne en rapidité en réalisant simplement une copie des dossiers refs
et objets
du répertoire .git
. En outre, pour économiser de la place, les éléments contenus dans objects
sont des liens physiques et partagent donc le même contenu.
Cela se vérifie facilement en affichant les inodes des fichiers.
# Dépôt source git-emojis-clone
ls -i git-emojis-clone/.git/objects
3131811 info 3131810 pack
# On créer un autre dossier et on s'y place
mkdir test && cd test
# On clone le répertoire d'origine
git clone ../git-emojis-clone
# On affiche les inodes
ls -i git-emojis-clone/.git/objects
3131811 info 3131810 pack
On voit que les inodes sont les mêmes. Cela indique bien qu’ils sont partagés. Si l’on veut simuler une copie comme à travers le réseau, on peut passer l’argument --no-local
. Cela permettra de forcer un import propre, notamment si le dépôt d’origine est un import depuis un autre VCS par exemple.
Le protocle Git
Le protocole Git n’utilise ni serveur web, ni le SSH. Il fonctionne via un dæmon Git qui écoute sur un port dédié (le port 9418 par défaut) et fournit un service assez similaire au SSH mais sans aucune authentification.
Dans la mesure où il n’y a aucune sécurité, lorque ce protocole est utilisé, c’est simpelment pour le clonage et l’ajout de modifications est désactivé. Il est toutefois possible de l’activer sur un réseau local par exemple.
Étant donné qu’il s’agit d’un dæmon spécifique écoutant sur un port dédié, sa mise en place n’est pas standard et de ce fait très peu répandue.
Lister les remotes
Lorsque l’on créé un projet localement, il n’y a par défaut pas de dépôt distant, cela semble logique. En revanche, si le dépôt est clôné depuis une source, alors la source est directement référencée comme dépôt distant.
# Test sur le dépôt localement créé dans le chapitre précédent
git remote
La commande ne retourne rien, c’est qu’il n’y a aucune remote… Testons donc maintenant sur le repo Git emojis hook clôné depuis GitHub.
git remote
origin
# L'option -v nous permet d'obtenir un peu plus de précisions
git remote -v
origin git@github.com:Buzut/git-emojis-hook.git (fetch)
origin git@github.com:Buzut/git-emojis-hook.git (push)
On voit cette fois que la remote nommée origin a pour origine le dépôt à l’adresse git@github.com:Buzut/git-emojis-hook.git
, pour les push et les pull. Votre dépôt distant est automatiquement nommé origin. C’est un choix par défaut de Git au même titre que le nom de master pour la branche par défaut.
Lorsque les adresses sont du type user@url
c’est qu’il s’agit du protocole SSH. Si vous utilisez HTTPS, l’adresse commencera par HTTPS.
En savoir plus sur une remote
Lorsque vous voulez plus d’information sur une remote en particulier, vous pouvez le demander à Git.
# On prend cette fois pour exemple le repo que j'utilise pour le code de ce site
git remote show origin
* remote origin
Fetch URL: git@git.buzut.net:Buzut/buzut-blog.git
Push URL: git@git.buzut.net:Buzut/buzut-blog.git
HEAD branch: master
Remote branches:
formations tracked
master tracked
Local branches configured for 'git pull':
formations merges with remote formations
master merges with remote master
Local refs configured for 'git push':
formations pushes to formations (up to date)
master pushes to master (fast-forwardable)
Nous avons de nouveau l’adresse du dépôt, mais en plus des informations détaillées sur les branches locales et distantes. On sait donc que j’ai deux branches locales configurées pour traquer les branches distantes du même nom et dans notre cas, la réciproque est vraie pour le push.
On apprend également que la banche locale “formations est à jour avec la version distante. En revanche, la master locale possède des commits qui ne sont pas encore sur la master distante. Git nous spécifie cependant que ces changements sont fast-forwardable ; il n’y aura pas de merge à prévoir en amont.
Cette commande nous montrerait également si nous avions des branches distantes qui n’ont pas été récupérées localement ou encore des branches distantes effacées mais toujours présentes localement.
Ajouter un dépôt
Chacun des dépôts distants possède un nom. Cela permet de facilement préciser à Git si vous voulez effectuer une action sur une remote plutôt qu’une autre.
Que votre dépôt possède un dépôt distant ou qu’il n’en possède encore aucun, vous pouvez toujours en ajouter de nouveaux. Par exemple, bien que mon repo Git emojis hook soit publiquement accessible en lecture, vous ne pourrez pas y pusher de modifications. Si vous voulez y effectuer des modification et les partager, il faudra que vous créiez votre propre dépôt distant.
# Ajout de ce dépôt distant au dépôt local
# La commande générique est : git remote add <name> <url>
git remote add testrepo git@github.com:Buzut/test.git
# On confirme l'ajout en affichant les remotes
git remote -v
origin git@github.com:Buzut/git-emojis-hook.git (fetch)
origin git@github.com:Buzut/git-emojis-hook.git (push)
testrepo git@github.com:Buzut/test.git (fetch)
testrepo git@github.com:Buzut/test.git (push)
Notre repo est bien là. Cependant, si nous demandons plus d’infos à Git, on réalise qu’il ne sait pas ce que doit traquer testrepo.
git remote show testrepo
* remote testrepo
Fetch URL: git@github.com:Buzut/test.git
Push URL: git@github.com:Buzut/test.git
HEAD branch: (unknown)
Lors du premier push, il faudra donc indiquer à Git quel est l’upstream par défaut, c’est à dire la branche distante principale. On fait pour cela un git push
en passant l’option -u
. Comme son nom l’indique dans sa version longue, --set-upstream
, permet de définir le dépôt upstream.
# La commande générique : git push -u <upstream> <branch>
git push -u testrepo master
# On vérifie de nouveau l'état de notre remote
git remote show testrepo
* remote testrepo
Fetch URL: git@github.com:Buzut/test.git
Push URL: git@github.com:Buzut/test.git
HEAD branch: master
Remote branch:
master tracked
Local ref configured for 'git push':
master pushes to master (up to date)
Récupérer et envoyer les données
Lorsque vous travaillez avec des dépôts distants, il faut régulièrement les synchroniser : récupérer les modifications effectuées par d’autres et pousser les vôtres.
push
: envoyer vos modifications
Lorsque vous clonez un dépôt, ce dernier est automatiquement ajouté comme dépôt distant sous le nom d’origin et vos branches traquent automatiquement les branches de ce dépôt.
Par défaut, lors du clonage, vous êtes sur la master, si vous y effectuez des modifications et faites un git push
, celles-ci seront automatiquement envoyées sur la branche master du repo par défaut (souvent origin). git push
fait donc en réalité implicitement git push origin master
.
git push
Total 0 (delta 0), reused 0 (delta 0)
To git@git.buzut.net:Buzut/buzut-blog.git
018dc17..58e4a6b master -> master
Comprendre le résultat du push
On s’aperçoit que Git envoit ici la master locale vers la master distante, sur le repo configuré comme le dépôt distant par défaut. La première ligne, sans ambiguité, indique sur quel dépôt distant est poussée la branche, la seconde ligne comporte quatre informations :
- un flag (espace,
+
,-
,*
,!
,=
), - le résumé sous la forme
<old-commit>..<new-commit>
, - les branches sous la forme
<localbranch> -> <remotebranch>
(dans le cas d’un effacement, seule la branche effacée est listée), - en cas d’échec, la raison de ce dernier.
Le flag prend une des quatre valeurs suivantes :
- espace : si l’update est un simple fast-forward ;
+
: s’il s’agit d’un update forcé ;-
: pour une référence effacée ;!
: lorsqu’il s’agit d’un échec ;=
: lorsqu’il n’y a eu aucun changements.
Si vous créez une nouvelle branche et que vous faites de nouveau un push, cette nouvelle branche sera aussi envoyée sur origin car c’est la remote par défaut. Vous pouvez cependant en décider autrement en spécifiant à Git le comportement à tenir.
# Commande push plus détaillée
git push [<upstream>] [<branch>]
Vous voyez ici qu’il n’est donc pas nécessaire d’être sur la branche que vous voulez pusher. Si vous êtes sur une autre branche, vous devez alors spécifier à Git qu’il faut pusher une branche tierce plutôt que l’actuelle.
Par la suite, Git se souvient de la remote associée à une branche. Sauf mention contraire, un push ultérieur associera donc automatiquement la branche distante sur laquelle envoyer les données.
Par ailleurs, si vous tentez de pousser vos modifications mais que la branche distante a été modifiée entre temps, Git refusera d’intégrer vos modifications tant que vous ne les intégrez par d’abord à votre historique.
fetch
: récupérer l’état du dépôt distant
Cette commande permet de récupérer l’ensemble des branches disponibles sur le dépôt distant.
git fetch testrepo
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:Buzut/test
1f80958..a0e6f51 master -> testrepo/master
Les modifications ne sont en revanche pas intégrées aux branches locales. Elles sont placées dans des branches spécifiques reflétant l’état du dépôt distant. C’est ce qui nous est ici indiqué sur la dernière ligne du git fetch
. On le constate par ailleurs en affichant toutes les branches locales.
# L'option -a pour "all"
git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/testrepo/master
# Un git log nous confirme que rien n'a bougé
git log --oneline
1f80958 (HEAD -> master, origin/master, origin/HEAD) 💄 update licence year
Directement depuis l’interface en ligne de GitHub, j’ai ajouté un fichier. La branche distante possède donc un commit d’avance sur la branche locale. Nous allons donc nous placer sur la branche remotes/testrepo/master
et observer ce qu’il s’y passe.
# Changeons maintenant de branche
git switch remotes/testrepo/master
Note: switching to 'remotes/testrepo/master'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at a0e6f51 Create DOC.md
Git nous indique ici que nous ne pouvons pas directement effectuer de modifications dans cette branche – elle reflète l’état de la remote, ça n’aurait donc pas de sens. On peut regarder ce qui s’y passe et créer une nouvelle branche à partir de cette dernière si l’on veut sauvegarder des modifications que l’on aurait faites ou simplement créer une branche locale qui reflète l’état de la remote.
En outre, si nous voulons intégrer ces modifications directement dans la master (ou une autre branche locale), on peut tout à fait fusionner cette branche avec une branche locale déjà existante.
pull
: intégrer des modifications distantes
Lorsque vous souhaitez récupérer des modifications effectuées par d’autres sur la branche courante, vous pouvez effectuer un git pull
. Deux cas se présentent :
- les modifications sont des enfants directs du dernier commit de votre branche locale,
- la branche distante et votre branche locale ont chacune des modifications que l’autre n’a pas.
La premier cas est simple, les modifications distantes sont récupérées et les changements sont simplement ajoutées à la branche locale.
Dans le second cas, un merge doit avoir lieu. git pull
invoque merge
par défaut. En réalité, cette commande est un racourci permettant de faire en une seule fois git fetch
et git merge
.
Suivi des branches
Comme nous l’avons vu plus haut, lorsqu’un dépôt upstream est définit, Git présume que toute nouvelle branche suivra automatiqument la branche du même nom de l’upstream par défaut. Deux cas sont alors couramment rencontrés :
- Lorsqu’une branche locale du nom d’une branche distante est créée, la branche locale suit automatiquement la branche distante. Ainsi si le
fetch
a récupéréorigin/feature
, lors dugit switch feature
, la branche localefeature
reflètera l’état deorigin/feature
. Subséquemment, toutgit push
enverra les modifications sur la branche distanteorigin/feature
et toutgit pull
intègrera les ajouts deorigin/feature
àfeature
. - Lorsqu’une nouvelle branche est créée, tout
git push
sans plus de précisions créera alors une branche du nom de la branche locale sur la remote par défaut.
Il est courant d’avoir des branches suivant différentes remotes. Nous l’avons vu, lorsqu’il n’y a pas encore de remote par défaut, on indique à Git lors du push quelle branche distante traquer avec git push -u
. Lorsqu’une branche locale est créée et qu’elle n’a pas d’équivalent sur une remote, il est alors possible de lui spécifier quelle remote traquer lors du push en utilisant git push -u <upstream>
.
Par ailleurs, on peut déterminer la branche distante à suivre directement lors de la création de cette dernière via la commande branch
.
# branch permet de crééer la branche en précisant l'upstream
# Si la branche existe déjà, la configuration de l'upstream sera simplement ajoutée
git branch -u <upstream> <branch>
Ces paramètres sont enregistrés dans le configuration locale du projet, dans le fichier .git/config
. On peut facilement obtenir des informations via git config
.
# Connaître la remote d'une branche (la master dans notre exemple)
# Ici nous allons constater que la remote par défaut est gogs et non origin
git config branch.master.remote
gogs
# La configuration nous permet aussi d'éditer ces préférences
git config branch.master.remote origin
# On vérifie de nouveau
git config branch.master.remote
origin
Il est également possible de dissocier la cible du push et du pull. On peut donc vouloir récupérer les modifications depuis un dépôt donné et les envoyer sur un autre.
# Définir une cible pour le push autre que la "remote" par défaut
git config branch.<name>.pushRemote
# Définir une cible pour fetch/pull/rebase autre que la remote par défaut
git config branch.<name>.merge
Limiter les branches suivies
Lors de l’ajout d’une remote,
Il est possible de limiter les branches traquées (et donc récupérées par fetch). On utilise pour cela set-branches
.
De nombreuses autres options sont disponibles. Plus rarement utilisées, nous ne les listerons pas ici. Cependant, pour des besoins spécifiques, ne perdez pas les bons réflexes et référez-vous à la doc.
Limiter les données récupérées
Lorsque l’on travaille avec de gros projets, il est possible de vouloir n’en récupérer qu’une partie pour s’épargner un lent téléchargement et gagner un peu d’espace disque.
Lorsque l’on est pas encore en posession du repo, tout commance avec la commande clone
.
# Cloner une branche seulement
# Si la branche n'est pas précisée, la branche référencée par HEAD (probablement master) sera récupérée
git clone [--branch <branch-name>] --single-branch <repo_url>
# Cloner seulement depuis une date donnée
git clone --shallow-since=<date> <repo-url>
# On peut alternativement spécifier un nombre de commits
# Penser à précier --branch si la cible est autre que master
# Ou spécifier --no-single-branch pour récupérer toutes les branches
git clone --depth <number> <repo-url>
fetch
permet ensuite de travailler avec ce type de repo partiel. On trouve bien entendu les paramètres --depth
et --shalow-since
qui permettent soit de récupérer partiellement une autre branche ou de modifier la taille de l’historique d’une branche déjà récupérée.
On trouve également --unshallow
qui permet de récupérer l’ensemble de l’historique.
# Par ailleurs, dans le cas où l'on voudrait ne récupérer qu'une branche supplémentaire du projet
# On le spécifie à fetch
git fetch <remote-name> <branch>
On peut également limiter le tracking des branches directement lors de l’emploi de git remote
.
# Tracking d'une seule branches distante
git remote add -t <name> <branch>
# Modifications des branches suivies (écrase les réglages précédents)
git remote set-branches <remote-name> <branch-name> [<branch-name2>]
# On peut également ajouter une tracking plutôt que de tout écraser
git remote set-branches --add <remote-name> <branch-name>
Supprimer et renomer une remote
Il arrive que l’on veuille renomer un dépôt distant, il arrive aussi qu’un dépôt distant ne soit plus d’aucune utilité, dans ce cas, on le supprime. Dans un cas comme dans l’autre, ce sont des actions très simples.
# On renome le dépôt testrepo en secondary
git remote rename testrepo secondary
# On vérifie si la commande a bien été exécutée
git remote
origin
secondary
# Pour supprimer le repo, c'est tout aussi simple
git remote rm secondary
# Vérification
git remote
origin
Pour supprimer une branche distante, cela semble parfois contre-intuitif car il faut pusher.
# Suppression de la branche "unebranche"
git push [<upstream>] -d unebranche
Les plateformes web
Que vous vouliez collaborer ou bénéficier d’une copie distante de votre travail pour une plus grande sécurité, de nombreuses options existent. Le plupart des plateformes incluent, en plus de Git, des outils de collaborations avancés : gestion des tickets, gestion des modifications, gestion avancée des droits, création de wiki, etc.
Les deux plateformes les plus connues sont GitHub et GitLab. GitHub est vraiment la plus grosse plateforme d’hébergement de code d’Internet. Elle héberge de très nombreux projets open source et propose un niveau d’utilisation gratuit.
Elle a été racheté par Microsoft, on peut donc s’attendre à un haut niveau d’intégration avec Azure, le cloud de Microsoft. GitHub possède de nombreuses fonctionalités, mais son plus gros atout est à mon avis sa communauté de développeur. C’est d’ailleurs là que j’héberge mes projets open source.
GitLab est le challenger de GitHub. Dans l’ensemble, GitLab offre plus de fonctionalités et prend une posture résoluement DevOps, qui en fait une plateforme très complète. De plus, GitLab a la particularité d’avoir une version communautaire open source. Il est donc possible d’installer GitLab sur ses propres serveurs et de garder le contrôle sur l’hébergement de ses données. La version SaaS possède toutefois plus de fonctionalités.
En dernier lieu, il faut mentionner Bitbucket, la plateforme d’Atlassian. Elle propose également un large éventail de fonctionalités et s’intègre particulièrement bien avec les autres services de l’entreprise, notamment Jira et Trello.
GitLab est à mon sens la plateforme la plus riche et flexible, cependant, si vous êtes hésitant, GitLab propose des comparatifs avec GitHub et Bitbucket :
Auto-hébergement
J’ai mentionné le fait que GitLab peut être librement installé afin d’en posséder sa propre instance privée. Le principal inconvénient de GitLab dans ce cas est son besoin élevé en ressources. Alternativement, Git possède lui-même un serveur web intégré permettant de parcourir ses repos depuis un navigateur web. Cependant, il est très limité et son intérêt en est donc réduit.
Gogs est une plateforme open source écrite en Go. Elle est facile à installer et consomme très peu de ressources, à tel point qu’elle peut se contenter d’un petit VPS ou d’un Rasberry Pi. Malgré sa faible consommation en ressources, elle offre un grand nombre de fonctionalités et constitue une alternative viable aux plateformes SaaS.
J’en ai moi même une instance que j’utilise pour certain des projets sur lesquels je travaille. Il faut également mentionner Gitea, un fork de Gogs développé par la communauté et possédant plus de fonctionalités.
Quel que soit votre besoin, vous avez maintenant les clefs pour choisir la bonne solution et la mettre en œuvre sans plus attendre. Dans le prochain chapitre, nous allons explorer les usages avancés de Git : les commandes que nous n’utilisons que rarement mais qui s’avèrent bien pratiques dans certaines situations.
Commentaires
Rejoignez la discussion !