Flux de travail pour les Pull Request

Le "flux de travail pour les PR" utilisé par Godot est commun à de nombreux projets utilisant Git, et devrait être familier aux contributeurs vétérans de logiciels libres. L'idée est que seul un petit nombre (s'il y en a) s'engage directement dans la branche master. Au lieu de cela, les contributeurs fork le projet (c'est-à-dire en créent une copie, qu'ils peuvent modifier comme ils le souhaitent), puis utilisent l'interface GitHub pour demander un pull d'une des branches de leur fork vers une branche du dépôt original (souvent appelé upstream).

La Pull Request (PR) qui en résulte peut alors être examinée par d'autres contributeurs, qui peuvent l'approuver, la rejeter ou, le plus souvent, demander que des modifications soient apportées. Une fois approuvée, la PR peut alors être fusionnée par l'un des développeurs principaux, et son ou ses commit(s) feront partie de la branche cible (généralement la branche master).

Nous passerons ensemble par un exemple pour montrer le flux de travail typique et les commandes Git associées. Mais tout d'abord, jetons un coup d'oeil rapide à l'organisation du dépôt Git de Godot.

Dépôt Git source

Le repository sur GitHub est un dépôt de code de Git avec un système de suivi des problèmes et PR intégré.

Note

Si vous participer à la documentation, son dépôt peut être trouvé ici.

Le système de contrôle de version Git est l'outil utilisé pour garder la trace des modifications successives du code source - pour contribuer efficacement à Godot, l'apprentissage des bases de la ligne de commande Git est hautement recommandé. Il existe quelques interfaces graphiques pour Git, mais elles encouragent généralement les utilisateurs à prendre de mauvaises habitudes concernant le flux de travail Git et PR, et nous recommandons donc de ne pas les utiliser. En particulier, nous conseillons de ne pas utiliser l'éditeur en ligne de GitHub pour les contributions de code (bien qu'il soit toléré pour les petites corrections ou modifications de la documentation) car il impose un commit par fichier et par modification, ce qui conduit rapidement à des PR avec un historique Git illisible (surtout après une révision par les pairs).

Voir aussi

Les premières sections du "Livre de Git" sont une bonne introduction à la philosophie de l'outil et aux différentes commandes que vous devez maîtriser dans votre travail quotidien. Vous pouvez les lire en ligne sur le site web de Git SCM.

Les branches sur le dépôt Git sont organisées de cette manière :

  • La branche master est celle ou se déroule le développement de la prochaine version majeure. En tant que branche de développement, elle peut être instable et n'est pas destinée à être utilisée en production. C'est ici que les Pull Requests devraient être faites en priorité.
  • Les branches stables sont nommées d'après leur version, par exemple 3.1 et 2.1. Elles sont utilisées pour le rétro-portage des corrections de bugs et des améliorations depuis la branche master vers la version stable actuellement maintenue (par exemple 3.1.2 ou 2.1.6). En règle générale, la dernière branche stable est maintenue jusqu'à la prochaine version majeure (par exemple, la branche 3.0 a été maintenue jusqu'à la sortie de Godot 3.1). Si vous voulez effectuer les Pull Requests sur une branche stable maintenue, vous devrez vérifier si vos modifications sont également pertinentes pour la branche master, et si c'est le cas, faites les PR pour la branche master en priorité. Les responsables de version peuvent ensuite sélectionner la correction pour une branche stable si cela est pertinent.
  • Il peut y avoir occasionnellement des branches de fonctionnalités, généralement destinées à être fusionnées dans la branche master à un moment donné.

Forker et cloner

La première étape consiste à forker le dépôt godotengine/godot sur GitHub. Pour ce faire, vous devez disposer d'un compte GitHub et y être connecté. Dans le coin en haut à droite de la page du dépôt GitHub, vous devriez voir un bouton "Fork" comme montré ci-dessous :

../../_images/github_fork_button.png

Cliquez dessus, et après un certain temps, vous devriez être redirigé vers votre propre fork du dépôt Godot, avec votre nom d'utilisateur GitHub comme espace de noms :

../../_images/github_fork_url.png

Vous pouvez ensuite cloner votre fork, c'est-à-dire créer une copie locale du dépôt en ligne (dans Git, on parle d'origin remote), Si vous ne l'avez pas déjà fait, téléchargez Git depuis son site web si vous utilisez Windows ou macOS, ou installez le à partir de votre gestionnaire de paquets si vous utilisez Linux.

