git history コマンド
(lalitm.com)- Git 2.54 と 2.55 にかけて導入された実験的な
git historyコマンドは、既存の Git ワークフローを維持しつつ、過去のコミット修正と派生ブランチの再構成を自動化する fixupはステージした変更を過去のコミットに統合した後、そのコミットから派生したすべてのローカルブランチを新しいコミットハッシュに合わせて自動的にリベースするrewordは過去のコミットメッセージを変更し、splitはコミットの変更内容をハンク単位で分割して 2 つのコミットに分けたうえで、後続コミットとブランチを再構成する- 3 つのサブコマンドは、競合の可能性がある操作を拒否する 原子的な処理 によって、作業ツリーが途中で壊れた状態になるのを防ぐが、マージコミットを含む履歴では動作しない
jjの第一級の競合処理、操作ログベースの取り消し、作業コピーのコミットモデリングまでは提供しないが、別途インストールなしで Git コア配布版から複数のjj風の履歴編集機能 を利用できる
従来の Git 履歴編集が扱いづらい理由
- 複数の変更を並行して扱うと、ブランチとコミットを行き来し続ける必要があり、
rebase -iの途中で作業ツリーが不完全な状態に陥ることがある - Git の代替である
jjはこうした問題の解決策としてよく挙げられるが、1 年半のあいだ 3 か月ごとに数日ずつ試しても、結局は従来の Git ワークフローに戻ってしまった git historyは Git コア配布版に含まれる 実験的なコマンド なので、別のツールをインストールせずに試せる- サブコマンドは
fixup、reword、splitの 3 種類で構成される
fixup: 過去のコミットと派生ブランチを同時に修正
git history fixupは、過去のコミットの誤りを修正し、そのコミットから派生したブランチを自動的にリベースする- 一般的な修正手順と同じように
git addで変更をステージしたあと、git history fixup <commit>を実行すると、ステージ済みの変更が対象コミットに統合される git commit --fixupと autosquash rebase を組み合わせたものに近いが、対象コミットを含む ほかのブランチまで更新 する点が異なるgit rebase --update-refsが現在のリベース範囲内の参照だけを移動するのに対し、git historyは対象コミットから派生した すべてのローカルブランチ を見つけて再書き込みする- 必要であれば、更新範囲を現在のブランチだけに限定できる
- マージコミットを含む履歴では動作しないため、一部の Git の使い方には適用しにくい
Bに修正を統合すると、新しいハッシュを持つB*が生成され、その上のCとDもそれぞれC*、D*として再作成されるfeat-1とfeat-2のブランチ先端も新しいコミットに追従して移動する
原子的な処理と競合の制限
- 3 つのサブコマンドはいずれも 原子的に処理 され、作業ツリーが半端に壊れた状態で残らない
- 競合を引き起こしうる操作そのものを拒否する方式で原子性を保証している
- これは 競合を第一級オブジェクトとして扱い、リベース中の競合状態を保持してあとから解決できる
jjと比べると、機能面では制限がある git historyの制限事項 は、履歴の書き換えが状態保持型の作業に変わらないよう意図的に設けられている- Git が将来的に 第一級の競合 をサポートすれば、この制限を解除できる可能性がある
reword: 作業ツリーを触らずにコミットメッセージを変更
git history rewordは、過去のコミットメッセージを変更し、その上のコミットとブランチを自動的に再構成するgit history reword <commit>を実行すると、エディタに既存メッセージが開き、修正して保存すると後続のコミットスタックが新しいハッシュに合わせて再構築される- 反復的な開発の中で設計が変わったとき、以前のコミットメッセージを現在の構成に合わせて修正するのに役立つ
- ツリー内容ではなくコミットメッセージだけを変える点を除けば、
fixupと同じ方式で動作する rewordはインデックスや作業ツリーに触れず、コミットグラフだけを変更 する- チェックアウトしていないブランチのコミットも、現在の作業を妨げずに修正できる
Bのメッセージを変更するとハッシュが変わるため、後続のCはC*として再作成され、feat-1も新しいコミットに追従する
split: 1 つのコミットを 2 つに分割
git history splitは、1 つのコミットの変更を対話的に選択して 2 つのコミットに分割するgit history split <commit>を実行すると、対象コミットの変更内容をハンク単位で選ぶプロンプトが開く- 選択して残したハンクが 1 つ目のコミットを構成する
- 残りのハンクは 2 つ目のコミットに入る
git add -pに似ているが、git rebaseを組み合わせる複雑な手順は不要- 互いに無関係な 2 つの変更が入っていた
Bを分割すると、B1とB2が生成され、後続のCはその 2 つのコミットの上にC*として再構築 される - 3 つのコマンドの中では最も用途が特化しているが、実際にコミットを分割する必要があるときには有用
jj との機能差
git historyはjjが提供する作業モデル全体を置き換えるものではないjjは 操作ログと簡単な取り消し を提供する- 作業コピーをコミットとしてモデリング する
- リベース過程で競合状態を保持 できる
- 一方で
git historyは、既存の Git の中で過去コミットの修正、メッセージ変更、コミット分割と派生ブランチの更新をサポートし、jjの一部の利点 を現在のワークフローに取り込める - 今後、制限が緩和される可能性もあり、以後の Git リリースでさらに改善される余地がある
1件のコメント
Hacker News の意見
Git は使いづらかったが、無料で公開されている Pro Git の本の最初の3章を読んで改善した。Git の内部動作について優れたメンタルモデルを提供しており、良くも悪くも Git のユーザーインターフェースは内部構造を直接反映しているため、それを理解するとすべてが腑に落ちた
git checkoutは、ブランチを切り替えるときはまだ Git に入っていない変更を失わないが、ファイルの別バージョンを復元するときは変更を戻すように設計されている。これがどう内部構造を直接反映しているというのか?それとは別に、内部でやりたいことやコマンドが分かっていても正確なコマンドライン構文を見つけにくくしている CLI の一貫性のなさも、Git を使いにくくしている
git commit -am “Changes”だけでは不十分だgit rebase --abortがあり、リベース前にタグを作っておいてgit reset --hard $set_tagで戻ることもできる。以前の状態が消えるわけではないので、対話的リベースを怖がる理由はないtempやbefore-rebaseというブランチをたくさん作っている。タグのように使うが、ブランチのほうが動かしやすいので、大きなrebase -iの前にはたいていgit branch before-rebase/some-featureを実行する実際に
git reset before-rebaseが必要になったことはほとんどないが、git log -p before-rebaseをリベース後のブランチと比較して、コンフリクト解決過程での意図しない変更を確認することは多かったgit reflogを使っている?jj undoより簡単にはなり得ないgit rebaseと入力した後、半分くらいはすぐに--abortを入力する自分をコーダーやプログラマーだとは思っていないが、Git を学ぶことは自分の考え方に整理の超能力を与えてくれたように感じた。ウェブサイト、デザインプロジェクト、電子工作、作曲、個人の知識リポジトリ、リモート管理スクリプト、設定管理、コードスニペットなど非常に多くの用途に活用しており、完璧ではなくても一緒に働く全員に学ぶよう勧めている
David Allen の『Getting Things Done』には、考えを集めて整理するための信頼できるシステムが必要だと書かれている。自分はタスク・プロジェクト・リストには OmniFocus を使っているが、コンピューター上で作ったソフトウェアや一部のデータを整理・保存することについては Git を信頼している
これらのコマンドは理論的にはとても気に入っており、
jjの同等コマンドも常に使っているが、git historyが修正したコミットに署名する方法を見つけられなかった。マニュアルと Git のソースコードを見ても、既存の GPG 署名付きコミットをgit history reword HEAD~で修正すると署名がすべて消えてしまうコミット署名を維持したかったので、結局従来の
git rebase -iに戻ったgit historyは実験的機能だとしているので、バグとして報告する価値がある新しいコマンドがなくても、10年以上にわたってローカルの Git コミット履歴を大きな苦労なく大幅に修正してきた。Git とリベースが難しく感じられる大きな理由は、正確なコマンドラインオプションと挙動を自分で把握しなければならないというユーザーインターフェースの問題だと確信している
Windows では TortoiseGit のリベースダイアログが強力でありながら使いやすかった。コミットログで基準コミットを右クリックして
"Rebase onto this"を選べばよく、https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm... のように、コミットの順序変更・分割・結合・編集が明確で、途中でキャンセルすれば元の状態に戻るただしシェル拡張方式なのでクリックが多すぎ、タブのある独立アプリならもっとよいと思うが、これは TortoiseSVN を引き継いだ設計原則に見える。Git Extensions も少し使ってみたが、よく使っていた機能を見つけられなかったか、すでに TortoiseGit に慣れていて定着しなかった。ログ画面は https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht... で見られる
My Computerのような特殊オブジェクトを追加できた統合シェルだ。XP で頂点に達し、Vista 以降は Microsoft が拡張しにくい方向に変えたのだと理解しているただし、VC++6 のあるメニューが通常の一覧ではなくフォルダのように見えるシェルオブジェクトを作り、ファイルを開くダイアログに表示していた理由はいまでも理解できない。プロジェクトにコンポーネントを追加するウィザードだったが、ダイアログが開いている間、その偽フォルダを
My Computerで探したものだfixupはすでにgit commit --fixupや、それ以前からgit rebase -iで可能だったし、rewordもgit rebase -iでできた各コミットに対して行う操作を決めるためにファイルを編集する過程が不便だというフィードバックが十分に積み重なり、既存機能を包んだ上位レベルのコマンドが追加されたように見える
コマンドラインツールでは、次のコマンドを実行すると以前の出力にアクセスしにくくなる。
git logで見たコミットをチェリーピックしようとしても、ハッシュが端末に残っていなければ記憶だけで入力することはできないが、状態保持型インターフェースは複数の相互作用を並行して維持し、以前の情報を保存してくれるGit には現在のブランチ、直前のブランチ、変更されたファイル、プッシュしていないコミットなど、覚えておくべき状態情報が非常に多い。シェルプラグインのブランチ表示と補完、複数タブやウィンドウ分割、タブ間コピー、補完検索といった周辺ツールに慣れていないと、Git の利用はかなり不快になり得る
jjの実質的な弱点はGit 連携だけだと思う。ファイル集合、リビジョン集合、テンプレートのためのドメイン固有言語を定式化し、設定と CLI 全体で一貫して使い、内部構造を直接さらすことなく強力な概念をインターフェースとして提供しているしかし柔軟すぎるため、Git との相互運用性は慣例と規律に大きく依存し、ほぼすべての
jjの状態を Git で表現できるので、リモートリポジトリを壊しやすい。これを復旧するには Git を深く理解している必要がある新しい単独開発プロジェクトなら
jjを試す価値はある。有利な条件で見ると、Subversion と比較した初期の Git のような感覚で、まずjj自体のやり方に慣れておけば、その後の険しい Git 連携にも少しは備えられる。関連する文脈は https://news.ycombinator.com/item?id=48903054 にあるjjには Git ではないバックエンドもあるが、現時点で運用環境に適していると明示されているのはGit バックエンドだけだ。Git 連携のないjjは、まだ現実というより概念に近いjjとそのバージョン管理のメンタルモデルが好きなので使い続けるつもりだが、ときどき作業を終わらせるために Git へ降りなければならず、新しいバージョン管理システムというよりはGit の上にある便利なラッパーのように感じる。Git がjjのワークフローを支援するコマンドを追加すれば、jjを別途インストールする理由を見つけにくくなるだろうgit history fixupがすべての子孫ブランチを自動でリベースする方式は、自分のワークフローには合わない。機能ブランチの各バージョンをmyfeature.1、myfeature.2のように保存しておき、新しいバージョンで挙動が変わったときに古いバージョンを探すのだが、git reflogではこれを代替できないたとえば問題が
myfeature.58とmyfeature.59の間で発生し、機能ごとにコミットが 15 個あるなら、git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59によって、ベースコミットが異なっていても 2 つのバージョンの差分をコミットごとに見られる。したがって、特定のコミットを含むすべてのブランチの自動リベースは望んでおらず、古いバージョンにタグを使う方式もうまく合わなかった最新の Git は、よく使われる
git rebase --interactiveの操作 3 つを、摩擦の少ない個別コマンドとして実装しており、衝突がない場合にだけ動作するようだgit historyが本当に有用かどうかは疑問だ。私は対話型リベースでコミットを視覚的に移動するほうを好むし、現在のローカルブランチから消えた孤立コミットのハッシュを、誤ってgit historyの対象に指定したくないGit の履歴を完璧に整えることに、なぜそこまで労力をかけるのか理解できない。どうせ個々のコミットを読み返さないのだから、マージ前に全部 squashすればよい
"oops"や"fix"だらけなら即座に差し戻すコミットは作業のストーリーを伝えるものなので、レビュアーが意図を理解するために余計な苦労をしなくて済むよう、読みやすいコミット履歴を提供すべきだ
コードベースと機能がごく小さいなら全部 squash してもよいだろうが、私は実際の変更の流れを確認し、コミットを選択的にcherry-pick したり revert したりできる形を好む
現在
Dをチェックアウトしている状態でBをB'に変えるとき、git rebase --update-refsは rebase 範囲外のEはそのままにしてB' → C' → D'だけを作り、既存のB → Eを維持するものだと理解していた一方で
git history fixupはEまでE'に更新するのか? だとすると、何十年も手になじんだgit rebaseでもすべての子孫ブランチを更新する方法があるのか気になるEをそのままにすることはできない。--update-refsを直接使ったことはないが、ドキュメントどおりならE'まで生じる 3 つ目のグラフが結果になるはずなので、望んでいる動作はすでにgit rebaseで可能だ