Blog Zenika

#CodeTheWorld

DevOps

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 :

  1. Forker le projet
  2. Créer une branche et travailler dessus
  3. Publier la branche sur son fork
  4. 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 (ou branch.<name>.pushRemote pour une branche spécifique) : pour indiquer le dépôt par défaut sur lequel publier
  • push.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


Puis publier :

$ 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.


En apprendre plus :

13 réflexions sur “Les pull-request : comment ça marche, comment en faire une, comment en intégrer une ?

  • Jérôme W

    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

    Répondre
  • 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.

    Répondre
    • 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

      Répondre
  • @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.

    Répondre
    • Alexandre Garnier

      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.

      Répondre
  • Christian

    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

    Répondre
    • Alexandre Garnier

      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 et issue-7867) et de suivre le process classique de pull request pour chaque branche.

      Répondre
  • Ping : Épisode 38 – Le poireau mutant – Android Leaks

  • Fredolu

    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.

    Répondre
    • Alexandre Garnier

      Bonjour,

      Au début de l’article il est indiqué que cette référence @{push} ne fonctionne qu’avec la définition des configurations remote.pushDefault=origin et push.default=current.

      Répondre
  • Florent

    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

    Répondre
    • Alexandre Garnier

      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.

      Répondre

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

En savoir plus sur Blog Zenika

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Continue reading