1 points par GN⁺ 2 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Git a réussi comme dépôt distribué du code source, mais sa gestion des workflows distribués ressemble davantage à une solution greffée a posteriori, dont les limites apparaissent aujourd’hui
  • Les commits et branches de Git ne savent pas exprimer par eux-mêmes les commits ultérieurs, l’historique des amend, l’historique des rebase ni les états abandonnés
  • Avec les Stacked PR, il faut retrouver les PR suivantes et les rebase en conservant la pile, mais Git a du mal à identifier cette relation de manière fiable
  • Git place des états mutables comme le staging, l’unstaged, le système de fichiers et HEAD en dehors des commits et des branches, ce qui complique l’apprentissage et l’usage
  • Dans les flux de développement asynchrone où plusieurs PR doivent être utilisées ensemble avant fusion, le modèle d’historique immuable orienté vers le passé de Git engendre des problèmes récurrents

Les deux rôles de Git

  • Git sert à la fois de dépôt distribué du code source et d’outil de workflow distribué
  • Comme dépôt de code, c’est un immense succès, mais sa manière de gérer les workflows distribués relève pour l’essentiel de solutions ajoutées après coup
  • Le développement asynchrone est presque une condition de base, comme le formule East River Source Control : cela ne concerne pas seulement la collaboration entre fuseaux horaires différents, mais aussi le fait de travailler avec soi-même à des moments distincts
  • jj est un outil qui met plus clairement en lumière les limites de Git, et ceux qui estiment que Git leur suffit auront peu de chances d’essayer jj sérieusement

Les relations que le modèle de base de Git ne représente pas

  • La pensée Git s’organise autour des commits et des branches
    • Un commit est un objet immuable qui contient le code source et son historique
    • Une branche est un pointeur mutable accompagné d’un log
  • Les diagrammes Git classiques dessinent les commits sous la forme C1, C2, C3, ce qui donne l’impression que leur ordre et leurs relations sont évidents, mais dans un dépôt réel les noms de commit ressemblent à des hash ou à des messages, et cette relation d’ordre n’existe pas dans le système lui-même
  • Une notation comme C2 et C2’ après un rebase n’est qu’une façon simple pour les humains de comprendre, mais Git ne sait pas que ces deux commits se correspondent
  • Pour retrouver les commits ultérieurs d’un commit donné, il faut parcourir toutes les branches et rechercher les commits situés sur les chemins qui y mènent ; ce n’est donc pas une opération simple

Il n’y a pas de « C » dans Git

  • Un commit Git ne peut pas connaître par lui-même les informations suivantes
    • Commits ultérieurs

      • L’historique des modifications qui relie un ancien commit à un nouveau commit après un amend
    • Historique des rebase

      • Le fait que ce commit ait été abandonné ou non
      • Les branches ont elles aussi leurs limites
      • Elles ont bien une notion d’historique, mais on ne peut pas leur faire confiance pour correspondre en 1:1 à une modification de code
      • Les branches n’ont pas de relation entre elles ; par exemple, il est impossible de retrouver de façon fiable wp/bugfix à partir de trunk
      • Comme il n’existe pas de référence avant de trunk vers wp/bugfix, ce n’est pas non plus une relation de portée
      • Les diagrammes Git semblent montrer un ordre et des correspondances lisibles pour les humains, mais ils peuvent surestimer les capacités réelles offertes par l’outil

Pourquoi les Stacked PR sont difficiles

  • Lorsqu’on collabore avec des personnes dans d’autres fuseaux horaires et qu’on préfère ne pas fusionner avant revue, il faut pipeliner le travail comme un CPU
  • Au lieu de créer une seule PR puis d’attendre la fin de la revue, on crée une deuxième PR au-dessus de la première, puis la suivante au-dessus de la deuxième, afin de soumettre simultanément à la revue plusieurs PR séquentielles : c’est le principe des Stacked PR
  • Git rend cette structure de Stacked PR difficile à gérer de façon fiable
    • On peut créer une PR suivante comme Refactor key entry code au-dessus de Fix key entry race, puis après avoir récupéré les mises à jour de trunk, il faut rebase l’ensemble tout en conservant la pile
    • Comme Git ne connaît pas les commits ultérieurs, il est difficile de voir facilement Refactor key entry code à partir de Fix key entry race
    • Un commit peut aussi avoir été abandonné ; même si l’on trouve un commit ultérieur, il reste difficile de savoir s’il est à jour
    • Les branches servent pratiquement de PR, mais dans ce flux il est facile de les écraser par erreur
  • Des outils d’empilement comme Graphite permettent de faire cela au-dessus de Git, sans pour autant renforcer les commits ou les branches de Git eux-mêmes
    • Il faut créer un stockage de métadonnées de branche séparé et le synchroniser avec Git
    • Si l’utilisateur manipule directement Git, ce stockage peut se désaligner par rapport à l’état réel de Git

