1 points par GN⁺ 2 시간 전 | 1 commentaires | Partager sur WhatsApp
  • La commande expérimentale git history, introduite entre Git 2.54 et 2.55, automatise la modification d’anciens commits et la reconstruction des branches dérivées tout en conservant le flux de travail Git existant
  • fixup fusionne des changements indexés dans un ancien commit, puis rebase automatiquement toutes les branches locales dérivées de ce commit sur les nouveaux hash de commit
  • reword modifie le message d’un ancien commit, et split divise les changements d’un commit en deux commits au niveau des hunks avant de reconstruire les commits et branches suivants
  • Les trois sous-commandes utilisent un traitement atomique qui refuse les opérations susceptibles de provoquer des conflits, afin d’éviter de laisser l’arbre de travail dans un état intermédiaire cassé, mais elles ne fonctionnent pas sur un historique contenant des commits de fusion
  • Sans offrir la gestion des conflits comme objet de premier ordre, l’annulation basée sur un journal d’opérations, ni la modélisation de la copie de travail en commits comme le fait jj, elles permettent d’utiliser plusieurs fonctions d’édition d’historique à la jj directement dans la distribution Git de base, sans installation séparée

Pourquoi l’édition d’historique Git classique est pénible

  • Quand on gère plusieurs changements en parallèle, il faut constamment naviguer entre branches et commits, et rebase -i peut laisser l’arbre de travail dans un état incomplet en cours d’opération
  • L’alternative à Git jj est souvent citée comme solution à ce problème, mais même après l’avoir essayé pendant quelques jours tous les trois mois durant un an et demi, on finit par revenir au flux de travail Git classique
  • git history est une commande expérimentale incluse dans la distribution Git de base, ce qui permet de l’essayer sans installer d’outil supplémentaire
    • Git 2.54 a introduit reword et split en avril
    • Git 2.55 a ajouté fixup en juin
  • Les sous-commandes se limitent à trois options : fixup, reword et split

fixup : modifier un ancien commit et les branches dérivées en même temps

  • git history fixup corrige les erreurs dans un ancien commit et rebase automatiquement les branches dérivées de ce commit
  • Comme dans une procédure de correction classique, il suffit d’indexer les changements avec git add, puis d’exécuter git history fixup <commit> pour fusionner les changements indexés dans le commit cible
  • C’est similaire à la combinaison de git commit --fixup et d’un rebase autosquash, à la différence près que cela met aussi à jour les autres branches contenant le commit cible
  • Alors que git rebase --update-refs ne déplace que les références situées dans la portée du rebase en cours, git history retrouve et réécrit toutes les branches locales dérivées du commit cible
    • Si nécessaire, on peut limiter la portée de la mise à jour à la branche courante uniquement
    • Cela ne fonctionne pas sur un historique contenant des commits de fusion, ce qui complique l’usage dans certains styles de travail Git
  • Si l’on fusionne une correction dans B, un nouveau B* avec un hash différent est créé, puis C et D au-dessus sont eux aussi recréés respectivement en C* et D*
    • Les extrémités des branches feat-1 et feat-2 se déplacent également pour suivre les nouveaux commits

Traitement atomique et limites liées aux conflits

  • Les trois sous-commandes sont toutes traitées de manière atomique et ne laissent pas l’arbre de travail dans un état à moitié cassé
  • Cette atomicité est garantie en refusant d’emblée les opérations qui pourraient provoquer des conflits
  • Sur ce point, elles sont plus limitées que jj, qui traite les conflits comme des objets de premier ordre et permet de conserver un état conflictuel pendant un rebase pour le résoudre plus tard
  • Les limitations de git history sont intentionnellement appliquées pour éviter que la réécriture d’historique ne devienne une opération de préservation d’état
    • Si Git prend en charge à l’avenir les conflits de premier ordre, ces limitations pourraient être levées

