Travailler à plusieurs
Ce que ce chapitre apporte
- Distinguer dépôt local et dépôt distant, et expliquer ce que fait chaque commande de synchronisation.
- Comprendre pourquoi une poussée est refusée et comment se remettre à jour.
- Choisir entre fusion et rebasage, et connaître la règle qui interdit le second.
- Organiser le travail par branche courte et demande de fusion.
- Relire le code d'un collègue avec des critères, pas au ressenti.
- Résoudre un conflit en sachant ce que Git a calculé avant de vous le passer.
Le dépôt distant
Un dépôt distant est une copie du dépôt hébergée ailleurs, que plusieurs personnes utilisent comme point de rendez-vous. origin est le nom par défaut de celui d'où l'on a cloné.
Il n'a rien de spécial : c'est un dépôt Git ordinaire. Le serveur ne fait pas d'arbitrage, ne fusionne rien de lui-même, et ne détient pas plus la vérité que votre copie. Ce sont les conventions d'équipe qui lui donnent son statut, pas l'outil.
Sur un poste, on rapatrie le projet une seule fois :
Ensuite, la synchronisation est quotidienne, et c'est vous qui la déclenchez. Trois commandes suffisent, et il vaut la peine de bien les distinguer : la première regarde, la deuxième intègre, la troisième publie.
git fetch ne touche pas à votre travail : il met seulement à jour ce que Git sait du serveur. On peut alors regarder ce qui est arrivé avec git log HEAD..origin/main avant de décider comment l'intégrer.git pull enchaîne les deux, ce qui est pratique et ce qui, un jour de divergence, déclenche une fusion à un moment que vous n'avez pas choisi.
Pourquoi la poussée est refusée
Le message est déroutant la première fois. Il ne dit pas que vous avez fait une erreur, il dit que le serveur a avancé depuis votre dernière synchronisation.
$ git push
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://exemple.fr/projet.git'
Accepter votre poussée reviendrait à faire disparaître les commits arrivés entre-temps. Git refuse, et vous laisse décider comment réunir les deux histoires. Deux réponses possibles, qui ne produisent pas le même historique.
Fusionner
git pull (ou git merge origin/main) crée un commit de fusion à deux parents. L'histoire garde la trace du travail parallèle.
Rebaser
git pull --rebase (ou git rebase origin/main) rejoue vos commits par-dessus ceux du serveur. L'histoire devient linéaire, comme si vous aviez travaillé après les autres.
Le commit rejoué n'est pas le même objet : son parent a changé, donc son empreinte aussi. C'est de là que vient la seule règle vraiment importante.
La règle tient en une phrase : rebaser ce qui n'est qu'à soi, fusionner ce qui est partagé.
Aucune des deux options n'est meilleure en général. La fusion conserve l'histoire réelle, au prix de commits de fusion nombreux sur un dépôt actif. Le rebasage donne un historique lisible et linéaire, au prix d'une histoire un peu reconstruite. Les équipes tranchent une fois, et s'y tiennent.
La branche de fonctionnalité et la demande de fusion
Le schéma courant tient en cinq temps : partir de la branche principale, travailler sur une branche nommée, publier cette branche, demander l'intégration, fusionner après relecture.
Une demande de fusion (pull request sur GitHub, merge request sur GitLab) est une proposition d'intégrer une branche dans une autre. Elle ouvre un espace de discussion sur un ensemble de commits, et sert de point d'accroche aux vérifications automatiques.
Ce n'est pas une commande Git : aucun git pull-request n'existe. C'est une page web de l'hébergeur, qui compare votre branche à la branche cible et affiche les différences, les commentaires et le résultat des tests. Git s'arrête à git push ; tout le reste appartient à la plateforme.
La parade est de découper le travail en apports intégrables séparément, et de rapatrier régulièrement la branche principale dans la branche de travail plutôt que d'attendre la fin.
Deux grandes familles d'organisation en découlent. Le travail sur tronc commun garde des branches de quelques heures, intégrées plusieurs fois par jour, et suppose des tests automatiques solides. Les modèles à branches longues, dont le plus connu est Gitflow, séparent développement, versions et correctifs urgents ; ils conviennent à un logiciel livré par versions, et pèsent lourd sur un service déployé en continu. Le critère de choix n'est pas la mode mais la fréquence de livraison.
Relire le code d'un collègue
Une relecture n'est utile que si elle sait ce qu'elle cherche. Dans l'ordre.
Est-ce que cela répond au besoin ? Avant le style, avant les détails. Un code impeccable qui traite le mauvais problème n'apporte rien.
Est-ce correct aux limites ? Liste vide, valeur nulle, division par zéro, fichier absent, deux appels simultanés. C'est là que se trouvent les défauts, pas dans le cas nominal.
Est-ce lisible dans six mois ? Noms, découpage, commentaire là où le code ne peut pas expliquer le pourquoi.
Est-ce testé ? Le test doit échouer si l'on casse le comportement décrit.
Ce qu'une relecture ne fait pas : discuter de mise en forme, si un formateur automatique est en place ; réécrire à la place de l'auteur ; approuver sans lire parce que la personne est compétente.
d revient trente lignes plus bas, où l'on ne sait plus s'il s'agit d'une date ou d'une durée » ne se discute pas : la raison est vérifiable.Et une demande de fusion de deux mille lignes ne se relit pas. Elle se découpe.
Le conflit
Git fusionne seul tant que les deux branches ont modifié des endroits différents. Il s'arrête quand les deux ont modifié la même ligne, parce qu'aucune règle automatique ne peut décider laquelle des deux intentions l'emporte.
<<<<<<< HEAD
taux = 0.20
=======
taux = 0.196
>>>>>>> correctif-tva
Entre les deux premiers marqueurs, votre version. Entre les deux suivants, celle qui arrive.
Résoudre, en trois gestes
Il n'y a pas de commande magique : on ouvre le fichier et on écrit ce qui doit y être. La version correcte n'est d'ailleurs pas forcément l'une des deux.
Avant, le fichier tel que Git vous le laisse. Remarquez que le reste du fichier, lui, est intact : seule la zone en désaccord est encadrée.
<<<<<<< HEAD
taux = 0.20
=======
taux = 0.196
>>>>>>> correctif-tva
seuil = 100
devise = "EUR"
Après, le fichier tel qu'il doit être avant de continuer. On a tranché en faveur de 20 %, et surtout on a supprimé les trois lignes de marqueurs :
taux = 0.20
seuil = 100
devise = "EUR"
Puis on signale à Git que ce fichier est réglé, et l'on achève la fusion :
Les laisser et commiter produit un fichier qui ne compile plus, ou un script qui échoue à sa première ligne, et le message d'erreur ne parle pas de Git du tout : l'étudiant cherche pendant une heure une faute de syntaxe qu'il n'a pas écrite. Avant de valider, relisez le fichier, ou cherchez les chevrons dans tout le projet.
Deuxième erreur du même moment : commiter sans avoir fait
git add sur le fichier résolu. Git considère alors la fusion comme inachevée et refuse d'avancer.
git merge --abort annule la fusion entière et remet le dépôt dans l'état d'avant. Rien n'est perdu, et c'est la bonne réaction quand on découvre trente fichiers en conflit : mieux vaut repartir d'une branche à jour que résoudre trente fois.
Ce que Git a calculé avant de vous passer la main
L'ancêtre commun de deux branches est le dernier commit qu'elles ont en partage, avant de diverger. C'est le point de départ que Git compare aux deux versions actuelles.
La règle exacte se voit mieux en la programmant. Une fusion à trois points compare chaque côté à cet ancêtre, et non l'un à l'autre : c'est ce qui lui permet de savoir qui a changé quoi, au lieu de constater seulement que les deux fichiers diffèrent.
Trois lignes sur quatre se règlent sans rien demander, dont les deux qu'un seul des deux côtés avait modifiées. Seule la dernière, changée différemment des deux côtés, remonte à l'humain. C'est aussi pourquoi un ancêtre commun proche, donc une branche courte, produit mécaniquement moins de conflits.
Exercices type
Quelle différence entre git fetch et git pull ?
git fetch récupère les commits du dépôt distant et met à jour les références locales du type origin/main. Votre branche de travail et vos fichiers ne bougent pas.
git pull fait ce fetch, puis intègre immédiatement dans votre branche courante, par fusion ou par rebasage selon la configuration.
L'intérêt de séparer les deux est de regarder ce qui est arrivé avant de décider. En cas de divergence, pull déclenche une opération que vous n'avez pas nécessairement anticipée.
Le serveur refuse votre poussée. Que s'est-il passé, et que faire ?
Le dépôt distant contient des commits que vous n'avez pas. Accepter la poussée telle quelle les ferait disparaître, Git refuse donc.
La marche à suivre : git fetch, regarder ce qui est arrivé, puis intégrer. Fusion si la branche est partagée, rebasage si les commits sont encore à vous seul. Ensuite la poussée passe.
Ce qu'il ne faut pas faire est git push --force, qui écrase le travail des autres. La variante --force-with-lease est moins brutale, mais reste réservée à une branche personnelle.
Pourquoi ne pas rebaser une branche déjà publiée ?
Parce que le rebasage recrée les commits : leur parent change, donc leur empreinte change.
Quiconque avait récupéré les anciens commits garde une histoire qui n'existe plus sur le serveur. À la synchronisation suivante, il obtient les deux versions, en double, et doit démêler à la main.
Sur une branche que personne d'autre n'a récupérée, la question ne se pose pas : rebaser y est même recommandé pour ranger ses commits avant relecture.
Deux personnes ont modifié le même fichier. Y aura-t-il forcément un conflit ?
Non. Git fusionne sans rien demander tant que les modifications portent sur des lignes différentes.
Le conflit apparaît quand les deux versions changent la même ligne de façon différente, ou quand l'une supprime ce que l'autre modifie.
C'est pourquoi la taille du fichier importe moins que la proximité des modifications, et pourquoi des branches courtes, donc un ancêtre commun récent, réduisent nettement le nombre de conflits.
Vous ouvrez un fichier en conflit. Décrivez ce que vous faites, dans l'ordre.
Le fichier contient trois lignes de marqueurs que Git a insérées :
<<<<<<< HEAD
seuil = 100
=======
seuil = 150
>>>>>>> nouvelle-limite
1. Décider. On garde 100, ou 150, ou autre chose : la bonne valeur n'est pas forcément l'une des deux, et c'est une question métier, pas une question d'outil. Si le doute persiste, on demande à l'auteur de l'autre version plutôt que de trancher au hasard.
2. Nettoyer. On écrit la ligne retenue et l'on supprime les trois lignes de marqueurs. Un fichier qui contient encore des chevrons est un fichier cassé, même s'il « ressemble » à du code correct.
3. Achever. git add sur le fichier, ce qui dit à Git « celui-ci est réglé », puis git commit pour terminer la fusion. Le message est déjà pré-rempli.
Deux réflexes utiles : git status rappelle à tout moment quels fichiers restent en conflit, et git merge --abort annule tout si l'on préfère repartir d'une branche à jour.
Que regarde-t-on en priorité dans une relecture de code ?
L'adéquation au besoin. Un code élégant qui ne résout pas le problème posé n'a aucune valeur.
Puis le comportement aux limites : entrée vide, valeur absente, erreur d'entrée-sortie, accès concurrents. Le cas nominal est presque toujours correct, les défauts sont ailleurs.
Puis la lisibilité, et l'existence de tests qui échouent si le comportement décrit disparaît.
La mise en forme ne devrait pas apparaître : c'est le travail d'un formateur automatique, pas d'un humain.
Pourquoi une branche de trois semaines coûte-t-elle plus cher que trois branches d'une semaine ?
Parce que l'écart avec la branche principale grandit tout du long, et que la difficulté de la fusion croît avec cet écart.
Parce que les conflits arrivent tous à la fin, sur du code que l'auteur a écrit trois semaines plus tôt et dont il ne se souvient plus.
Parce que la relecture d'un apport massif est de mauvaise qualité : passé quelques centaines de lignes, un relecteur ne voit plus les défauts.
Et parce que pendant trois semaines, personne ne bénéficie de ce qui est déjà terminé.
La méthode
- Pars d'une base à jour :
switch main,pull, puis crée ta branche. - Une branche par sujet, et un sujet qui tient en quelques jours.
fetchet regarde avant d'intégrer, plutôt quepullpar réflexe.- Rebase ce qui est à toi, fusionne ce qui est partagé.
- Rapatrie la principale dans ta branche régulièrement, sans attendre la fin.
- Relis dans l'ordre : besoin, cas limites, lisibilité, tests.
- N'utilise jamais
--forcesur une branche que d'autres suivent.
En résumé
- Un dépôt distant est un dépôt ordinaire ; son statut vient de la convention d'équipe.
fetchrécupère sans rien modifier,pullrécupère et intègre.- Une poussée refusée signifie que le serveur a avancé, pas que vous avez fauté.
- Fusionner conserve l'histoire parallèle, rebaser la linéarise en recréant les commits.
- On ne rebase que ce qui n'a pas été publié.
- Une demande de fusion ouvre la relecture et sert d'accroche aux vérifications automatiques.
- Une fusion compare les deux côtés à leur ancêtre commun, pas l'un à l'autre.
- Un conflit ne survient que si les deux côtés changent la même ligne différemment.
- Le coût d'une branche croît avec sa durée de vie.
Et ensuite ? Une équipe qui sait s'échanger du code doit encore pouvoir l'exécuter à l'identique. C'est l'objet du chapitre suivant : la structure d'un projet et ses dépendances.