La commande `git history`
(lalitm.com)- 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 fixupfusionne 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 commitrewordmodifie le message d’un ancien commit, etsplitdivise 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 à lajjdirectement 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 -ipeut laisser l’arbre de travail dans un état incomplet en cours d’opération - L’alternative à Git
jjest 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 historyest une commande expérimentale incluse dans la distribution Git de base, ce qui permet de l’essayer sans installer d’outil supplémentaire- Les sous-commandes se limitent à trois options :
fixup,rewordetsplit
fixup : modifier un ancien commit et les branches dérivées en même temps
git history fixupcorrige 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écutergit history fixup <commit>pour fusionner les changements indexés dans le commit cible - C’est similaire à la combinaison de
git commit --fixupet 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-refsne déplace que les références situées dans la portée du rebase en cours,git historyretrouve 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 nouveauB*avec un hash différent est créé, puisCetDau-dessus sont eux aussi recréés respectivement enC*etD*- Les extrémités des branches
feat-1etfeat-2se déplacent également pour suivre les nouveaux commits
- Les extrémités des branches
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 historysont 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 rewordmodifie 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 rewordne 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 leCsuivant est recréé enC*etfeat-1suit lui aussi le nouveau commit
split : séparer un commit en deux
git history splitpermet 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 combinegit rebase - Si l’on scinde un
Bcontenant deux changements sans rapport,B1etB2sont créés, puis leCsuivant est reconstruit enC*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
git historyne remplace pas l’ensemble du modèle de travail proposé parjj- En revanche,
git historyprend en charge, au sein même de Git, la correction d’anciens commits, la modification de messages, le fractionnement de commits et la mise à jour des branches dérivées, ce qui permet d’introduire une partie des avantages dejjdans le flux de travail actuel - Comme un assouplissement de ces limites reste possible à l’avenir, il existe encore une marge d’amélioration dans les prochaines versions de Git
1 commentaires
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
git checkoutest 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 commit -am “Changes”ne suffit pasIl existe
git rebase --abort, et on peut aussi créer un tag avant le rebase puis revenir en arrière avecgit 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 interactiftempoubefore-rebaseprécisément pour ça. Je m’en sers comme de tags, mais les branches sont plus faciles à déplacer ; avant un grosrebase -i, je lance donc souventgit branch before-rebase/some-featureEn pratique, j’ai rarement eu besoin de
git reset before-rebase, mais il m’est souvent arrivé d’utilisergit log -p before-rebasepour comparer avec la branche après rebase et vérifier les modifications involontaires introduites lors de la résolution des conflitsgit reflog?jj undogit rebase, environ une fois sur deux je tape immédiatement--abortJe 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
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
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 quegit historysigne 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é avecgit history reword HEAD~, toutes les signatures disparaissentComme je veux conserver les signatures de commits, je suis finalement revenu au classique
git rebase -igit historyest une fonctionnalité expérimentale, donc cela vaut la peine de le signaler comme bugMê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...
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.fixupétait déjà possible avecgit commit --fixup, ou auparavant avecgit rebase -i, etrewordpouvait aussi se faire avecgit 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.
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
jjest 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
jjpeut ê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,
jjvaut 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=48903054jjdispose 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,jjrelève encore davantage du concept que de la réalité.J’aime
jjet 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 dejj, il deviendra difficile de trouver une raison d’installerjjséparément.La façon dont
git history fixuprebase automatiquement toutes les branches descendantes ne correspond pas à mon workflow. Je conserve chaque version d’une branche de fonctionnalité sous des noms commemyfeature.1,myfeature.2, afin de retrouver une ancienne version lorsque le comportement change dans une nouvelle version ;git reflogne peut pas remplacer cela.Par exemple, si le problème est apparu entre
myfeature.58etmyfeature.59et que chaque fonctionnalité compte 15 commits,git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59permet 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 --interactivesous forme de commandes séparées avec moins de friction, et elles ne semblent fonctionner qu’en l’absence de conflits.git historyest 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 degit historyle 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
"oops"et de"fix", je la rejette immédiatementLes 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
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
Si je suis actuellement sur
Det que je remplaceBparB', je comprenais quegit rebase --update-refslaisseE, qui est hors de la plage du rebase, tel quel, et ne crée queB' → C' → D', tout en conservant l’ancienB → EÀ l’inverse, est-ce que
git history fixupmet aussi à jourEenE'? Si oui, je me demande s’il existe un moyen, avec legit rebaseauquel je suis habitué depuis des décennies, de mettre à jour toutes les branches descendantesEtel quel. Je n’ai pas essayé--update-refsmoi-même, mais d’après la documentation, le résultat devrait être le troisième graphe, avecE'également ; le comportement souhaité est donc déjà possible avecgit rebase