Un historique Git linéaire et bien rangé

L’une des choses négligées dans de nombreux projets basés sur Git est la valeur d’un historique de commit linéaire. En fait, de nombreux outils et directives découragent les flux de travail Git qui visent un historique linéaire. Je trouve cela triste, car une histoire bien rangée est très précieuse, et il existe un flux de travail simple qui assure une histoire linéaire.

Historique linéaire vs non linéaire

Un historique linéaire est simplement un historique Git dans lequel tous les commits se succèdent. C’est-à-dire vous ne trouverez aucune fusion de branches avec des historiques de commit indépendants.

1 - non linéaire-vs-linéaire

Pourquoi voulez-vous un historique linéaire?

En plus d’être propre et logique, une histoire linéaire est utile lorsque:

  • En regardant l’histoire. Une histoire non linéaire peut être très difficile à suivre – parfois au point que l’histoire est tout simplement incompréhensible.
  • Modifications de retour en arrière. Par exemple: “La fonctionnalité A a-t-elle été introduite avant ou après le correctif B?”.
  • Traquer les bugs. Git a une fonction très soignée appelée Git bisect, qui peut être utilisée pour trouver rapidement quel commit a introduit un bogue ou une régression. Cependant, avec un historique non linéaire, Git bissect devient difficile, voire impossible à utiliser.
  • Annulation des modifications. Dites que vous avez trouvé un commit qui a provoqué une régression ou que vous souhaitez supprimer une fonctionnalité qui n’était pas censée sortir dans une version spécifique. Avec un peu de chance (si votre code n’a pas trop changé), vous pouvez simplement annuler le(s) commit (s) indésirable(s) en utilisant Git revert. Cependant, si vous avez un historique non linéaire, peut-être avec beaucoup de fusions entre branches, ce sera beaucoup plus difficile.

Il y a probablement une autre poignée de situations dans lesquelles un historique linéaire est très précieux, selon la façon dont vous utilisez Git.

Le point est: Moins votre historique est linéaire, moins il est précieux.

Causes d’un historique non linéaire

En bref, chaque commit de fusion est une source potentielle d’un historique non linéaire. Cependant, il existe différents types de commits de fusion.

Fusionner d’une branche de sujet dans master

Lorsque vous avez terminé avec votre branche de sujet et que vous souhaitez l’intégrer dans master, une méthode courante consiste à fusionner la branche de sujet dans master. Peut-être quelque chose le long des lignes:

git checkout mastergit pullgit merge --no-ff my-topic-branch

Une bonne propriété avec cette méthode est que vous conservez les informations sur les commits faisant partie de votre branche de sujet (une alternative serait de laisser de côté “–no-ff”, ce qui permettrait à Git de faire une avance rapide au lieu d’une fusion, auquel cas il peut ne pas être aussi clair lequel des commits appartenait réellement à votre branche de sujet).

Le problème de fusion en master se pose lorsque votre branche de sujet est basée sur un ancien master au lieu de la dernière astuce de master. Dans ce cas, vous obtiendrez inévitablement un historique non linéaire.

2 - merging-old-branch

Le fait que ce soit un problème courant ou non dépend en grande partie de l’activité du référentiel Git, du nombre de développeurs travaillant simultanément, etc.

Fusionner du maître dans une branche de sujet

Parfois, vous souhaitez mettre à jour votre branche pour qu’elle corresponde au dernier maître (par ex. il y a de nouvelles fonctionnalités sur master que vous souhaitez intégrer à votre branche de sujet, ou vous constatez que vous ne pouvez pas fusionner votre branche de sujet en master car il y a des conflits).

Une méthode courante, qui est même recommandée par certains, consiste à fusionner la pointe de master dans votre branche de sujet. C’est une source majeure d’histoire non linéaire!

3 - merging-master-into-topic

La solution : Rebase!

Git rebase est une fonction très utile que vous devriez utiliser si vous voulez un historique linéaire. Certains trouvent le concept de rebasage gênant, mais c’est vraiment assez simple: rejouez les modifications (commits) dans votre branche au-dessus d’un nouveau commit.

Par exemple, vous pouvez utiliser Git rebase pour changer la racine de votre branche de sujet d’un ancien maître à la pointe du dernier maître. En supposant que votre branche de sujet soit extraite, vous pouvez faire:

git fetch origingit rebase origin/master

4 - rebasage