L’état mutable se trouve en dehors des commits

  • Plusieurs problèmes de Git proviennent du fait qu’il ne modélise pas directement la mutabilité
  • Dans le workflow d’édition de Git, il existe plusieurs états distincts en dehors des commits et des branches
    • Le staging ou index est un snapshot du code source construit à partir de la copie de travail, et c’est à partir de lui qu’est créé un nouveau commit
    • L’unstaged est un deuxième diff qui représente l’écart entre l’index et le système de fichiers
    • Le système de fichiers contient le contenu checkouté, auquel s’ajoutent les changements staged et unstaged
    • HEAD est l’endroit où sera créé le nouveau commit
  • Le stash agit comme un stockage séparé permettant de sauvegarder puis de restaurer les changements staging et unstaged
  • Lorsqu’on change de checkout vers un autre commit ou une autre branche, Git tente d’adapter le système de fichiers à cette nouvelle position tout en conservant les diff du staging ou de l’unstaged
  • Le processus emploie d’autres commandes, mais si l’on ne regarde que les relations en flèches, il ressemble à une forme de rebase qui déplace le staging sur une nouvelle base

Pourquoi il est difficile de tout modéliser comme des commits

  • Le staging et la copie de travail ont eux aussi des ancêtres clairs et contiennent du code source ; si l’on ne regarde que l’état statique, on peut donc les représenter comme des commits
  • Mais l’identifiant d’un commit est un hash de son contenu : si le commit est mutable, son ID change en permanence
  • Pour désigner de manière cohérente ce que sont le staging et la copie de travail, il faudrait les traiter non comme des commits mais comme des branches, or les branches ont les limites déjà évoquées
  • Cette complexité se traduit en problèmes concrets
    • L’apprentissage et l’usage de Git deviennent plus difficiles, car le même concept existe séparément des deux côtés
    • L’état global du dépôt diffère fortement de celui que l’on récupère via un clone, ce qui rend son export maladroit
    • Les flux asynchrones où les ensembles de changements évoluent avec le temps fonctionnent mal
    • Le système du côté mutable ne peut pas exprimer les merge, ce qui empêche parfois de représenter le workflow réel

Quand Git ne peut pas représenter le workflow réel

  • En développant sur une nouvelle branche de fonctionnalité sans avoir encore commit, on peut découvrir un bug sur l’appareil qui perturbe le développement
  • Si ce bug ne bloque pas la nouvelle fonctionnalité mais rend le travail pénible, on peut stash le travail en cours, passer sur une nouvelle branche, créer un test de reproduction et un correctif, puis ouvrir une PR
  • Ensuite, lorsqu’on revient sur la branche de nouvelle fonctionnalité, les options sont limitées
    • Rebase new-feature au-dessus de bugfix, même s’il n’existe pas de dépendance réelle, puis poursuivre la revue
    • Pendant le développement, travailler avec new-feature rebased sur bugfix, puis annuler ce rebase avant de soumettre la branche
  • Avec Git, il est impossible d’exprimer l’état suivant : « dans l’espace de travail d’édition, tout le code du bugfix et le code déjà commit de la new feature doivent coexister »
  • Cette exigence réapparaît avec la même structure dans des problèmes plus difficiles, comme les tests de compatibilité avec des PR non encore fusionnées
  • Avec un outil adapté comme Jujutsu megamerges, on peut maintenir plusieurs PR en parallèle tout en les utilisant ensemble dans l’espace d’édition

Git ne suffit plus

  • Les outils de gestion de versions du début des années 2000 étaient souvent difficiles à utiliser et à administrer, avec une qualité inégale, et Subversion passait lui aussi largement pour un outil pénible
  • À l’époque, le besoin de disposer localement d’une copie complète du dépôt n’était pas courant, pas plus que celui de créer des branches locales
  • Beaucoup trouvaient le verrouillage de fichiers pénible, mais d’autres estimaient qu’il était nécessaire et demandaient même s’il était possible de verrouiller des fichiers ou répertoires individuels avec Git
  • Pour ceux qui vivaient directement des workflows distribués, comme dans l’open source, les DVCS ont été perçus comme un pansement protégeant d’anciennes blessures
  • Aujourd’hui, pour ceux qui utilisent un workflow réellement distribué, le modèle d’historique immuable tourné vers le passé de Git devient une source récurrente de problèmes
  • Des entreprises comme Meta utilisent depuis près de dix ans des systèmes internes qui surpassent largement Git
  • La tendance qui consiste à dire que « désormais, c’est Claude qui manipule Git à notre place » ne rend pas ces alternatives sans intérêt
  • Avec les LLM, il semble qu’au sein d’une même machine les ingénieurs pratiquent davantage qu’auparavant le développement asynchrone

