Gitは大丈夫ではない
(billjings.com)- Gitはソースコードの分散リポジトリとしては成功したが、分散ワークフロー処理は後付けの解決策に近く、限界が見えている
- Gitのコミットとブランチは、後続コミット、amendの履歴、rebaseの履歴、破棄された状態を自ら表現できない
- Stacked PRでは後続PRを見つけ、スタックを維持したままrebaseする必要があるが、Gitはこの関係を安定して把握しにくい
- Gitはstaging、unstaged、ファイルシステム、HEADといった変更可能な状態をコミット・ブランチの外側に置いており、学習と利用を複雑にしている
- マージ前のPRを一緒に使う必要がある非同期開発の流れでは、Gitの後ろ向きな不変履歴モデルが反復的な問題を生む
Gitの2つの役割
- Gitはソースコードの分散リポジトリであると同時に、分散ワークフローツールとしても使われている
- ソースコードのリポジトリとしては非常に成功したが、分散ワークフローを扱う方法の大半は後から継ぎ足した解決策に近い
- 非同期開発は、East River Source Controlの表現を借りれば基本条件に近く、タイムゾーンの異なる協業だけでなく、自分自身と時間差を置いて作業するときにも起こる
- jjはGitの限界をより鮮明にするツールであり、Gitで十分だと感じている人はjjを真剣に試していない可能性が高い
Gitの基本モデルが見落とす関係
- Gitの思考の中心にはコミットとブランチがある
- コミットはソースコードと履歴を含む不変オブジェクトである
- ブランチはログが付いた変更可能なポインタである
- 典型的なGitダイアグラムではコミットを
C1、C2、C3のように描き、順序や関係が明確に見えるが、実際のリポジトリにおけるコミット名はハッシュやメッセージに近く、そのような順序関係はシステム内に存在しない - rebase後の
C2とC2’のような表記は人間にとって分かりやすい説明にすぎず、Gitは2つのコミットが対応していることを認識していない - 特定コミットの後続コミットを見つけるには、すべてのブランチをたどってそのコミットへ続く経路上のコミットを探す必要があり、簡単ではない
Gitには「C」がない
- Gitコミットは次の情報を自ら把握できない
-
後続コミット
- amend後の新しいコミットから古いコミットへつながる修正履歴
-
rebase履歴
- そのコミットが破棄されたかどうか
- ブランチにも限界がある
- ブランチには履歴の概念はあるが、コード変更と1:1で対応すると信頼するのは難しい
- ブランチ同士は関係を持たず、たとえば
trunkからwp/bugfixを安定して見つけることはできない trunkからwp/bugfixへ向かう前方参照がないため、到達可能な関係でもない- Gitダイアグラムは人間には順序や対応関係があるように見えるが、実際にツールが提供する能力を誇張して見せることがある
-
Stacked PRが難しい理由
- タイムゾーンの異なる相手と協業し、レビュー前にはマージしないようにするには、CPUのように作業をパイプライン化する必要がある
- 1つのPRを作ってレビュー完了まで待つ代わりに、最初のPRの上に2つ目のPRを作り、さらにその上に次を積んで、複数の逐次PRを同時にレビューに載せる方式がStacked PRである
- GitはStacked PR構造を安定して扱いにくくしている
Fix key entry raceの上にRefactor key entry codeのような後続PRを作り、その後trunkをfetchして更新したら、スタックを維持したままrebaseする必要がある- Gitは後続コミットを認識しないため、
Fix key entry raceからRefactor key entry codeを簡単に見つけられない - コミットが破棄されている可能性もあるため、後続コミットが見えてもそれが最新状態かどうか判断しにくい
- ブランチはPRそのもののように使われるが、この流れでは誤って上書きしやすい
- GraphiteのようなスタッキングツールはGit上でこの作業を実現できるが、Gitのコミットやブランチ自体を強化できるわけではない
- 別個のブランチメタデータリポジトリを作り、Gitと同期する必要がある
- ユーザーがGit自体を直接操作すると、そのリポジトリがGitの状態と食い違う可能性がある
変更可能な状態がコミットの外にある
- Gitの多くの問題は、**変更可能性(mutability)**を直接モデル化しない方式に由来する
- Gitの編集ワークフローには、コミットとブランチの外側に別の状態が存在する
- Stagingまたはindexは作業コピーから作られるソーススナップショットであり、新しいコミットはここから作られる
- Unstagedはindexとファイルシステムの間の差分を表す2つ目のdiffである
- ファイルシステムはcheckoutされた内容を保持し、そこにstagedとunstagedの変更が加わる
- HEADは新しいコミットが作られる位置である
- stashはstagingとunstagedの変更を保存・復元する別のリポジトリのように振る舞う
- checkoutを別のコミットやブランチに切り替えると、Gitはファイルシステムを新しい位置に合わせつつ、stagingまたはunstagedのdiffを保持しようとする
- この過程はコマンドこそ異なるが、矢印の関係だけを見ればstagingを新しい基準の上へ移すrebaseに似た形を取る
すべてをコミットとしてモデル化しにくい理由
- Stagingと作業コピーも明確な祖先を持ちソースコードを含むため、静的な状態だけを見ればコミットのように表現できる
- しかしコミットIDは内容のハッシュであるため、コミットが変更可能ならIDは絶えず変わってしまう
- Stagingと作業コピーが「何であるか」を一貫して指せるようにするには、コミットではなくブランチのように扱う必要があるが、ブランチには前述の限界がある
- この複雑さは実際の問題につながる
- Gitの学習と利用がより難しくなる。同じ概念が両側に別々に存在するためである
- リポジトリ全体の状態がcloneで取得する状態と大きく異なり、エクスポートが不自然になる
- 時間とともに変更セットが変わる非同期フローがうまく機能しない
- 変更可能な側のシステムはmergeを表現できず、実際のワークフローを表せない場合がある
Gitが実際のワークフローを表現できない場合
- 新しい機能ブランチでまだコミットしていないまま開発している途中、デバイス上で開発を妨げるバグを見つけることがある
- そのバグが新機能を妨げるわけではないが開発を煩わしくするなら、作業をstashして新しいブランチへ移動し、再現テストと修正作業を作ってPRを出せる
- その後でもう一度新機能ブランチに戻ると、選択肢は限られる
new-featureを実際には依存関係のないbugfixの上へrebaseしてレビューを進める- 開発中は
new-featureをbugfixの上へrebaseして使い、ブランチを提出する前にそのrebaseを元に戻す
- Gitでは「編集作業空間にはbugfixのすべてのコードと、すでにコミットしたnew featureのコードが一緒に存在しているべきだ」という状態を表現できない
- こうした要求は、未マージのPRとの互換性テストのような、より難しい問題でも同じ構造で現れる
- Jujutsu megamergesのように適切なツールを使えば、複数のPRを並列に保ちながら、編集空間ではそれらを一緒に使うことができる
Gitはもはや十分ではない
- 2000年代初頭のバージョン管理ツールは使いにくく管理もしづらく、品質にもばらつきがあり、Subversionですらつらいものだという認識が広く共有されていた
- 当時はローカルにリポジトリ全体のコピーを持ちたいという要求は一般的ではなく、ローカルブランチを作りたいという要求も普遍的ではなかった
- ファイルロックを不便だと感じる人も多かったが、必要だと考える人もおり、Gitで個別ファイルやディレクトリをロックできるかと尋ねる人もいた
- オープンソースのように分散ワークフローを直接経験していた人々にとって、DVCSは古い傷を防ぐための包帯のように受け止められた
- 今日、意味のある形で分散したワークフローを使う人にとって、Gitの後ろ向きな不変履歴モデルは反復的な問題の源になっている
- Metaのような企業は、ほぼ10年にわたりGitを大きく上回る社内システムを使ってきた
- 「今ではGitはClaudeが代わりに触る」という流れが、こうした代替手段を無意味にするわけではない
- LLMを使うことで、単一マシン内であってもエンジニアは以前より多くの非同期開発を行っているように見える
1件のコメント
Lobste.rsの意見
この記事で提起された問題をjjがどう解決するのかを示してくれていたらよかったかもしれない
jjユーザーには自明だろうが、その人たちがこの記事の主な対象ではない可能性が高い
この記事でGitがよくない根拠として挙げられていた機能は、個人的には必要になったことがない
自分だけそうなのかと思った
ツールで重要な点のひとつは、ツールが動的なシステムの一部だということだ。ツールが可能にすることは「自分ができると信じていること」に影響し、その信念がまたツールの認識やツールの進化の方向を変える
あるツールが既存の状態を揺さぶると、何ができるかについての信念や期待値も一緒に変わる
面白そうではあるが、ダイアグラムを見ると目が回る
2000年代初頭のように今が深刻というわけではなく、Git以前のバージョン管理システムの限界はかなり明確だった、という話については、DarcsはGitより先に登場しており、スナップショットベースのバージョン管理の一部の問題を根本的に解決していた面があった
当初は性能が悪くて広まらなかったが、その後性能は改善され、人々は再確認しに戻ってこなかった。面白いことをしている他のバージョン管理システムもあるのだから、「GitでなければJujutsu」という唯一の選択肢であるかのようには扱わないでほしい。そういう論法はあまりにも頻繁に見る
それもデータモデルの問題だ
jjはこれをどう処理するのか? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png
jj new A Bを使うと、作業コピーコミットが複数の親を持てるので、マージコミットのように動作するそのため作業コピーには両方の親の変更が入ってきて、そのマージの上で作業を続けることも、どちらか一方のコミットにamendする形で進めることもできる
まだGitのほうが好みで、筆者にはバイアスがあるように見える
jj newを実行する必要がある点さえ覚えていれば、gitとjjは混ぜて使えるGitは常に親コミットを指し、現在の
jj commitは作業ツリーの未コミット変更のように見える自分はそうやって
jjを覚えた。リベース処理やツリー移動のようにjjが得意なことにはjjを使い、まだjjに対応するコマンドを知らないものや、git blameのようにGitコマンドが先に思い浮かぶ日常作業には引き続きgitコマンドを使っていた実際に毎日使ってみるまでは、なぜ
jjがより良いのかあまり実感できず、読むだけだったときは「この機能は本当に必要なのか」あるいは「Gitでもすでにできるのに」と思っていたもちろん
jjにも欠点はある。最新の.gitignoreがないと、バイナリファイルが誤ってコミットに入る可能性がある。幸い、非常に大きなファイルを追加しようとするとjjが警告するが、小さなファイルはすり抜けることがあるデバッグ中に現在のディレクトリに追跡ファイルやログファイルがあると、それも入りうるので、ツリーを一通り操作した後はdiffstatをすべて確認するのがよい
特に
jjで二分探索をしていて、.gitignoreを更新したコミットより前のコミットをテストすることになると問題になりうる。二分探索には読み取り専用モードが必要かもしれない