1 ポイント 投稿者 GN⁺ 2 시간 전 | 1件のコメント | WhatsAppで共有
  • 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 コア配布版に含まれる 実験的なコマンド なので、別のツールをインストールせずに試せる
    • Git 2.54 では 4 月に rewordsplit が導入された
    • Git 2.55 では 6 月に fixup が追加された
  • サブコマンドは fixuprewordsplit の 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* が生成され、その上の CD もそれぞれ C*D* として再作成される
    • feat-1feat-2 のブランチ先端も新しいコミットに追従して移動する

原子的な処理と競合の制限

  • 3 つのサブコマンドはいずれも 原子的に処理 され、作業ツリーが半端に壊れた状態で残らない
  • 競合を引き起こしうる操作そのものを拒否する方式で原子性を保証している
  • これは 競合を第一級オブジェクトとして扱い、リベース中の競合状態を保持してあとから解決できる jj と比べると、機能面では制限がある
  • git history の制限事項 は、履歴の書き換えが状態保持型の作業に変わらないよう意図的に設けられている
    • Git が将来的に 第一級の競合 をサポートすれば、この制限を解除できる可能性がある

reword: 作業ツリーを触らずにコミットメッセージを変更

  • git history reword は、過去のコミットメッセージを変更し、その上のコミットとブランチを自動的に再構成する
  • git history reword <commit> を実行すると、エディタに既存メッセージが開き、修正して保存すると後続のコミットスタックが新しいハッシュに合わせて再構築される
  • 反復的な開発の中で設計が変わったとき、以前のコミットメッセージを現在の構成に合わせて修正するのに役立つ
  • ツリー内容ではなくコミットメッセージだけを変える点を除けば、fixup と同じ方式で動作する
  • reword はインデックスや作業ツリーに触れず、コミットグラフだけを変更 する
    • チェックアウトしていないブランチのコミットも、現在の作業を妨げずに修正できる
    • B のメッセージを変更するとハッシュが変わるため、後続の CC* として再作成され、feat-1 も新しいコミットに追従する

split: 1 つのコミットを 2 つに分割

  • git history split は、1 つのコミットの変更を対話的に選択して 2 つのコミットに分割する
  • git history split <commit> を実行すると、対象コミットの変更内容をハンク単位で選ぶプロンプトが開く
    • 選択して残したハンクが 1 つ目のコミットを構成する
    • 残りのハンクは 2 つ目のコミットに入る
  • git add -p に似ているが、git rebase を組み合わせる複雑な手順は不要
  • 互いに無関係な 2 つの変更が入っていた B を分割すると、B1B2 が生成され、後続の C はその 2 つのコミットの上に C* として再構築 される
  • 3 つのコマンドの中では最も用途が特化しているが、実際にコミットを分割する必要があるときには有用

jj との機能差