1 commentaires

 
GN⁺ 2 시간 전
Commentaires sur Lobste.rs
  • J’aurais aimé que l’article montre comment jj résout les problèmes soulevés
    Pour les utilisateurs de jj, c’est sans doute évident, mais il est peu probable qu’ils soient le public principal de l’article

  • Personnellement, je n’ai jamais eu besoin des fonctionnalités citées dans l’article pour justifier que Git ne va pas si bien
    Je me demande si je suis le seul

    • Tu es loin d’être seul
      L’un des points importants avec un outil, c’est qu’il fait partie d’un système dynamique. Ce qu’un outil rend possible influence « ce que je crois pouvoir faire », et cette croyance modifie à son tour la perception de l’outil ainsi que la direction de son évolution
      Quand un outil bouscule l’existant, les croyances et les attentes sur ce qu’il est possible de faire changent elles aussi
  • Ça a l’air intéressant, mais les diagrammes me donnent le tournis

  • À propos de l’idée qu’on n’est pas aujourd’hui dans une situation aussi grave qu’au début des années 2000, et que les limites des systèmes de gestion de versions d’avant Git étaient assez claires : Darcs est sorti avant Git, et il a corrigé de façon fondamentale certains problèmes de la gestion de versions basée sur les snapshots
    Il a été écarté au départ à cause de mauvaises performances, mais celles-ci se sont ensuite améliorées, et les gens ne sont pas revenus vérifier. Il existe aussi d’autres systèmes de gestion de versions qui font des choses intéressantes, donc j’aimerais qu’on ne présente pas “si ce n’est pas Git, alors c’est Jujutsu” comme si c’était le seul choix possible. Je vois ce type de raisonnement bien trop souvent

    • C’est un peu drôle que l’auteur dise que le modèle de données de Git est très bon, puis remarque au passage que les branches Git ne sont que des pointeurs vers le bout d’une branche et que cela nuit au flux de travail
      C’est aussi un problème de modèle de données
  • Comment jj gère-t-il ceci ? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png

    • Si on utilise jj new A B, le commit de copie de travail peut avoir plusieurs parents, et se comporte donc comme un commit de fusion
      Du coup, la copie de travail récupère les changements des deux parents, et on peut soit continuer à travailler au-dessus de cette fusion, soit faire un amend sur l’un des deux commits
  • Pour l’instant, je préfère encore Git, et l’auteur me semble avoir un certain biais

    • Je me demande si cette préférence pour Git vient du fait que tu n’as pas rencontré les situations que l’auteur dit difficiles dans Git et que tu y es déjà habitué, ou si, même dans ces situations, tu penses que Git offre un meilleur flux de travail que Jujutsu
    • Tant qu’on se souvient qu’il faut exécuter jj new, on peut mélanger git et jj
      Git pointe toujours vers le commit parent, et l’actuel jj commit se présente alors comme les changements non commités de l’arbre de travail
      C’est comme ça que j’ai appris jj. J’utilisais jj pour ce qu’il fait bien, comme gérer les rebases ou déplacer des arbres, et je continuais à utiliser les commandes git pour les tâches quotidiennes quand je ne connaissais pas encore la commande équivalente dans jj, ou quand une commande Git comme git blame me venait d’abord à l’esprit
      En pratique, je n’ai vraiment compris pourquoi jj était meilleur qu’en l’utilisant tous les jours ; en me contentant d’en lire, je pensais plutôt « est-ce que j’ai vraiment besoin de cette fonctionnalité ? » ou « on peut déjà faire ça avec Git »
      Bien sûr, jj a aussi des inconvénients. En l’absence d’un .gitignore à jour, des fichiers binaires peuvent être ajoutés par erreur dans un commit. Heureusement, jj avertit quand on essaie d’ajouter un très gros fichier, mais des fichiers plus petits peuvent passer entre les mailles du filet
      Si, pendant du débogage, il y a dans le répertoire courant un fichier suivi ou un fichier de log, il peut aussi se retrouver inclus ; après avoir beaucoup manipulé l’arbre, mieux vaut donc vérifier tous les diffstats
      C’est particulièrement problématique si, en faisant une recherche dichotomique avec jj, on teste un commit antérieur à celui qui a mis à jour le .gitignore. Il faudrait peut-être un mode lecture seule pour la recherche dichotomique