Les pull-request : comment ça marche, comment en faire une, comment en intégrer une ?
GitHub a popularisé le principe de pull-request et tous les autres outils de gestion de dépôt Git s’y sont mis : Bitbucket Cloud, Bitbucket Server (anciennement Stash), GitLab (sous le terme de merge-request).
Dans le principe c’est simple : pour contribuer à un projet sur l’une de ces plateformes :
- Forker le projet
- Créer une branche et travailler dessus
- Publier la branche sur son fork
- Créer la pull-request
Mais dans les faits, ça peut être un peu plus compliqué…
Nous allons voir étape par étape comment cela fonctionne et comment s’en servir au mieux.
Nous allons nous concentrer sur la situation de workflow triangulaire, c’est à dire de travail avec 3 dépôts :
- Un dépôt de référence, conventionnellement appelé upstream
C’est le dépôt du projet auquel nous voulons contribuer.
Nous n’avons que les droits en lecture dessus. - Un dépôt de fork, conventionnellement référencé origin
C’est une copie du dépôt de référence.
Nous avons tous les droits dessus. - Un dépôt local
C’est notre dépôt de travail.
Depuis la version 2.5, Git simplifie le travail avec ce type de workflow avec l’introduction de la référence <branch>@{push}
.
Il existe déjà la référence <branch>@{upstream}
qui permet de déterminer la branche distante traquée par une branche. Celle-ci est automatiquement définie lorsque vous faites un git checkout -b branch upstream_branch
ou peut être explicitement définie par un git branch --set-upstream-to upstream_branch branch
. Cette référence permet de déterminer la branche avec laquelle fusionner/rebaser lors d’un git pull
. Elle permet aussi, si votre configuration push.default
est à upstream
, de déterminer la branche vers laquelle publier par défaut lors d’un git push
.
Avec l’apparition de la référence <branch>@{push}
, il est maintenant possible de mieux contrôler la branche vers laquelle publier par défaut. Celle-ci est configurée par les options de configuration :
remote.pushDefault
(oubranch.<name>.pushRemote
pour une branche spécifique) : pour indiquer le dépôt par défaut sur lequel publierpush.default
: une valeur àcurrent
va provoquer une publication par défaut sur une branche portant le même nom que la branche courante dans le dépôt de publication
Il est possible de vérifier la valeur de ces références via les commandes suivantes (ici des valeurs dans un cas de workflow triangulaire) :
$ git rev-parse --symbolic-full-name --abbrev-ref @{upstream} upstream/master $ git rev-parse --symbolic-full-name --abbrev-ref @{push} origin/current_branch
Dans notre exemple, nous allons apporter une contribution au logiciel ‘example’ publié sur http://git.example.com/org/example.
Ce dépôt contient 2 commits C1
et C2
et une branche par défaut master
positionnée sur C2
:
Côté contributeur
Faire une contribution
Tout d’abord, il faut forker le projet.
Cette étape est simple : il suffit d’aller sur l’interface web du projet auquel nous voulons contribuer, en l’occurrence http://git.example.com/org/example, et de cliquer sur le bouton ‘Fork’ (c’est a priori le même sur GitHub, GitLab ou Bitbucket).
Cela va avoir pour effet de créer un clone du dépôt mais côté serveur.
Ce dépôt va alors être accessible à une URL du genre http://git.example.com/contributor/example.
Il nous appartient et nous avons tous les droits dessus.
Ensuite nous clonons localement notre dépôt :
git clone http://git.example.com/contributor/example
Comme indiqué en introduction, nous allons configurer un peu pour publier par défaut sur notre fork origin :
git config remote.pushdefault origin # Publier par défaut sur le dépôt 'origin' git config push.default current # Publier par défaut sur une branche portant le même nom que la branche courante dans le dépôt de publication
Enfin, nous allons ajouter en remote le dépôt de référence avec le nom conventionnel upstream :
git remote add upstream http://git.example.com/org/example git fetch upstream
Voilà, nous pouvons enfin vraiment travailler ! Enfin presque…
Une bonne pratique est de toujours travailler sur une branche spécifique, jamais sur la branche cible de notre développement.
git checkout -b contribution upstream/master
Ainsi nous allons travailler sur la branche contribution
qui va automatiquement traquer la branche upstream/master
.
Nous pouvons maintenant faire les modifications de code et commiter :
# hack, hack, hack git add X Y Z git commit # hack again git add A B C git commit
Publier et proposer une contribution
Avant publication, nous pouvons vérifier les commits que nous allons publier et où :
$ git rev-parse --symbolic-full-name --abbrev-ref @{push} origin/contribution $ git log --graph --oneline --decorate --date-order --full-history @{push}..HEAD * C4 (HEAD -> contribution) Commit C4 * C3 Commit C3
Pour publier ces modifications, il nous suffit alors simplement de faire un :
git push
Ainsi, git va publier sur la branche configurée en publication ou @{push}
, or, puisque nous avons défini les options remote.pushdefault=origin
et push.default=current
, c’est origin/contribution
.
Enfin, il faut passer par l’interface web du projet pour créer la pull-request depuis votre branche contribution
de votre fork vers la branche master
du dépôt de référence.
Corriger une pull-request
Si vous avez des retours sur votre pull-request et des corrections à faire, il suffit de réitérer l’étape précédente.
Simplement corriger et commiter :
# fix, fix, fix git add --update git commit
$ git log --graph --oneline --decorate --date-order --full-history @{push}.. * C5 (HEAD -> contribution) Commit C5 $ git push
La pull-request sera automatiquement mise à jour avec le(s) nouveau(x) commit(s).
Mettre à jour une pull-request
Il peut arriver que le dépôt de référence évolue pendant le temps de validation de votre pull-request, il vous faut alors la mettre à jour.
Se pose ici la question du merge vs. rebase mais je vais laisser d’autres répondre à ce débat, et opter pour ma recommandation dans ce cas : le rebase.
Vérifions ce que l’on va récupérer :
$ git log --graph --oneline --date-order --full-history ..@{upstream} * C7 Commit C7 * C6 Commit C6
Et intégrons-le :
git pull --rebase
Cela va faire un rebase
sur la dernière version de la branche traquée par votre branche locale ou @{upstream}
, qui est upstream/master
comme expliqué plus haut.
Et maintenant republions tout simplement.
git push
Mais ça coince… Car la publication n’est pas fast-forward.
En effet, origin/contribution
n’est pas un ancêtre de contribution
.
Mais ce n’est pas grave, nous voulons justement écraser avec notre nouvelle version, donc nous allons forcer un peu, mais en s’assurant quand même que nous n’allons pas écraser quelque chose que nous ne connaîtrions pas (ce qui est peu probable dans le cas de notre fork personnel) :
git push --force-with-lease
La pull-request sera encore une fois automatiquement mise à jour.
Côté mainteneur
Voyons maintenant comment gérer les pull-requests qui nous sont soumises.
Vérifier une pull-request
Si la pull-request n’est pas en conflit et que nous pouvons nous contenter de lire la pull-request en ligne et que nous avons des tests automatisés, tant mieux.
Par contre, si vous avez besoin de la retravailler en local, les différents outils proposent des références spéciales pour récupérer les pull-requests :
# GitHub git fetch upstream refs/pull/{PR_NUMBER}/from:pull/{PR_NUMBER} # GitLab git fetch upstream refs/merge-requests/{PR_NUMBER}/head:pull/{PR_NUMBER} # BitBucket Server git fetch upstream refs/pull-requests/{PR_NUMBER}/from:pull/{PR_NUMBER} # Bitbucket Cloud (obligé ici de taper directement sur le fork du contributeur) git fetch http://git.example.com/{PR_CONTRIBUTOR}/example {PR_SOURCE_BRANCH}:pull/{PR_NUMBER}
Nous nous retrouvons alors avec une branche locale pull/{PR_NUMBER}
sur laquelle nous pouvons travailler classiquement.
Intégrer une pull-request
Si tout se passe bien et que la pull-request peut être intégrée via l’interface web, parfait : simplement cliquer sur ‘fusionner’.
Mais parfois, si la branche cible a évolué de manière conflictuelle avec la pull-request (et que le contributeur ne peut la mettre à jour comme montré précédemment) ou que vous préférez simplement faire les choses vous-même, voici comment procéder.
Tout d’abord, récupérer la branche de la pull-request comme présenté dans le point précédent, ensuite, selon vos préférences, fusionner avec ou sans fast-forward, avec ou sans squashing :
git merge --no-ff pull/{PR_NUMBER}
Il ne reste plus qu’à publier :
git push upstream
La pull-request sera alors automatiquement fermée.
Conclusion
J’espère que vous comprenez un peu mieux comment fonctionne une pull-request et comment la manipuler de manière plus fine.
Bonjour,
Je voudrais vous soumettre un cas :
j’ai dans mon upstream un dépôt provenant du split d’un dépôt monolithique. Celui-ci est donc en lecture seule mais je peux lui soumettre une contribution. Comment transférer la PR du dépôt splité au dépôt monolithique ?
N’hésitez pas à me contacter si vous voulez echanger
Bonjour,
Si j’ai un pending pull request et je dois l’intégrer dans mon application mais elle est toujours Pending (en attente de validation de l’organisation), comment je peux la builder et l’utiliser dans mon livrable ?
Merci.
Idéalement, on ne devrait pas livrer du code non approuvé ailleurs qu’en environnement de CI (mais ce n’est alors que temporaire et le faire depuis un merge local entre la branche de la pull-request et master est suffisant).
Si vous avez le droit de livrer du code non validé, alors c’est que la validation est inutile finalement, non ?
Donc la réponse est simplement : merger manuellement la pull-request (celle-ci sera alors automatiquement fermée par le système de gestion de dépôt) et retomber sur une validation a posteriori.
Les autres solutions que je vois :
– intégrer dans la branche master de votre fork et builder depuis celui-ci
– intégrer dans une branche annexe à master et builder depuis celle-ci
Mais dans les 2 cas, cela va poser plusieurs problèmes :
– maintenir la branche master de votre fork ou la branche annexe par rapport à la branche master de référence
– lorsque la PR sera intégrée dans master, vous repartirez alors de master pour vos build et livraisons donc plus “vraiment” le même source
@Zenkia:
Vous avez l’art de compliquer les choses pour rien…, nous faisons régulièrement des merges requests (ou pull requests) et ce n’est pas plus compliqué que :
1. créer une branche (en général depuis le master)
2. faire ses modifications sur sa branche, pusher
3. faire une demande de merge request sur l’interface (ex: GitLab)
Je ne comprends pas trop toutes vos commandes complexes…. juste pour une pull request…
Faisons simple et arrêtons de compliquer les choses qui le sont déjà par ailleurs, je n’ai pas envie de passer 10 ans à taper des commandes juste une pull request.
Bonjour,
Effectivement, lorsque l’on a les droits directement sur le dépôt de référence (ce qui est majoritairement le cas en entreprise avec GitLab), la création d’une MR/PR se résume aux étapes que vous avez indiquées.
Cependant, dans le cas d’une contribution à un projet dont on est pas propriétaire on est obligé de passer par l’étape de fork, ce qui fait bien tomber dans la situation de workflow triangulaire.
Ensuite, pour une PR/MR simple et rapide, un simple fork-branch-commit-push-PR est, comme vous le précisez, la plupart du temps suffisant.
Mais il peut arriver que la PR/MR stagne pendant un moment, que le projet soit très actif à côté ou autre qui nécessite alors la mise à jour de votre PR/MR par rapport au projet upstream.
Et à ce moment-là, il est utile de savoir comment gérer cela, d’où cet article.
Pour information, depuis janvier 2017 (date de l’article), les plateformes ont beaucoup évolué permettant la gestion des conflits dans l’interface, la proposition/intégration de suggestions, etc. Ce qui n’était pas le cas à l’époque et nécessitait de gérer à la main avec ces fameuses commandes complexes.
Cet article est aussi une excuse pour aller un peu plus loin dans la compréhension du fonctionnement de git sur la gestion des branches et des remotes et découvrir les possibilités qu’offre l’outil.
Bonjour,
J’ai beaucoup apprécié cette article, qui a le mérite de détailler le principe du pull request.
Merci pour le temps passer à détailler.
J’ai néanmoins une question néophyte.
Supposons que vous participiez à un projet, et que votre objectif est de proposer des corrections sur des problèmes remontés. Quelle est la démarche à adopter.
Le principe est de travailler sur une branche personnel (autre que master) dans mon cas supposons la branche “Work”
Je vais par exemple décider de corriger le bug #2324, et vais faire un pull request de mon Work vers le l’upstream avec commentaire “Proposition de fix pour #2324”
Par la suite je souhaite corriger le bug #7867
Comment est-ce que je procède ?
Je ne souhaite en fait que proposer la solution #7867 dans Pull request.
Et travailler sur Work.
Une solution serait de créer autant de branche que de correction (fix) que je souhaite remonter.
Mais est-ce l’approche a adopter ?
Merci
Cdt.
Christian
Bonjour,
Content que l’article vous satisfasse.
Afin de corriger plusieurs problèmes d’un même projet (et donc dépôt), la bonne pratique est effectivement de créer autant de branche que de problème à corriger (nommées par exemple
issue-2324
etissue-7867
) et de suivre le process classique de pull request pour chaque branche.Ping : Épisode 38 – Le poireau mutant – Android Leaks
Bonjour,
Je ne sais pas ce qui a pu changer depuis votre article mais j’ai une erreur avec les @{push} :
fatal: ambiguous argument ‘@{push}..HEAD’: unknown revision or path not in the working tree.
Je précise que je suis au niveau 0 absolu sur git et je suis tombé sur cette page à force de recherches sur le sujet, pour cloner un repo, faire des modifs en local et soumettre des pull request.
Merci.
Bonjour,
Au début de l’article il est indiqué que cette référence
@{push}
ne fonctionne qu’avec la définition des configurationsremote.pushDefault=origin
etpush.default=current
.Bonjour,
j’ai suivi l’article est même en ayant utilisé la définition des configurations, j’ai la même erreur citée précédemment :
fatal: ambiguous argument '@{push}': unknown revision or path not in the working tree.
Merci
Bonjour,
effectivement, bien que ce soit exactement ce qui est documenté pour la définition de
@{push}
(https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltbranchnamegtpushemegemmasterpushemempushem), il semble que cela ne fonctionne plus.Potentiellement un bug à vérifier et remonter côté git ?…
En attendant, le mieux est d’utiliser à la place explicitement
origin/le_nom_de_votre_branche
Après un peu d’investigation, il semblerait que, contrairement au moment de l’écriture de l’article il y a 6 ans, la résolution ne fonctionne qu’après avoir publié la branche.
On peut donc adapter en faisant un premier push juste après avoir créé la branche, avant d’avoir fait de commit.