紛らわしいGit用語
(jvns.ca)- Gitは長く使っていても混乱しやすい用語の重複と再利用が多く、コマンドを覚えるだけでは実際の動作を予測しにくい
HEAD、heads、detached HEAD、ours/theirs、../...のように、同じ単語や記号が文脈によって異なる意味に変わるindex、staged、cachedはいずれも.git/indexに関連するが、reset、revert、restoreは似た名前とは裏腹に、コミット履歴・ワークツリー・ブランチポインタへ与える影響が異なるmainがリモートをtrackすることと、origin/mainのようなremote-tracking branchは別物であり、checkoutはブランチの切り替えとファイルの復元を1つのコマンドにまとめている- Gitを理解するには、個々のコマンドよりも、ブランチ、参照、コミット、インデックス、リモート追跡状態が噛み合う内部モデルをあわせて見る必要がある
HEAD、ブランチ、detached HEAD
- Gitにおけるheadsはブランチを指し、内部的にはブランチは
.git/refs/headsディレクトリに保存される- 公式Git用語集ではbranchをその上にあるすべてのコミット、headを最新コミットとして区別しているが、同じ対象を見る2つの方法と捉えられる
HEADは現在のブランチを指し、.git/HEADに保存される- 「
headはブランチで、HEADは現在のブランチ」という表現自体が、Git用語の混乱をよく示している
- 「
- detached HEAD stateは、
HEADがブランチではなくコミットIDを直接指している状態- タグをcheckoutすると、タグはブランチではないためdetached HEAD状態になることがある
- この状態では
git pullは動作せず、git pushも特別な方法でない限り動作しない git commit、git merge、git rebase、git cherry-pickは可能だが、新しいコミットがどのブランチにもつながらず、見つけにくくなることがある- 新しいブランチを作るか既存ブランチへ切り替えると、detached HEAD状態から抜けられる
mergeとrebaseで変わるoursとtheirs
- コンフリクト解決で
git checkout --ours file.txtは「ours」側のファイルを選ぶが、mergeとrebaseの基準点は互いに異なる - mergeでは現在のブランチが
ours、マージされて入ってくるブランチがtheirs- 例:
git checkout merge-into-oursの後にgit merge from-theirsを実行すると、現在のブランチがoursになる
- 例:
- rebaseでは現在のブランチが
theirs、rebase対象ブランチがours- 例:
git checkout theirsの後にgit rebase oursを実行すると、対象ブランチがoursになる
- 例:
- この違いは、
git rebase mainが内部的には現在ブランチのコミット群をmainブランチのコピーへ繰り返しマージする方式と関係している- 関連する例は
git rebaseをgit mergeで実装したシェルスクリプトにある
- 関連する例は
- ourstheirsは、
oursとtheirsという用語を短く説明している- VSCodeの「current change」と「incoming change」も同じように混乱しやすい
リモート状態メッセージとfast-forward
Your branch is up to date with 'origin/main'は、現在のmainがリモートの最新状態であるという意味ではない- 最後に
git fetchまたはgit pullした時点のorigin/mainとローカルブランチが同じ、という意味 - 最後のfetchが5日前なら、「5日前の時点で」最新状態ということになる
- 最後に
- Gitはreflogに直近のfetch時点の情報を保存するが、状態メッセージには「最後のfetch基準」の時刻は表示されない
can be fast-forwardedは、リモートブランチがローカルブランチより先に進んでおり、ローカルに別コミットがないためポインタだけを前へ進められるという意味- 例:
mainがA-B-C、origin/mainがA-B-C-D-Eなら、git pull後はどちらもA-B-C-D-Eになる - この場合、マージコンフリクトの可能性はない
- 例:
- fast-forwardできない場合は、両方のブランチが分岐している状態
- 例:
mainにorigin/mainにはないXコミットがあり、origin/mainにはmainにないD、Eがあるなら、fast-forwardはできない - このときGitは、2つのブランチがdiverged状態で、それぞれ異なる数のコミットを持つと表示する
- 例:
コミット選択構文: ^、~、..、...
HEAD^とHEAD~はいずれも1つ前のコミットを指すHEAD^^^、HEAD~~~、HEAD~3はいずれも3つ前のコミットを指す
- merge commitのように親が複数あるコミットでは、
HEAD^2、HEAD^3の意味が変わるHEAD^は1番目の親HEAD^2は2番目の親HEAD^3は3番目の親HEAD~3は親の親の親を意味するため、HEAD^3とは異なる
git log main..testとgit log main...testも互いに異なる動作をするmain..testはtestにだけあるコミットを表示するtest..mainはmainにだけあるコミットを表示するmain...testは両側のどちらか一方にだけあるコミットをまとめて表示する
git diffでは、..と...はgit logとは異なって解釈されるgit diff test..mainはtest側の変更とmain側の変更の両方を含めて比較するgit diff test...mainは共通祖先とmain側の先端を比較し、片側のdiffだけを表示する
- ドット表記の混乱についてはpain in dotsでも追加で扱っている
reference、refspec、tree-ish
- Gitでreferenceは複数の意味で使われる
main、v0.2のようなブランチやタグ- 現在のブランチを指す
HEAD HEAD^^^のようにコミットIDとして解釈される表現
- Git文書では
HEAD^^^のような表現を厳密には「revision parameters」と呼ぶが、日常的にはreferenceのように扱われることがある - symbolic referenceは、
HEADのように別の参照を指す参照を意味するHEADはGitの中核コマンドの動作に大きく影響する中心概念
.git/configのremote設定にはrefspecが入る- 例:
+refs/heads/main:refs/remotes/origin/main - デフォルトの
git cloneやgit remote addが作る値をそのまま使う場合が多い
- 例:
tree-ishは、git checkout THING .でTHINGになり得る対象を指す- コミットID
main、HEAD^^、v0.3.2のようなコミット参照main:./docsのようなコミット内のサブディレクトリ- 実務上は「コミットまたはコミット参照」程度に理解してよい場合が多い
index、staged、cached
index、staged、cachedはいずれも**.git/index**に関連する.git/indexは、git addで変更をstageするときに使われるファイル
- 同じ対象を指す表現が複数のコマンドに散らばっている
git diff --cachedgit rm --cachedgit diff --staged.git/index
- ただし、
--indexと--cachedフラグが常に同じ意味とは限らない- GitのリードメンテナーであるJunio Hamanoの記事が細かな違いを説明している
- indexは性能上の理由でuntracked filesも列挙するが、一般にstaging areaがuntracked filesを含むとは見なされない
reset、revert、restore
reset、revert、restoreは似た単語だが、Gitで影響を与える対象は異なるgit revert COMMITは、現在のブランチにCOMMITの反対の処理を行う新しいコミットを作るCOMMITが3行を追加していたなら、新しいコミットはその3行を削除する
git reset --hard COMMITは、現在のブランチをCOMMIT時点へ強制的に戻すCOMMIT以降の新しい変更を消す、非常に危険な操作
git restore --source=COMMIT PATHは、PATHのファイル群をCOMMIT当時の状態へ戻す- 他のファイルやコミット履歴は変更しない
git reset --hardとgit restore .は単独で使うと概ね似た動作をするが、git reset --hard COMMITとgit restore --source COMMIT .はまったく異なる動作をする
track、remote-tracking branch、untracked files
- Gitはtrackという単語を3通りに使う
git statusのUntracked files:は、Gitが管理しておらずコミットに含まれないファイルを意味するorigin/mainのようなremote-tracking branchは、最後にgit pullまたはgit fetchした時点でリモートoriginのmainが指していたコミットIDを持つローカル参照- “branch foo set up to track remote branch bar from origin”のように、ローカルブランチがリモートブランチをtrackするよう設定されることがある
mainがリモートをtrackするブランチだという話と、origin/mainがremote-tracking branchだという話は別物mainは実際のブランチなので、コミットを作ったりmergeしたりできる.git/configでリモートmainをtrackするよう設定されると、git pullとgit pushを便利に使える
origin/mainは「remote-tracking branch」という名前でも通常のブランチではない- 直接コミットすることはできない
git pullまたはgit fetchでリモートの最新main状態を取得するときだけ更新される
checkout、reflog、merge/rebase/cherry-pick
git checkoutは互いに異なる2つの作業を1つのコマンドに含んでいるgit checkout BRANCHはブランチを切り替えるgit checkout file.txtはfile.txtのunstaged変更を捨てる
- Gitはこの混乱を減らすため機能を
git switchとgit restoreに分けたが、既存のcheckoutも引き続き使える reflogはreference logであり、referenceが過去に何を指していたかの記録- referenceはブランチ、タグ、
HEADを含む包括的な用語 - reflogは、重要なブランチを誤って削除した場合のような非常にまずいGit状況から抜け出す助けになることがある
- referenceはブランチ、タグ、
- merge、rebase、cherry-pickはコミットを統合する方式が異なる
- mergeは2つのブランチを統合する新しいコミットを1つ作る
- rebaseは現在ブランチのコミットを対象ブランチの上へ1つずつコピーする
- cherry-pickはrebaseに似ているが構文が異なり、現在ブランチへコミットをコピーする
git rebase --onto main otherbranch mybranchは、mybranchの一部コミットだけをmain上へ移す形で使える- 例示構造で
F、Gだけをmain上へrebaseしたいときに使える - 3つのブランチ名が入るため、覚えにくい構文
- 例示構造で
commitとその他の紛らわしい用語
commitはGitで動詞としても名詞としても使われる- 動詞: 「頻繁にcommitせよ」
- 名詞:
mainの最新commit
- Git commitは3つの方法で見ることができる
- すべてのファイルの現在状態のスナップショット
- 親コミットとの差分であるdiff
- それ以前のすべてのコミットのhistory
- コマンドごとにcommitの扱い方は異なる
git showはcommitをdiffのように扱うgit logはcommitをhistoryのように扱うgit restoreはcommitをsnapshotのように扱う
blob、tree、origin、upstream、downstream、fetchとpullの関係、stash、worktree、subtree、submoduleも紛らわしいGit用語に含まれる- GitHub用語では、「pull request」、「squash and merge」、「rebase and merge」が混乱しやすい例に挙げられる
繰り返される混乱の3つの軸
- 特に紛らわしいGit用語は3つに圧縮できる
headはブランチで、HEADは現在のブランチ- “remote tracking branch”と“branch that tracks a remote”は別物
index、staged、cachedはいずれも同じ対象を指す
- 2012年のthe most confusing git terminologyも、Git用語がCVSやSubversionの用語とどうつながるかを扱っている
- Git用語の混乱は一部の特殊機能ではなく、中核機能全般で繰り返される
- 特にブランチを論じるとき、「tracking」が複数の方法で使われるため、初見では違いに気づきにくい
- 一部の説明にはGitのまれな隅の機能が含まれており、誤りがあるかもしれないという但し書きが残されている
1件のコメント
Hacker Newsのコメント
ブランチ名、
HEAD、タグはすべてポインタである。HEADは今見ているコミットを指し、my-featureのようなブランチ名はそのブランチの最新コミットを指す。mainでgit checkout -b my-featureを実行すると、main、my-feature、HEADがすべてmainの最新コミットを指すようになるmy-branchでコミットするたびに、HEADとmy-branchは新しいコミットを指すように移動する。detached HEADとは、HEADが指すコミットをどのブランチも指していない状態を意味する。タグとブランチの違いは、タグは特定のコミットを指して動かない点であるもう一つ混乱したのは、多くのコマンドが一貫していないように見える理由が、Gitがデフォルト引数を仮定するためだということだ。
git checkout file.txtはgit checkout HEAD -- file.txtと同じで、my-branchにいるときのgit rebase mainはgit rebase main my-branchと同じである。ただし後者の形式は別のブランチからでも実行できる最後に、すべてがこじれたとき、どんな変な状態からでも抜け出す助けになるコマンドは
git reflogである。HEADが指していたすべてのコミットを表示してくれる。それでもGitがこうした混乱を認め、より意外性が少なく、より単純なインターフェースのコマンドを作っているのは喜ばしいreflogという名前になっているまた、Gitを使う多くの人があまり思い浮かべない点として、取得してきたリモートのブランチやタグのrefもすべてrefなので、ref名を使える場所ならどこでも参照できるということがある
例えば「このブランチに何があろうと構わない。fast-forwardもマージもrebaseもせず、ただローカルブランチをリモートの内容で上書きせよ」と言いたいなら、こうすればよい:
git checkout foo,git reset --hard origin/fooHEADポインタがコミットを直接指すのではなく、通常はmy-branchのようなブランチポインタを指し、そのブランチポインタがコミットを指す。detached HEAD状態は例外であるmy-branchでコミットするとき、HEADとmy-branchの両方が新しいコミットへ移動するのではなく、HEADは引き続きmy-branchポインタを指し、my-branchだけが新しいコミットを指すように移動する。もちろんHEADをたどってmy-branchを経由しコミットへ行けば、新しいコミットに到達するdetached HEADとは、
HEADがブランチポインタではなくコミットを直接指している状態である。detached HEAD状態でも、HEADとブランチポインタの両方が最新コミットを指すことはある。git log --decorateを見ると、通常の(HEAD -> my-branch)の代わりに、最新コミットに(HEAD, my-branch)のように表示される一方、注釈付きタグはメッセージ、タイムスタンプ、SHAなどを含められる。これらはれっきとしたGitオブジェクトで、コミットオブジェクトとかなり似た振る舞いをするが、通常はやはり別のGitオブジェクト、多くの場合コミットを参照する
git rev-parse HEADを実行して、その文字列を安全な場所にコピーしておくのもよい最後に正常だと確信していた状態を正確に印しておいたという安心感がある。混沌の中へ何段階か進んだ地点ではなく、最後の正常な状態へたいてい戻れる。もちろん
git gcやgit filter-branchのようなものを不用意に使っていないことが前提である冗談のようで冗談ではないが、Gitを使うには本当にコンピュータサイエンスの学位が必要だという話は間違ってはいない。Gitのデフォルト引数は非常に一貫性のないユーザー体験を生むという点には同意する
git checkout .、git reset --hard/soft、git cleanupはいずれもものすごく似て見えるが、実際には非常に異なる。それでもPerforceやSVNよりはやはりGitのほうがよい。昔はMercurialのほうを少し好んでいたが、今でもそうかは分からないgit reflogこそ王であり、真実を示してくれるLinus Torvaldsが、人々がより親切なレイヤーを重ねずにGitを直接使い始めたことに驚いた、という文章を読んだ記憶がある。業界に自己反省ができるなら、Gitを選んだのは間違いだったと認め、別のバージョン管理システムへ移行していただろう。テキスト保存がすべてのツールに膨大な時間を浪費せずに済んだはずだ
もちろん、もっと良くはなり得る。それでもGitが受ける憎悪にはいつも驚かされる。Gitはすごいツールであり、非開発者向けに作られたWordやGoogle Docsのようなツールよりはるかに先を行っている
私がSubversionが王だった時代を覚えているほど年を取っているからかもしれない
毎週、同僚たちがGitで混乱しているのを見て、オンラインで助けを求める投稿を見て、ここでの記事のように単純であるべきことを改めて説明しようとする文章をまた見る
コーディングしている間に、SVN、TFVC、Perforceなどで自分が作っためちゃくちゃな状態を元に戻すために何時間も無駄にしている人は思い出せない
ツールは人生を楽にするために存在する。それができないなら、時間をかける価値はない
いまでも、そういう多様性があってほしいと思う。Git の単一文化は、Git を置き換えようとするものがまず Git を再実装しなければならない、という意味でもある。だから ASCII や Scroll Lock キーのように、ほぼ永遠に Git に縛られることになる
核心的な懸念は GitHub があまりに優れているため、GitHub が Git を文字管理エンジンから分離し、他のエンジンも許可しない限り、私たちは Git に縛られ続けるという点
https://trolololo.xyz/github
https://news.ycombinator.com/item?id=38098109
漏れのある抽象化のような用語だ
cacheと呼んでいて、最もコンピュータサイエンス的な用語だったその後 Junio が、分散バージョン管理システムにより特化した
indexに変えたようで、多くのユーザーはこれを staging area と呼んだ。いまではこの用語の進化が Git の UI と内部に化石のように残っているpullしているorigin/masterは origin にある master ではなく、最後に把握していたorigin/masterが指していたコミットを意味する。この値はfetchすると更新され、pullは自動でfetchするls -l .git/refs/remotes/origin/masterを見れば、origin/masterが自分のシステム上にあるファイルにすぎないことが分かる。いつ変わったかも見られる。魔法のように更新されるわけではないgit fetch originを実行して変更があれば、タイムスタンプと内容が変わる。cat .git/refs/remotes/origin/masterで確認できるGit の基礎は非常に単純で、中心的なデータ構造といくつかの操作は 1 日で実装できる。これを知っておくのは本当に価値がある。どういうわけか Git は、かなり単純な中核の上に非常に複雑なユーザーインターフェースを作り上げてしまった
masterブランチを消してしまう。たいていmasterには push できないし、そもそもmasterで直接作業したくない。なら、ローカルに変更可能なmasterブランチを置いておく理由がないだから
origin/masterだけ更新すればよく、それにはfetchで十分だgit fetch && git pull3 年後に引退して、この壊れたゴミから足を洗える日を待っている
git statusの出力をパースするプロジェクトがあるのだが、--porcelainフラグを付けると本当に役立った。より簡潔で、プログラムからパースしやすい出力を生成してくれるどれほど多くのコマンドに、より機械可読な出力があるのか気になり、git-scm の “Git Internals - Plumbing and Porcelain” ページまで見ることになった。要約すると、Git は最初から洗練されたバージョン管理システムというより、バージョン管理を扱うツール群として書かれていた
初期から Git を使っていた多くの人は、この低レベルコマンドでバージョン管理作業を学び、そのワークフローを他の人にも広めた。これが plumbing 層だ。その後 Git はより磨かれた層を作り、それを porcelain と呼んでいる
まだどのコマンドがどちらの層に属するのかは完全には分かっていないが、ここ数年で推奨されるようになった新しいワークフローを理解する助けになった。自分のワークフローを変えるときにも、よりよく判断できるようになった
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
残念ながら、高速化のためにその UI をハードコードしたが、改善はしなかった
振り返ると、始めた当初はものすごく混乱していたはずなのに、格好つけるためにそうではないふりをしていた気がする
https://news.ycombinator.com/item?id=28143078
“Your branch is up to date with ‘origin/main’” は、実は少し誤解を招く表現です。
mainブランチが最新だという意味に受け取れますが、実際にはそうではありません。ただし、ここにあまり「厳密に言えば」を付け加える必要はありません。二人の将軍問題が当てはまるのではないでしょうか。本当に細かく突き詰めると、まさにこの瞬間に「最新」かどうかは分かりません。
statusの前にfetchしても同じです。では、合理的な期待値は何でしょうか。自分のコンピュータ上のオブジェクトデータベース内にある ref が、最後に確認した時点で、遠くにある別のオブジェクトデータベースの ref と同じだった、という程度です。
単純な
status呼び出しがネットワーク越しのfetchをしたり、今使っているツールの基本原理を毎回思い出させたりすることはできません。個人的にはそう思います。ところが驚くことに、StackOverflow の回答に付いた多くのコメントは、まさにその両方が必要だと考えています。“is up to date with the origin’s main as of your last fetch 5 days ago” のような文言なら、もう少し合理的です。最後に
fetchしてからどれくらい経ったかだけを知らせるものだからです。[1] https://stackoverflow.com/questions/27828404/why-does-git-st...
もう一つ。
ORIG_HEADはFETCH_HEADのように「origin の head」と関係があるのだと思っていました。しかし違いました。この「pseudoref」は rebase のような、より複雑な書き換えの前に保存地点として使われるもののようで、reflog ができる前に実装されたものです。おそらく “original head” という意味なのでしょう。Git にはかなり好意的ですが、あのメッセージの文言については筆者に同意します。ただし、提案されている代案には同意しません。
“Your branch is up to date with ‘origin/main’” は技術的には正しいものの、up to date という表現はざっと読むと二つのことを示唆します。
mainがorigin/mainと同じであり、origin/mainも最新である、つまりコマンド中に更新されたか、自動的に最新状態に保たれていて直近の同期に成功している、という印象です。これは CPU の 1 サイクルの間だけ真でなければならない機械判読用の信号ではなく、ユーザーに見える状態メッセージです。ネットワーク理論まで持ち出さなくても、そのような解釈は合理的です。
“Up to date” は、空間的というより時間的に追いついているという意味に近いです。ローカルブランチの位置は「このブランチがどこを指しているか」という空間に近く、リモート ref の状態は「自分が最後にリモート ref をいつ更新したか」という時間に近いものです。もちろん非常に主観的で、どちらも両方の意味になり得ます。
より良い文言としては、“Your branch matches origin/main”、“Your branch’s head is the same as origin/main”、“Your branch is pointing to the same commit as origin/main” のようなものがあり得ます。明確さと冗長さの折衷です。ここに筆者の提案のように “remote ref last updated 5 days ago” を括弧付きで加えることもできます。
status呼び出しがネットワーク越しにfetchするのはうっとうしい、という意見には同意しません。私は、ネットワーク操作は明示的であるべきで、暗黙的であってはならないと思います。git statusを実行するときは、ファイルシステム上にある自分のリポジトリの状態を知りたいのです。他のリモートで何が起きているかを知りたければ、自分で
fetchしてから比較すればよいです。期待するのは、
statusコマンドを開始した時点を基準に最新の情報を得ることです。情報をターミナルに出力している間に競合状態が起きる可能性はありますが、ごく小さな時間枠です。概して、これは
fetchを望んでいるという意味です。二人の将軍問題はリモート側にだけ適用されます。リモートは、クライアントがデータを受け取ったという確認を受け取れなければ、しばらく再送を続けることができます。それはクライアントの問題ではありません。クライアントがサーバーからデータを受け取れなければ、正しい動作は停止したままタイムアウト後にエラーを出力することでしょうし、現在の
fetchはおそらくそうするはずです。Git を学ぶ上で最も価値のある段階は、正しいメンタルモデルを持つことです。すべてはグラフです。
このツールはよく共有されていますが、ここにもよく合います。Git の内部モデルとさまざまなコマンドの効果を視覚化してくれます。
https://learngitbranching.js.org/
このツールを 10 分使って学んだことは、それ以前のおよそ 10 年の Git 経験より多かったです。強くおすすめします。
これ以上付け加えたくはありませんが、Bitbucket の人たちが pull request という用語を作ったのも問題です。
“pull” は、リモートの変更をローカルリポジトリへマージする動作です。ユーザーが実際に依頼しているのは、サーバーに対して、自分のリモート上の変更をどこかのブランチへマージしてほしいということです。GitLab の “merge request” が正しい表現です。
誰かが
fetchしたところ、コミットが消えたのを見たことがあります。リポジトリが時間的に後戻りしたため、隠れていたのです。Git はそうした詳細をstatusに隠しているので、git-promptやpowerline-shellを使わなければ、暗闇の中で作業しているようなものです。git-request-pull(1)に残っている元々の考え方は、私たち全員が kernel.org/git や redhat.com/git のような場所にそれぞれ Git リポジトリを置き、別々のサーバー、組織、ドメインにあるリポジトリから pull してほしいとメールで依頼する、というものでした。GitHub は Git の
request-pullコマンドから着想を得ましたが、それをある GitHub リポジトリから別の GitHub リポジトリへマージするものとして再解釈しました。サーバーの視点では、ユーザーがリモートでサーバーがローカルです。だからユーザーがサーバーに、自分の側から pull するよう依頼しているのです。
少なくとも私はずっとそう読んでいました。
Git の本では、この用語の由来と、なぜ筋が通っているのかを説明しています。
要するに、サーバーに対する依頼ではなく、他の人に対する依頼です。プロジェクトに貢献したい変更を見てもらうために、相手に自分のブランチを pull してほしいと依頼するのです。
[0] https://git-scm.com/book/en/v2/Distributed-Git-Contributing-...