分散型バージョン管理システムのGitは2005年の登場以降シェアを伸ばし続け、2022年の調査では約94%のユーザーに利用されるほど一般的なツールとなっています。Gitにはさまざまな機能が搭載されていますが、その中で特に混乱を引き起こしがちな用語について、Gitを15年近く使用してきたというジュリア・エヴァンスさんが解説しています。

Confusing git terminology

https://jvns.ca/blog/2023/11/01/confusing-git-terminology/

◆HEADと「heads」

HEADは現在チェックアウト中のブランチやコミットを指しており、「.git/HEAD」に保存されています。一方「.git/refs/heads」に保存されているのはブランチで、「heads」は「branches」と読み替えればOKとのこと。

◆detached HEAD

Gitではブランチ以外にも、タグやコミットをチェックアウトすることが可能です。ブランチ以外をチェックアウトした場合には「detached HEAD」状態になり、この状態ではブランチがないため「git pull」や「git push」は基本的に機能せず、「git commit」「git merge」「git rebase」「git cherry-pick」は使用できるもののブランチから切り離された状態が維持されるのでコミットを見つけるのが難しくなります。

新たなブランチを作成するか、既存のブランチに切り替える事でdetached HEAD状態を脱出できます。

◆マージ・リベース中の「ours」と「theirs」の意味

マージ中に競合が発生した場合、「git checkout --ours file.txt」で「ours」側のファイルを選択できます。この時の「ours」は現在チェックアウト中のブランチを指し、「theirs」はマージ元のブランチを指します。

$ git checkout merge-into-ours # チェックアウトするブランチは"ours"
$ git merge from-theirs # マージで指定するブランチが"theirs"

一方リベースにおいては動作が逆になります。

$ git checkout theirs # チェックアウトするブランチは"theirs"
$ git rebase ours # リベースで指定するブランチが"ours"

◆「Your branch is up to date with 'origin/main'」

このメッセージを単純に解釈すると「あなたのブランチは'origin/main'上で最新です」という意味に捉えられますが、実際には最後に「git pull」や「git fetch」をした時の「origin/main」上で最新という意味です。しばらく「git pull」や「git fetch」をしていなかった場合、実際には最新でないのにユーザーに誤った安心感を与えてしまう可能性があります。

◆「HEAD^」「HEAD~」「HEAD^^」「HEAD~~」「HEAD^2」「HEAD~2」

これらをまとめると下記の通り。

・「HEAD^」と「HEAD~」は同じ意味で、1つ前のコミットの意味

・「HEAD^^^」と「HEAD~~~」と「HEAD~3」は同じ意味で、3つ前のコミットの意味

・「HEAD^3」は3番目の親コミットという意味で、「HEAD~3」とは異なる意味

ほとんどのコミットには親が1つだけ存在していますが、マージコミットなど親が2つ以上あるコミットも存在します。そうした複数の親をもつコミットにおいて、「○番目の親」という形で指定したい場合に「HEAD^○」を使用するとのこと。

◆「..」と「...」

「..」と「...」はともに一定範囲を指定するのに使用されます。例えば下図の通りmainブランチにAとBのコミットが存在し、Aから分岐したtestブランチにCとDのコミットが存在している場合を考えてみます。



このとき、「git log」において下記の範囲を指定すると、それぞれ次の通りになります。

・「main..test」

コミットCおよびDが含まれます。

・「test..main」

コミットBが含まれます。

・「main...test」

コミットB・C・Dが含まれます。

一方「git diff」における範囲指定は「git log」の時と異なり、下記の通りになるとのこと。

・「test..main」

両方のツリーでの差分、つまりBとDの差分が全て表示されます。

・「test...main」

一方のツリーでの差分、つまりAとDの差分が表示されます。

◆「can be fast-forwarded(早送り可能)」

早送り可能な状態とは、例えば下図のように一方のブランチだけに新たなコミットが付いている状態のことです。



見た目を変えると下図の通り。DとEの2つのコミットを追加するだけでマージでき、競合などの問題が発生する可能性はありません。



一方で、下図のようにそれぞれのブランチにコミットが追加されている場合は早送りできません。



◆「reference(参照)」「symbolic reference(シンボリック参照)」

Gitには「参照」と呼ばれるものが少なくとも下記の3つ存在しています。