Note

Si vous êtes sous Windows, ouvrez Git Bash pour entrer des commandes. Les utilisateurs de macOS et Linux peuvent utiliser leurs terminaux respectifs.

Pour cloner votre fork depuis GitHub, utilisez la commande suivante :

$ git clone https://github.com/USERNAME/godot

Note

Dans nos exemples, le caractère "$" désigne l'invite de la ligne de commande sur les shells UNIX. Il ne fait pas partie de la commande et ne doit pas être tapé.

Après un certain temps, vous devriez avoir un répertoire godot dans votre répertoire de travail courant. Déplacez-vous dedans en utilisant la commande cd :

$ cd godot

Nous allons commencer par mettre en place une référence vers le dépôt original que nous avons forké :

$ git remote add upstream https://github.com/godotengine/godot
$ git fetch upstream

Cela va créer une référence nommée upstream pointant vers le dépôt original godotengine/godot. Cela s’avérera utile quand vous voudrez récupérer les nouveaux commits de sa branche master pour mettre à jour votre fork. Vous avez une autre référence nommée origin, qui pointe directement vers votre fork (USERNAME/godot).

Vous n'avez besoin d'effectuer les étapes ci-dessus qu'une seule fois, tant que vous gardez ce répertoire godot local (que vous pouvez déplacer si vous le souhaitez, ses métadonnées importantes sont cachées dans son sous-dossier .git).

Note

Branch it, pull it, code it, stage it, commit, push it, rebase it... technologic.

Cette mauvaise interprétation de Technologic des Daft Punk montre la conception générale qu'ont les débutants du workflow Git : beaucoup de commandes étranges à apprendre par le copier-coller, en espérant qu'elles fonctionneront comme prévu. Cette manière d'apprendre n'est finalement pas si mauvaise, tant que vous êtes curieux et que vous n'hésitez pas à demander de l'aide à votre moteur de recherche quand vous êtes perdu. Nous allons donc vous donner les commandes de base à connaître quand vous travaillez avec Git.

Par la suite, nous supposerons que vous souhaitez implémenter une fonctionnalité dans le gestionnaire de projets de Godot, qui est codé dans le fichier editor/project_manager.cpp.

Les Branches

Par défaut, le git clone devrait vous avoir placé sur la branche master de votre fork(origin). Pour commencer le développement de votre propre fonctionnalité, nous allons créer une branche de fonctionnalité :

# Create the branch based on the current branch (master)
$ git branch better-project-manager

# Change the current branch to the new one
$ git checkout better-project-manager

Cette commande est équivalente :

# Change the current branch to a new named one, based on the current branch
$ git checkout -b better-project-manager

Si vous voulez retourner sur la branche master, vous devez utiliser :

$ git checkout master

Vous pouvez voir la branche sur laquelle vous êtes actuellement avec la commande git branch :

$ git branch
  2.1
* better-project-manager
  master

Assurez-vous de toujours revenir à la branche master avant de créer une nouvelle branche, car votre branche actuelle servira de base à la nouvelle. Vous pouvez aussi spécifier une branche de base personnalisée après le nom de la nouvelle branche :

$ git checkout -b my-new-feature master

Mettre à jour votre branche

Cela n'est pas nécessaire la première fois (juste après avoir forké le dépôt upstream). Cependant, la prochaine fois que vous voudrez travailler sur quelque chose, vous remarquerez que la branche master de votre fork se trouve plusieurs commits derrière la branche master du dépôt upstream : des pull requests d'autres contributeurs ont été fusionnées entre-temps.