reword : changer le message d’un commit sans toucher à l’arbre de travail

  • git history reword modifie le message d’un ancien commit et reconstruit automatiquement les commits et branches qui le suivent
  • En exécutant git history reword <commit>, l’éditeur s’ouvre avec le message existant ; après modification et enregistrement, la pile de commits suivante est reconstruite pour correspondre aux nouveaux hash
  • C’est utile quand l’architecture a évolué au fil du développement et qu’il faut adapter d’anciens messages de commit à la structure actuelle
  • À l’exception du fait qu’il ne modifie que le message du commit et non le contenu de l’arbre, son fonctionnement est le même que fixup
  • reword ne touche ni à l’index ni à l’arbre de travail et ne modifie que le graphe de commits
    • On peut donc modifier les commits de branches non checkoutées sans perturber le travail en cours
    • Si l’on change le message de B, son hash change, donc le C suivant est recréé en C* et feat-1 suit lui aussi le nouveau commit

split : séparer un commit en deux

  • git history split permet de sélectionner interactivement les changements d’un commit pour les répartir en deux commits
  • En exécutant git history split <commit>, une invite s’ouvre pour choisir les modifications du commit cible hunk par hunk
    • Les hunks sélectionnés et conservés constituent le premier commit
    • Les hunks restants vont dans le second commit
  • C’est comparable à git add -p, mais sans la procédure complexe qui combine git rebase
  • Si l’on scinde un B contenant deux changements sans rapport, B1 et B2 sont créés, puis le C suivant est reconstruit en C* au-dessus de ces deux commits
  • C’est la plus spécialisée des trois commandes, mais elle est utile lorsqu’il faut réellement découper un commit

Différences fonctionnelles avec jj