・ブランチやタグ

・HEAD

・「HEAD^^^」のようにコミットIDを指すもの

エヴァンスさんがこれまで使ったことのあるシンボリック参照はHEADのみとなっており、「シンボリック参照」という用語を使って一般化する理由が分からないとのこと。

◆refspecs

参照元と参照先の情報を示す「refspecs」には下記の情報が保存されていますが、エヴァンスさんは「git clone」や「git remote add」で追加する以外の操作をする動機は無いと述べています。



◆「tree-ish」

例えば「git checkout」のマニュアルを見ると、下記のように「<tree-ish>」という文言が出現します。

git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...

この<tree-ish>に当てはまるのは下記の3つとのこと。

・コミットID(例:「182cd3f」)

・コミットIDへの参照(例:「main」「HEAD^^」「v0.3.2」)

・コミット内のサブディレクトリ(例:「main:./docs」)

ただし、エヴァンスさんはコミット内のサブディレクトリを使ったことはないと述べており、単にコミットIDやコミットIDへの参照と考えておけば良さそうです。

◆「index」「staged」「cached」

これら3つの単語は全て「.git/index」という同じものを指しています。「.git/index」には「git add」で追加したファイルが保存されています。

◆「reset」「revert」「restore」

「git reset --hard」と「git restore .」は同じ動作をするのに「git reset --hard COMMIT」と「git restore --source COMMIT .」は完全に異なる動作をするように、「reset」「revert」「restore」はかなり混乱しがちな単語です。エヴァンスさんによるそれぞれの動作についての簡単な説明は下記の通り。

・「git revert COMMIT」

COMMITの内容を打ち消すような新たなコミットを作成します。

・「git reset --hard COMMIT」

COMMITより後ろの全ての変更点を削除し、COMMIT当時の状態に強制的に戻す危険な操作です。

・「git restore --source=COMMIT PATH」

PATHで指定したファイルをCOMMIT当時の状態に戻します。それ以外のファイルやコミット履歴を変更することはありません。

◆「untracked files」「remote-tracking branch」「track remote branch」

Gitでは「track」という用語が3つの異なる意味で使用されます。

・untracked files

「git status」の表示における「untracked files」は、Gitの管理外のファイルであることを意味しています。

・remote-tracking branch

「origin/main」のようなリモートを追跡しているローカルのブランチのことです。通常のブランチとは異なり、コミットを作成することはできません。

・track remote branch

「track remote branch」は「このブランチはあのリモートブランチを追跡している」というように使用し、例えばmainブランチが特定のリモートブランチを追跡している場合、mainブランチ上でpushやpullを実行すると自動で追跡中のリモートブランチへ変更点を送信したり受信したりします。

◆checkout

checkoutは完全に異なる2つの意味で使用されています。

・「git checkout BRANCH」

ブランチを切り替えます。

・「git checkout file.txt」

file.txtのステージングされていない変更点を破棄します。

Gitの開発陣もこの問題を認識しており、checkoutは「git switch」および「git restore」という2つのコマンドに分割されています。

◆reflog

「re-flog」ではなく「ref-log」で、ブランチ・タグ・HEADなどの「reference(参照)」についてのについての履歴を提供します。重要なブランチをうっかり削除してしまった場合などに役立ちます。

◆merge vs rebase vs cherry-pick

それぞれの機能は下記の通り。

・merge

マージは2つのブランチをマージする1つのコミットを作成します。

・rebase

現在のブランチのコミットを1つずつターゲットブランチへコピーします。

・cherry-pick

rebaseに似ていますが、ピック元のブランチからコミットを現在のブランチへコピーする機能で、構文も大きく異なります。

◆rebase -onto

下記のようなブランチやコミットの状況を考えてみます。



このとき、「git rebase --onto main otherbranch mybranch」というコマンドを実行することで、FとGのコミットをmainブランチのトップに移動させることができるとのこと。なお、エヴァンスさんはこのような状況に出会ったことがないと述べています。

Gitにはエヴァンスさんも意味を理解していないような混乱を引き起こす用語がまだまだ多数存在しているとのこと。Gitの用語がCVSやSubversionの用語とどのように関連するかを解説した投稿も存在しているので、気になる人は確認してみてください。