Pour vous assurer qu'il n'y aura pas de conflits entre la fonctionnalité que vous développez et la branche master actuelle du dépôt upstream, vous allez devoir mettre à jour votre branche en faisant un pulling de la branche de ce dépôt.

$ git pull --rebase upstream master

L'argument --rebase garantira que tout changement local que vous avez engagé sera appliqué à nouveau en haut de la branche pulled, ce qui est généralement ce que nous voulons dans notre flux de travail PR. De cette façon, lorsque vous ouvrez une pull request, vos propres commits seront la seule différence avec la branche master en amont.

Lors du rebase, des conflits peuvent survenir si votre commit a modifié du code qui a été modifié dans la branche amont entre-temps. Si cela se produit, Git s'arrêtera au commit en conflit et vous demandera de résoudre les conflits. Vous pouvez le faire avec n'importe quel éditeur de texte, puis effectuer les modifications (plus à ce sujet plus tard) et continuer avec `` git rebase --continue``. Répétez l'opération si les validations ultérieures comportent également des conflits, jusqu'à ce que l'opération de rebase soit terminée.

Si vous n'êtes pas sûr de ce qui se passe lors d'un rebase et que vous paniquez (pas d'inquiétude, nous le faisons tous les premières fois), vous pouvez annuler la rebase avec git rebase --abort. Vous serez alors de retour à l'état initial de votre branche avant d'appeler git pull --rebase.

Note

Si vous omettez l'argument --rebase, vous allez créer à la place un commit de merge(fusion) qui indique à Git ce qu'il faut faire des deux branches distinctes. Si des conflits surviennent, ils seront résolus en une seule fois via ce commit de merge(fusion).

Bien qu'il s'agisse d'un flux de travail valable et du comportement par défaut de git pull, les commits de merge(fusion) au sein des PR sont mal vus dans notre flux de travail PR. Nous ne les utilisons que lorsque nous mergeons(fusionnons) des PR dans la branche en amont.

La philosophie est qu'une PR doit représenter l'étape finale des changements apportés à la base de code, et nous ne sommes pas intéressés par les erreurs et les corrections qui auraient été faites dans les étapes intermédiaires avant la fusion. Git nous donne d'excellents outils pour "réécrire l'histoire" et faire comme si nous avions bien fait les choses la première fois, et nous sommes heureux de l'utiliser pour garantir que les changements sont faciles à examiner et à comprendre longtemps après leur fusion.

Si vous avez déjà créé un commit de merge(fusion) sans utiliser rebase, ou si vous avez fait d'autres changements qui ont entraîné un historique non souhaité, la meilleure option est d'utiliser un rebase interactive sur la branche amont. Voir la section dédiée pour les instructions.

Astuce

Si à tout moment vous souhaitez réinitialiser une branche locale sur un commit ou une branche donnée, vous pouvez le faire avec git reset --hard <commit ID> ou git reset --hard <remote>/<branch> (par exemple git reset --hard upstream/master).

Soyez averti que cela supprimera tous les changements que vous avez pu commit dans cette branche. Si jamais vous perdez des commits par erreur, utilisez la commande git reflog pour trouver l'ID de commit de l'état précédent que vous souhaitez restaurer, et utilisez-le comme argument de git reset --hard pour revenir à cet état.

Effectuer des modifications

Vous pouvez ensuite faire vos modifications sur le fichier editor/project_manager.cpp de notre exemple avec votre environnement de développement habituel (éditeur de texte, IDE, etc.).

Par défaut, ces changements sont unstaged. La zone de staging est une couche entre votre répertoire de travail (où vous effectuez vos modifications) et le dépôt Git local (les commits et toutes les métadonnées du dossier .git). Pour apporter des modifications depuis le répertoire de travail vers le dépôt Git, vous devez les stage avec la commande git add, puis les valider avec la commande git commit.

Il existe plusieurs commandes que vous devez connaître pour revoir votre travail actuel, avant de le mettre en zone de staging, pendant qu'il est mis en zone de staging et après qu'il ait été commis.

  • git diff vous montrera les changements non stagé (absent de la zone de staging) actuels, c'est-à-dire les différences entre votre répertoire de travail et la zone de staging.
  • git checkout -- <files> annule les modifications non stagé apportées aux fichiers donnés.
  • git add <files> stage les changements sur les fichiers listés.
  • git diff --staged affichera les changements d'étape actuels, c'est-à-dire les différences entre la zone de staging et le dernier commit.
  • git reset HEAD <files> unstage les changements des fichiers listés.
  • git status vous indiquera toutes les modifications en cours, qu'elles soient ou non dans la zone de staging.
  • git commit commitra les fichiers mis en staging. Il ouvrira un éditeur de texte (vous pouvez définir celui que vous voulez utiliser avec la variable d'environnement GIT_EDITOR ou le paramètre core.editor dans votre configuration Git) pour vous permettre d'écrire un journal de commit. Vous pouvez utiliser git commit -m "Cool commit log" pour écrire le journal directement.
  • git commit --amend vous permet de modifier le dernier commit avec vos modifications en cours (ajouté avec git add). C'est la meilleure option si vous voulez corriger une erreur dans le dernier commit (bug, faute de frappe, problème de style, etc.).
  • git log vous listera les dernier commits de votre branche courante. Si vous avez fait des commits locaux, ils devraient être affichés en haut.
  • git show vous montrera les modifications effectuées par le dernier commit. Vous pouvez également spécifier le hash d'un commit pour voir les modifications de ce commit.

Cela fait beaucoup à mémoriser ! Ne vous inquiétez pas, consultez juste cet aide-mémoire quand vous avez besoin de faire des modifications, et apprenez par la pratique.

Voici à quoi pourrait ressembler l'historique du shell dans notre exemple :

# It's nice to know where you're starting from
$ git log

# Do changes to the project manager with the nano text editor
$ nano editor/project_manager.cpp

# Find an unrelated bug in Control and fix it
$ nano scene/gui/control.cpp

# Review changes
$ git status
$ git diff

# We'll do two commits for our unrelated changes,
# starting by the Control changes necessary for the PM enhancements
$ git add scene/gui/control.cpp
$ git commit -m "Fix handling of margins in Control"

# Check we did good
$ git log
$ git show
$ git status

# Make our second commit
$ git add editor/project_manager.cpp
$ git commit -m "Add a pretty banner to the project manager"
$ git log

Avec cela, nous devrions avoir deux nouveaux commits dans notre branche better-project-manager qui n'étaient pas dans notre branche master. Ces modifications restent seulement locales et le fork distant, ainsi que le dépôt upstream ne savent pas qu'ils existent.

Pousser les modifications sur un dépôt distant

C'est là qu'intervient le git push. Dans Git, un commit est toujours fait sur le dépôt local (contrairement à Subversion ou un commit modifie directement le dépôt distant). Vous devez pousser les nouveaux commits sur une branche distante pour les partager au reste du monde. Sa syntaxe est la suivante :

$ git push <remote> <local branch>[:<remote branch>]

La partie à propos de la branche distante peut être omise si vous voulez qu'elle ait le même nom que la branche locale, ce qui est le cas dans notre exemple, nous ferons donc :

$ git push origin better-project-manager

Git vous demandera votre nom d'utilisateur et votre mot de passe, et les modifications seront envoyées sur votre dépôt distant. Si vous vous rendez sur la page GitHub de votre fork, vous devriez voir une nouvelle branche avec les commits que vous avez ajouté.

Émission d'une Pull Request

Lorsque vous chargez la branche de votre fork sur GitHub, vous devriez voir une ligne disant "This branch is 2 commits ahead of godotengine:master." (et potentiellement quelques commit derrière, si votre branche master était désynchronisée avec la branche upstream master).

../../_images/github_fork_make_pr.png

Sur cette ligne, il y a un lien "Pull request". En cliquant sur ce lien, vous ouvrirez un formulaire qui vous permettra de demande une pull request sur le dépôt en amont godotengine/godot. Il devrait vous montrer vos deux commits, et indiquer "Able to merge". Si ce n'est pas le cas (par exemple, il y a beaucoup plus de commits, ou il y a des conflits de merge(fusion)), ne créez pas la PR, quelque chose s'est mal passé. Allez sur l'IRC et demandez de l'aide :)

Utilisez un titre explicite pour la PR et mettez les détails nécessaires dans la zone de commentaires. Vous pouvez faire glisser et déposer des captures d'écran, des GIF ou des projets zippés, le cas échéant, pour présenter ce que votre travail met en œuvre. Cliquez sur "Create a pull request", et tadaa !

Modifier une pull request

Bien qu'elle soit examinée par les autres contributeurs, vous devrez souvent apporter des modifications à votre Pull Request encore non fusionnée, soit parce que les contributeurs le demandent, soit parce que vous avez vous-même trouvé des bugs lors de tests.

La bonne nouvelle, c'est que vous pouvez modifier une pull request simplement en agissant sur la branche depuis laquelle vous avez fait la pull request. Vous pouvez par exemple faire un nouveau commit sur cette branche, le pousser sur votre fork et la Pulll Request sera mise à jour automatiquement :

# Check out your branch again if you had changed in the meantime
$ git checkout better-project-manager

# Fix a mistake
$ nano editor/project_manager.cpp
$ git add editor/project_manager.cpp
$ git commit -m "Fix a typo in the banner's title"
$ git push origin better-project-manager

Cependant, sachez que dans notre flux de travail PR, nous privilégions les commits qui amènent la base de code d'un état fonctionnel à un autre état fonctionnel, sans les commits intermédiaires corrigeant des bogues dans votre code ou des problèmes de style. La plupart du temps, nous préférerons un seul commit dans une PR donné (sauf s'il y a une bonne raison de garder les changements séparés), donc au lieu de créer un nouveau commit, envisagez d'utiliser git commit --amend pour modifier le commit précédent avec vos correctifs. L'exemple ci-dessus deviendrait alors :