1件のコメント

 
GN⁺ 2 시간 전
Hacker News の意見
  • Git は使いづらかったが、無料で公開されている Pro Git の本の最初の3章を読んで改善した。Git の内部動作について優れたメンタルモデルを提供しており、良くも悪くも Git のユーザーインターフェースは内部構造を直接反映しているため、それを理解するとすべてが腑に落ちた

    • ユーザーインターフェースが内部構造を直接反映している、という点には同意しない。例えば git checkout は、ブランチを切り替えるときはまだ Git に入っていない変更を失わないが、ファイルの別バージョンを復元するときは変更を戻すように設計されている。これがどう内部構造を直接反映しているというのか?
      それとは別に、内部でやりたいことやコマンドが分かっていても正確なコマンドライン構文を見つけにくくしている CLI の一貫性のなさも、Git を使いにくくしている
    • Git は言われているほど混乱するものではなく、数時間かけて理解すればよい。ソフトウェアエンジニアの必須ツールであるにもかかわらず、それを学ばないのは残念で、プロなら git commit -am “Changes” だけでは不十分だ
    • 自分も Git コマンドは紛らわしいと思っていたが、Linus が Git の内部データ構造を説明する YouTube 動画を見て、とても単純だと分かった。ツールの機能がデータ構造をどう操作するかを簡単に対応付けられたし、むしろ Git が提供しようとしている高レベルの抽象化のほうが状況をより混乱させている
    • そうではない。Git コマンドの半分は低レベルで、残り半分は高レベルなので、内部構造と1対1で対応していない
    • Mercurial のユーザー体験は間違いなく最高で、別途ドキュメントを読まなくても学べた。Git を使うときも Mercurial のように使い、問題が起きたらローカルリポジトリを削除するというやり方で、キャリアを通じて何とかやってきた
  • git rebase --abort があり、リベース前にタグを作っておいて git reset --hard $set_tag で戻ることもできる。以前の状態が消えるわけではないので、対話的リベースを怖がる理由はない

    • まさにその用途で tempbefore-rebase というブランチをたくさん作っている。タグのように使うが、ブランチのほうが動かしやすいので、大きな rebase -i の前にはたいてい git branch before-rebase/some-feature を実行する
      実際に git reset before-rebase が必要になったことはほとんどないが、git log -p before-rebase をリベース後のブランチと比較して、コンフリクト解決過程での意図しない変更を確認することは多かった
    • 別の安全用タグを作る必要すらなく、reflog が自動でその役割を果たしてくれる
    • git reflog を使っている?
    • jj undo より簡単にはなり得ない
    • その通り。git rebase と入力した後、半分くらいはすぐに --abort を入力する
  • 自分をコーダーやプログラマーだとは思っていないが、Git を学ぶことは自分の考え方に整理の超能力を与えてくれたように感じた。ウェブサイト、デザインプロジェクト、電子工作、作曲、個人の知識リポジトリ、リモート管理スクリプト、設定管理、コードスニペットなど非常に多くの用途に活用しており、完璧ではなくても一緒に働く全員に学ぶよう勧めている

    • 自分も整理能力は大きく向上したが、これは Git 自体というよりバージョン管理システム全般の効果に近い。バージョン管理システムは、作ったものを整理・保存し、いつでも必要なだけコピーを取り出せるので、心配せずに作業できるようにしてくれる
      David Allen の『Getting Things Done』には、考えを集めて整理するための信頼できるシステムが必要だと書かれている。自分はタスク・プロジェクト・リストには OmniFocus を使っているが、コンピューター上で作ったソフトウェアや一部のデータを整理・保存することについては Git を信頼している
    • 作曲には 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... で見られる

    • Windows から Linux に移って残念に思ったことの一つは、すべてのプログラムがファイルやフォルダの右クリックメニュー、TortoiseGit の状態アイコンのようなオーバーレイ、My Computer のような特殊オブジェクトを追加できた統合シェルだ。XP で頂点に達し、Vista 以降は Microsoft が拡張しにくい方向に変えたのだと理解している
      ただし、VC++6 のあるメニューが通常の一覧ではなくフォルダのように見えるシェルオブジェクトを作り、ファイルを開くダイアログに表示していた理由はいまでも理解できない。プロジェクトにコンポーネントを追加するウィザードだったが、ダイアログが開いている間、その偽フォルダを My Computer で探したものだ
    • 既存のワークフローが少し面倒で、新しいコマンドのほうが便利なのは確かだ。fixup はすでに git commit --fixup や、それ以前から git rebase -i で可能だったし、rewordgit rebase -i でできた
      各コミットに対して行う操作を決めるためにファイルを編集する過程が不便だというフィードバックが十分に積み重なり、既存機能を包んだ上位レベルのコマンドが追加されたように見える
    • Git のユーザーインターフェースの中では、Magit に近いツールを見たことがない。深い機能と、単純化された状態保持型インターフェースを同時に提供している
      コマンドラインツールでは、次のコマンドを実行すると以前の出力にアクセスしにくくなる。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.1myfeature.2 のように保存しておき、新しいバージョンで挙動が変わったときに古いバージョンを探すのだが、git reflog ではこれを代替できない
    たとえば問題が myfeature.58myfeature.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すればよい

    • 強く反対する。PR をレビューするときは少なくともコミット履歴を読んで変更の流れを把握するし、"oops""fix" だらけなら即座に差し戻す
      コミットは作業のストーリーを伝えるものなので、レビュアーが意図を理解するために余計な苦労をしなくて済むよう、読みやすいコミット履歴を提供すべきだ
    • 私は個々のコミットをよく読む。丁寧に管理されたリポジトリでは、バグやリグレッションを追跡し、作者の変更意図を理解するのに非常に役立つ
    • コードがなぜこうなっているのか気になったとき、そのコードを導入したコミットの文脈を見ると大いに助けになる。異なる変更を一度に詰め込んだ巨大なコミットなら、その価値はずっと下がる
    • 大きなチームで働いていないことが分かる考え方だ。休暇から戻ったらまず Git 履歴を読んで、その間に何が変わったのか確認するし、CI が壊れたときにも有用なので、実質的に毎日何らかの形で履歴を見ている
      コードベースと機能がごく小さいなら全部 squash してもよいだろうが、私は実際の変更の流れを確認し、コミットを選択的にcherry-pick したり revert したりできる形を好む
    • 論理的な変更 1 つにつき 1 コミットとしてよく整理された履歴は、リグレッションを二分探索するときに非常に有用だ
  • 現在 D をチェックアウトしている状態で BB' に変えるとき、git rebase --update-refs は rebase 範囲外の E はそのままにして B' → C' → D' だけを作り、既存の B → E を維持するものだと理解していた
    一方で git history fixupE まで E' に更新するのか? だとすると、何十年も手になじんだ git rebase でもすべての子孫ブランチを更新する方法があるのか気になる

    • コミットハッシュには親コミットのハッシュも反映されるため、E をそのままにすることはできない。--update-refs を直接使ったことはないが、ドキュメントどおりなら E' まで生じる 3 つ目のグラフが結果になるはずなので、望んでいる動作はすでに git rebase で可能だ
    • https://blog.hot-coffee.dev/en/blog/git_update_refs/ を参照すればよい