Notez que l’opération de fusion correspondante consisterait à fusionner la pointe du maître dans votre branche de sujet (comme illustré dans la figure précédente). Le contenu résultant des fichiers de votre branche de rubrique serait le même, que vous fassiez une rebase ou une fusion. Cependant, l’histoire est différente (linéaire vs non linéaire!).

Cela sonne bien et bien. Cependant, il y a quelques mises en garde avec rebase dont vous devez être conscient.

Mise en garde 1 : Rebase crée de nouveaux commits

Rebaser une branche créera de nouveaux commits. Les nouveaux commits auront des SHA:s différents des anciens commits. Ce n’est généralement pas un problème, mais vous rencontrerez des problèmes si vous rebasez une branche qui existe en dehors de votre référentiel local (par exemple si votre branche existe déjà sur origin).

Si vous deviez pousser une branche rebasée vers un référentiel distant qui contient déjà la même branche (mais avec l’ancien historique), vous le feriez:

  1. Vous devez forcer à pousser la branche (par exemple git push --force-with-lease), car Git ne vous permettra pas de pousser un nouvel historique vers une branche existante autrement. Cela remplace efficacement l’historique de la branche distante donnée par un nouvel historique.
  2. Peut rendre quelqu’un d’autre très malheureux, car sa version locale de la branche donnée ne correspond plus à la branche de la télécommande, ce qui peut entraîner toutes sortes de problèmes.

En général, évitez d’écraser l’historique d’une branche sur une télécommande (la seule exception est de réécrire l’historique d’une branche en cours de révision de code – selon le fonctionnement de votre système de révision de code – mais c’est une discussion différente).

Si vous devez rebasculer une branche partagée avec d’autres sur une télécommande, un flux de travail simple consiste à créer une nouvelle branche que vous rebasculez, au lieu de rebasculer la branche d’origine. En supposant que vous avez my-topic-branch extrait, vous pouvez faire:

git checkout -b my-topic-branch-2git fetch origingit rebase origin/mastergit push -u origin my-topic-branch-2

… puis dites aux personnes travaillant sur my-topic-branch de continuer à travailler sur my-topic-branch-2 à la place. L’ancienne branche est alors effectivement obsolète et ne doit pas être fusionnée à master.

Mise en garde 2: Résoudre les conflits dans une rebase peut être plus compliqué que dans une fusion

Si vous obtenez un conflit dans une opération de fusion, vous résoudrez tous les conflits dans le cadre de ce commit de fusion.

Cependant, dans une opération de rebase, vous pouvez potentiellement obtenir un conflit pour chaque commit de la branche que vous rebase.

En fait, vous constaterez souvent que si vous obtenez un conflit dans un commit, vous rencontrerez des conflits liés (très similaires) dans les commits ultérieurs dans votre branche, simplement parce que les commits dans une branche de sujet ont tendance à être liés (par exemple, modifier les mêmes parties du code).

La meilleure façon de minimiser les conflits est de suivre ce qui se passe dans master et d’éviter de laisser une branche de sujet s’exécuter trop longtemps sans rebasage. Gérer de petits conflits de temps en temps est plus facile que de les gérer tous dans un grand désordre de conflit heureux à la fin.

Quelques avertissements pour les utilisateurs de GitHub

GitHub est excellent dans beaucoup de choses. C’est fantastique pour l’hébergement Git, et il a une interface Web merveilleuse avec la navigation de code, une belle fonctionnalité de démarcation, l’essentiel, etc.

Les Pull requests, en revanche, ont quelques fonctions qui contrecarrent activement l’historique Git linéaire. Il serait très bienvenu que GitHub corrige réellement ces problèmes, mais d’ici là, vous devez être conscient des lacunes:

“Merge pull Request” permet des fusions non linéaires vers master

Il peut être tentant d’appuyer sur le bouton vert et convivial “Merge pull request” pour fusionner une branche de sujet dans master. D’autant plus qu’il lit “Cette branche est à jour avec la branche de base. La fusion peut être effectuée automatiquement “.

 GitHub merge pull request

Ce que GitHub dit vraiment ici, c’est que la branche peut être fusionnée pour maîtriser sans conflits. Il ne vérifie pas si la branche de demande d’extraction est basée sur le dernier maître.

En d’autres termes, si vous voulez un historique linéaire, vous devez vous assurer que la branche de requête de tirage est rebasée par-dessus le dernier maître vous-même. Pour autant que je sache, aucune information de ce type n’est disponible via l’interface Web de GitHub (sauf si vous utilisez des “branches protégées” – voir ci-dessous), vous devez donc le faire à partir de votre client Git local.