# Check out your branch again if you had changed in the meantime
$ git checkout better-project-manager

# Fix a mistake
$ nano editor/project_manager.cpp
$ git add editor/project_manager.cpp
# --amend will change the previous commit, so you will have the opportunity
# to edit its commit message if relevant.
$ git commit --amend
# As we modified the last commit, it no longer matches the one from your
# remote branch, so we need to force push to overwrite that branch.
$ git push --force origin better-project-manager

Le rebase interactif

Si vous n'avez pas suivi les étapes ci-dessus pour modifier(amend) les changements dans un commit au lieu de créer des commits de correction, ou si vous avez écrit vos changements sans être au courant de nos conseils d'utilisation de Git et du flux de travail, les réviseurs pourraient vous demander de rebase votre branche pour squash certains ou tous les commits en un seul.

En effet, si certains commit ont été effectués à la suite de revues de code pour corriger des bogues, des fautes de frappe, etc. dans le commit original, ils ne sont pas pertinents pour un futur lecteur de changelog qui voudrait savoir ce qui s'est passé dans la base de code de Godot, ou quand et comment un fichier donné a été modifié pour la dernière fois.

Pour squash (écraser) ces commits superflus dans le principal, nous devrons rewrite history. C'est vrai, nous avons ce pouvoir. Vous pouvez lire que c'est une mauvaise pratique, et c'est vrai en ce qui concerne les branches du répertoire en amont. Mais dans votre fork, vous pouvez faire tout ce que vous voulez, et tout est autorisé pour obtenir des PR soignées :)