1 commentaires

 
GN⁺ 2 시간 전
Avis sur Hacker News
  • Git me mettait mal à l’aise, mais ça s’est amélioré après avoir lu les trois premiers chapitres du livre gratuit Pro Git. Il donne un excellent modèle mental du fonctionnement interne de Git et, pour le meilleur ou pour le pire, l’interface utilisateur de Git reflète directement sa structure interne ; une fois cela compris, tout s’est mis en place

    • Je ne suis pas d’accord avec l’idée que l’interface utilisateur reflète directement la structure interne. Par exemple, git checkout est conçu pour ne pas perdre les modifications qui ne sont pas encore entrées dans Git quand on change de branche, mais pour annuler les changements quand on restaure une autre version d’un fichier : en quoi cela reflète-t-il directement la structure interne ?
      À part cela, même quand on sait ce qu’on veut faire en interne et quelle commande utiliser, l’incohérence de la CLI rend difficile de trouver la syntaxe exacte de la ligne de commande, ce qui complique aussi l’utilisation de Git
    • Git n’est pas aussi confus qu’on le dit ; il suffit d’y consacrer quelques heures pour le comprendre. C’est un outil indispensable pour les ingénieurs logiciel, et il est regrettable de ne pas l’apprendre ; pour un professionnel, git commit -am “Changes” ne suffit pas
    • Moi aussi, les commandes Git me semblaient déroutantes, mais après avoir vu une vidéo YouTube où Linus explique les structures de données internes de Git, j’ai compris que c’était très simple. Il devenait facile de faire le lien entre les fonctions de l’outil et la manière dont elles manipulent les structures de données ; ce sont plutôt les abstractions de plus haut niveau que Git essaie de fournir qui rendent les choses plus confuses
    • Non. La moitié des commandes Git sont de bas niveau et l’autre moitié de haut niveau, donc il n’y a pas de correspondance 1:1 avec la structure interne
    • L’expérience utilisateur de Mercurial était de loin la meilleure, et on pouvait l’apprendre sans lire de documentation particulière. Même en utilisant Git, j’ai tenu toute ma carrière en m’en servant comme de Mercurial, puis en supprimant le dépôt local quand un problème survenait
  • Il existe git rebase --abort, et on peut aussi créer un tag avant le rebase puis revenir en arrière avec git reset --hard $set_tag. Comme l’état précédent ne disparaît pas, il n’y a aucune raison d’avoir peur du rebase interactif

    • Je crée beaucoup de branches appelées temp ou before-rebase précisément pour ça. Je m’en sers comme de tags, mais les branches sont plus faciles à déplacer ; avant un gros rebase -i, je lance donc souvent git branch before-rebase/some-feature
      En pratique, j’ai rarement eu besoin de git reset before-rebase, mais il m’est souvent arrivé d’utiliser git log -p before-rebase pour comparer avec la branche après rebase et vérifier les modifications involontaires introduites lors de la résolution des conflits
    • Il n’est même pas nécessaire de créer un tag de sécurité séparé : le reflog joue automatiquement ce rôle
    • Tu utilises git reflog ?
    • Difficile de faire plus simple que jj undo
    • Exact. Après avoir tapé git rebase, environ une fois sur deux je tape immédiatement --abort
  • Je ne me considère pas comme codeur ni programmeur, mais apprendre Git a eu l’impression de donner à ma façon de penser un super-pouvoir d’organisation. Je l’utilise pour quantité d’usages : sites web, projets de design, électronique, composition musicale, base de connaissances personnelle, scripts d’administration à distance, gestion de configuration, fragments de code, etc. Ce n’est pas parfait, mais je recommande à toutes les personnes avec qui je travaille de l’apprendre

    • Moi aussi, ma capacité d’organisation s’est nettement améliorée, mais c’est davantage l’effet des systèmes de gestion de versions en général que de Git lui-même. Un système de gestion de versions permet d’organiser et de conserver ce qu’on crée, et d’en ressortir autant de copies qu’on veut à tout moment, ce qui permet de travailler sans inquiétude
      Dans Getting Things Done de David Allen, il est question de la nécessité d’un système fiable pour collecter et organiser ses pensées. J’utilise OmniFocus pour les tâches, projets et listes, mais je fais confiance à Git pour organiser et conserver les logiciels et certaines données que je crée sur ordinateur
    • Comment utilises-tu Git pour la composition musicale ?
  • J’aime beaucoup ces commandes en théorie et j’utilise toujours leurs équivalents dans jj, mais je n’ai pas trouvé comment faire pour que git history signe les commits qu’il modifie. Même en consultant le manuel et le code source de Git, si je modifie un commit GPG déjà signé avec git history reword HEAD~, toutes les signatures disparaissent
    Comme je veux conserver les signatures de commits, je suis finalement revenu au classique git rebase -i

    • Cela ressemble à un gros oubli et semble aussi contourner le flux standard de traitement des commits. Le manuel dit également que git history est une fonctionnalité expérimentale, donc cela vaut la peine de le signaler comme bug
  • Même sans nouvelle commande, j’ai fortement réécrit l’historique de commits Git local pendant plus de dix ans sans grande difficulté. Je suis convaincu que l’une des principales raisons pour lesquelles Git et le rebase paraissent difficiles tient à un problème d’interface utilisateur : il faut comprendre soi-même les options exactes de ligne de commande et leur comportement.
    Sous Windows, la boîte de dialogue de rebase de TortoiseGit était à la fois puissante et facile à utiliser. Dans le journal des commits, il suffit de faire un clic droit sur le commit de référence et de choisir "Rebase onto this" ; comme dans https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm..., le changement d’ordre, la division, la combinaison et l’édition des commits sont clairs, et si l’on annule en cours de route, on revient à l’état initial.
    Cela dit, comme il s’agit d’une extension du shell, il y a trop de clics ; une application autonome avec des onglets serait meilleure, mais cela semble relever du principe de conception hérité de TortoiseSVN. J’ai aussi essayé brièvement Git Extensions, mais je n’y ai pas trouvé des fonctions que j’utilisais souvent, ou bien j’étais déjà trop habitué à TortoiseGit pour m’y installer. L’écran de journal est visible ici : https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...

    • Après être passé de Windows à Linux, l’une des choses qui m’ont manqué est le shell intégré qui permettait à tous les programmes d’ajouter des menus contextuels sur les fichiers et dossiers, des overlays comme les icônes d’état de TortoiseGit, ou des objets spéciaux comme My Computer. À mon sens, cela a atteint son apogée avec XP, puis Microsoft a changé les choses à partir de Vista dans un sens rendant les extensions plus difficiles.
      Je ne comprends toutefois toujours pas pourquoi un certain menu de VC++6 créait, au lieu d’une liste normale, un objet shell ressemblant à un dossier qui s’affichait dans la boîte de dialogue d’ouverture de fichier. C’était un assistant pour ajouter des composants à un projet, et pendant que la boîte de dialogue était ouverte, je retrouvais ce faux dossier dans My Computer.
    • Il est vrai que le flux de travail existant était un peu plus laborieux et que la nouvelle commande est plus pratique. fixup était déjà possible avec git commit --fixup, ou auparavant avec git rebase -i, et reword pouvait aussi se faire avec git rebase -i.
      Il semble que suffisamment de retours se soient accumulés sur le côté pénible de l’édition d’un fichier pour décider de l’action à effectuer sur chaque commit, et qu’une commande de plus haut niveau enveloppant les fonctionnalités existantes ait été ajoutée.
    • Parmi les interfaces utilisateur pour Git, je n’ai jamais vu d’outil approchant Magit. Il offre à la fois des fonctionnalités profondes et une interface simplifiée à état persistant.
      Avec les outils en ligne de commande, dès que l’on exécute la commande suivante, il devient difficile d’accéder à la sortie précédente. Même si l’on veut cherry-pick un commit vu dans git log, si le hash n’est plus dans le terminal, on ne peut pas le saisir de mémoire ; une interface à état persistant maintient plusieurs interactions en parallèle et conserve les informations précédentes.
      Git comporte énormément d’informations d’état à retenir : branche courante, branche précédente, fichiers modifiés, commits non poussés, etc. Si l’on ne maîtrise pas les outils périphériques comme l’affichage de branche et l’autocomplétion des plugins de shell, les multiples onglets et divisions de fenêtre, la copie entre onglets ou la recherche par autocomplétion, l’utilisation de Git peut devenir très désagréable.
  • Selon moi, la seule vraie faiblesse de jj est l’intégration avec Git. Il formalise des langages spécifiques au domaine pour les ensembles de fichiers, les ensembles de révisions et les templates, les utilise de façon cohérente dans toute la configuration et la CLI, et propose des concepts puissants dans son interface sans exposer directement sa structure interne.
    Mais il est si flexible que l’interopérabilité avec Git dépend fortement des conventions et de la discipline, et comme presque tout l’état de jj peut être représenté dans Git, il est facile de casser un dépôt distant. Pour s’en remettre, il faut comprendre Git en profondeur.
    Pour un nouveau projet développé en solo, jj vaut la peine d’être essayé. Dans de bonnes conditions, il rappelle le Git des débuts par rapport à Subversion ; si l’on se familiarise d’abord avec la façon de penser propre à jj, on est ensuite un peu mieux préparé à l’intégration plus difficile avec Git. Le contexte lié se trouve ici : https://news.ycombinator.com/item?id=48903054

    • En principe, jj dispose aussi de backends autres que Git, mais le seul explicitement indiqué comme adapté à un environnement de production aujourd’hui est le backend Git. Sans intégration Git, jj relève encore davantage du concept que de la réalité.
      J’aime jj et son modèle mental du contrôle de version, et je continuerai à l’utiliser, mais comme il faut parfois redescendre dans Git pour terminer un travail, il ressemble davantage à un wrapper pratique au-dessus de Git qu’à un nouveau système de gestion de versions. Si Git ajoute des commandes prenant en charge le workflow de jj, il deviendra difficile de trouver une raison d’installer jj séparément.
  • La façon dont git history fixup rebase automatiquement toutes les branches descendantes ne correspond pas à mon workflow. Je conserve chaque version d’une branche de fonctionnalité sous des noms comme myfeature.1, myfeature.2, afin de retrouver une ancienne version lorsque le comportement change dans une nouvelle version ; git reflog ne peut pas remplacer cela.
    Par exemple, si le problème est apparu entre myfeature.58 et myfeature.59 et que chaque fonctionnalité compte 15 commits, git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59 permet de voir, commit par commit, les différences entre les deux versions même si les commits de base sont différents. Je ne veux donc pas d’un rebase automatique de toutes les branches contenant un commit donné, et l’approche consistant à utiliser des tags pour les anciennes versions ne m’a pas non plus vraiment convenu.

  • Les versions récentes de Git semblent avoir implémenté les trois opérations fréquentes de git rebase --interactive sous forme de commandes séparées avec moins de friction, et elles ne semblent fonctionner qu’en l’absence de conflits.

    • Je me demande si git history est vraiment utile. Je préfère déplacer visuellement les commits avec un rebase interactif, et je ne veux pas désigner par erreur comme cible de git history le hash d’un commit orphelin disparu de la branche locale courante.
  • Je ne comprends pas pourquoi on consacre autant d’efforts à peaufiner parfaitement l’historique Git. De toute façon, on ne relit pas les commits individuellement, donc il suffit de tout squasher avant la fusion

    • Pas du tout d’accord. Quand je relis une PR, je lis au minimum l’historique des commits pour comprendre le fil des changements, et s’il est rempli de "oops" et de "fix", je la rejette immédiatement
      Les commits racontent l’histoire du travail effectué ; il faut donc fournir un historique de commits lisible pour éviter au reviewer de devoir se démener davantage pour comprendre l’intention
    • Je lis souvent les commits individuellement. Dans un dépôt soigneusement entretenu, c’est très utile pour traquer des bugs ou des régressions et comprendre l’intention des changements de l’auteur
    • Quand on se demande pourquoi le code est comme ça, le contexte du commit qui l’a introduit aide beaucoup. Si c’est un énorme commit qui regroupe des changements sans rapport, sa valeur diminue fortement
    • C’est le genre de raisonnement qui montre qu’on ne travaille pas dans une grande équipe. Quand je reviens de congés, la première chose que je fais est de lire l’historique Git pour voir ce qui a changé entre-temps ; c’est aussi utile quand la CI est cassée, donc en pratique je consulte l’historique sous une forme ou une autre presque tous les jours
      Si la codebase et les fonctionnalités sont très petites, tout squasher peut convenir, mais je préfère pouvoir voir le vrai fil des changements et cherry-picker ou annuler sélectivement des commits
    • Un historique bien organisé, avec un commit par changement logique, est extrêmement utile pour faire une recherche dichotomique des régressions
  • Si je suis actuellement sur D et que je remplace B par B', je comprenais que git rebase --update-refs laisse E, qui est hors de la plage du rebase, tel quel, et ne crée que B' → C' → D', tout en conservant l’ancien B → E
    À l’inverse, est-ce que git history fixup met aussi à jour E en E' ? Si oui, je me demande s’il existe un moyen, avec le git rebase auquel je suis habitué depuis des décennies, de mettre à jour toutes les branches descendantes

    • Le hash d’un commit reflète aussi le hash de son commit parent, donc on ne peut pas laisser E tel quel. Je n’ai pas essayé --update-refs moi-même, mais d’après la documentation, le résultat devrait être le troisième graphe, avec E' également ; le comportement souhaité est donc déjà possible avec git rebase
    • Tu peux consulter https://blog.hot-coffee.dev/en/blog/git_update_refs/