Même si la requête d’extraction est correctement rebasée, il n’y a aucune garantie que l’opération de fusion dans l’interface web GitHub sera atomique (i.e. quelqu’un peut pousser les modifications à master avant que votre opération de fusion ne passe – et GitHub ne se plaindra pas).

Donc vraiment, la seule façon d’être sûr que vos branches sont correctement rebasées au-dessus du dernier maître est de faire l’opération de fusion localement et de pousser le maître résultant manuellement. Quelque chose le long des lignes:

git checkout mastergit pullgit checkout my-pullrequest-branchgit rebase mastergit checkout mastergit merge --no-ff my-pullrequest-branchgit push origin master

Si vous n’avez pas de chance et que quelqu’un parvient à pousser les modifications à maîtriser entre vos opérations de traction et de poussée, votre opération de poussée sera refusée. C’est une bonne chose, cependant, car cela garantit que votre opération est atomique. Juste git reset --hard origin/master et répétez les étapes ci-dessus jusqu’à ce qu’il passe.

Remarque: Respectez les directives de votre projet avec la révision et les tests du code. Par exemple, si vous exécutez des tests automatiques (builds, analyses statiques, tests unitaires, as) dans le cadre d’une pull request, vous devriez probablement soumettre à nouveau votre branche rebasée (soit en utilisant git push-f, soit en ouvrant un nouveau PR) plutôt que de simplement mettre à jour la branche principale manuellement.

La fonctionnalité des branches protégées encourage les fusions à partir du maître

Si vous utilisez des branches protégées et des vérifications d’état dans votre projet GitHub, vous bénéficiez en fait d’une protection contre la fusion d’une branche de requête d’extraction dans le maître à moins qu’elle ne soit basée sur le dernier maître (je pense que la justification est que les vérifications d’état effectuées sur la branche PR doivent toujours être valides après la fusion vers le maître).

Cependant If Si la branche de demande d’extraction n’est pas basée sur le dernier maître, un bouton convivial appelé “Mettre à jour la branche”, avec le texte “Cette branche est obsolète avec la branche de base. Fusionnez les dernières modifications de master dans cette branche “.

github-update-branch

À ce stade, votre meilleure option consiste à rebaser la branche localement et à la pousser de force vers la demande d’extraction. En supposant que vous avez my-pullrequest-branch extrait, faites:

git fetch origingit rebase origin/mastergit push -f

Malheureusement, les demandes d’extraction GitHub ne fonctionnent pas bien avec les poussées de force, de sorte que certaines informations de révision du code peuvent être perdues dans le processus. Si cela n’est pas acceptable, envisagez de créer une nouvelle demande d’extraction (c’est-à-dire de pousser votre branche rebasée vers une nouvelle branche distante et de créer une demande d’extraction à partir de la nouvelle branche).

Conclusion

Si vous vous souciez d’une histoire linéaire:

  • Rebasez votre branche de sujet au-dessus du dernier master avant de la fusionner en master.
  • Ne fusionnez pas master dans votre branche de sujet. Rebase à la place.
  • Lorsque vous partagez votre branche de sujet avec d’autres, créez une nouvelle branche chaque fois que vous devez la rebaser (my-topic-branch-2, my-topic-branch-3, …).
  • Si vous utilisez des pull requests GitHub, sachez :
    • Le bouton “Fusionner” ne garantit pas que la branche PR est basée sur le dernier maître. Rebasez manuellement si nécessaire.
    • Si vous utilisez des branches protégées avec des contrôles d’état, n’appuyez jamais sur le bouton ” Mettre à jour la branche “. Rebase manuellement à la place.

Si vous ne vous souciez pas trop d’une histoire Git linéaire, bonne fusion !

Liste de souhaits pour GitHub

À vous les personnes travaillant chez GitHub : Veuillez ajouter la prise en charge des flux de travail de rebase dans les pull requests (il peut s’agir d’options de dépôt opt-in).

  • Ajoutez la possibilité de désactiver le bouton de fusion/ l’opération dans les pull requests si la branche n’est pas basée sur le dernier maître (cela ne devrait pas nécessiter l’utilisation de contrôles d’état).
  • Ajoutez un bouton “Rebase sur le dernier maître”. Dans de nombreux cas, cela devrait être une opération sans conflit qui peut facilement être effectuée via l’interface Web.
  • Conserve l’historique des requêtes d’extraction (commits, commentaires, comments) après une poussée de rebase/force.

Leave a Reply