Nous utiliserons la rebase interactive git rebase -i pour faire ceci. Cette commande prend un ID de commit ou un nom de branche en argument, et vous permettra de modifier tous les commit entre ce commit/branche et le dernier de votre branche de travail, le dénommé HEAD.

Bien que vous puissiez donner n'importe quel ID de commit à git rebase -i et examiner tout ce qui se trouve entre les deux, le flux de travail le plus courant et le plus pratique consiste à rebase sur la branche ``master`` en amont, ce que vous pouvez faire avec :

$ git rebase -i upstream/master

Note

Référencer des branches dans Git est un peu délicat en raison de la distinction entre les branches distantes et locales. Ici, upstream/master (avec un /) est une branche locale qui a été extraite de la branche master distante upstream.

Les rebase interactifs ne peuvent se faire que sur des branches locales, donc le / est important ici. Comme le distant en amont change fréquemment, votre branche locale upstream/master peut devenir obsolète, vous pouvez donc la mettre à jour avec git fetch upstream master. Contrairement à git pull --rebase upstream master qui mettrait à jour votre branche actuelle, fetch ne mettra à jour que la référence upstream/master (qui est distincte de votre branche locale master... oui c'est déroutant, mais vous vous familiariserez avec cela petit à petit).

Cela va ouvrir un éditeur de texte (vi par défaut, voir Git docs <https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_editor>__ pour configurer votre préféré) avec quelque chose qui peut ressembler à ceci :

pick 1b4aad7 Add a pretty banner to the project manager
pick e07077e Fix a typo in the banner's title

L'éditeur vous montrera également des instructions sur la manière dont vous pouvez agir sur ces commits. En particulier, il devrait vous dire que "pick" signifie utiliser ce commit (ne rien faire), et que "squash" et "fixup" peuvent être utilisés pour fusionner le commit dans son commit parent. La différence entre "squash" et "fixup" est que "fixup" supprime du journal le commit écrasé. Dans notre exemple, nous ne souhaitons pas conserver dans le journal le commit "Fix a typo", donc nous utilisons :

pick 1b4aad7 Add a pretty banner to the project manager
fixup e07077e Fix a typo in the banner's title

La sauvegarde du fichier et le départ de l'éditeur entraînent l'action du rebase. Le second commit sera fusionné avec le premier, et git log et git show devraient maintenant confirmer que vous n'avez qu'un seul commit avec les changements des deux précédents.

Mais ! vous avez réécrit l'histoire, et maintenant vos branches locales et éloignées ont divergé. En effet, le commit 1b4aad7 dans l'exemple ci-dessus aura changé, et aura donc obtenu un nouveau hash de commit. Si vous essayez de pousser vers votre branche distante, cela provoquera une erreur :

$ git push origin better-project-manager
To https://github.com/akien-mga/godot
 ! [rejected]        better-project-manager -> better-project-manager (non-fast-forward)
error: failed to push some refs to 'https://akien-mga@github.com/akien-mga/godot'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.

C'est un comportement sain, Git ne vous laissera pas imposer des changements qui auraient pour effet de remplacer le contenu distant. Mais c'est en fait ce que nous voulons faire ici, donc nous devrons le forcer :

$ git push --force origin better-project-manager

Et tadaa ! Git sera heureux de remplacer votre branche distante avec ce que vous aviez localement (assurez-vous donc que c'est bien ce que vous vouliez, en utilisant git log). Cela permettra également de mettre à jour la PR en conséquence.

Supprimer une branche Git

Une fois que votre pull request a été fusionnée, il vous reste une dernière chose à faire : supprimer la branche Git de votre pull request. Cela ne posera pas de problèmes si vous ne supprimez pas votre branche mais c'est une bonne pratique à adopter. Vous devrez le faire deux fois, une fois pour la branche locale et une autre pour la branche distante sur GitHub.

Pour supprimer notre branche better-project-manager localement, utilisez cette commande :

$ git branch -d better-project-manager

Par ailleurs, si la branche n'avait pas été encore fusionnée et que nous voulions tout de même la supprimer, vous utiliseriez -D au lieu de -d.

Ensuite, pour supprimer la branche distante sur GitHub, utilisez cette commande :

$ git push origin -d better-project-manager

Vous pouvez également supprimer la branche distante depuis PR GitHub elle-même, un bouton devrait apparaître une fois qu'elle a été fusionné ou fermé.