Git はコミットに関するすべてのことに関係しています。多種多様な Gitコマンドを使用して、コミットのステージング、コミットの作成、古いコミットの表示、リポジトリ間でのコミットの転送を行います。これらのコマンドの大部分は何らかの形でコミットで動作し、その多くはコミット参照をパラメータとして受け入れます。たとえば、git checkout を使用して、コミットハッシュを渡すことで古いコミットを表示したり、ブランチ名を渡してブランチを切り替えたりすることができます。

コミットを参照するための多くの方法

コミットを参照する多くの方法を理解することで、これらすべてのコマンドをより強力に使用することができるようになります。この章では、コミットを参照する多くの方法を示しながら、git checkoutgit branchgit push などの一般的なコマンドの内部動作について説明します。

また、 Git の reflog メカニズムを介して「失われた」ようにみえるコミットにアクセスして復活させる方法も学習します。

ハッシュ

コミットを参照する最も直接的な方法は、SHA-1 ハッシュを利用することです。これは、各コミットの一意の ID として機能します。git log の出力からすべてのコミットのハッシュを見つけることができます。

commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date: Wed Jul 9 16:37:42 2014 -0500

 Some commit message

他の Git コマンドにコミットを渡す場合に必要なことは、そのコミットを一意に特定できる十分な数の文字を指定することです。たとえば、次のコマンドを実行することで、git show を使用して上記のコミットを検査できます。

git show 0c708f

場合によっては、ブランチ、タグ、または別の間接参照を対応するコミットハッシュに変換する必要があります。これには、git rev-parse コマンドを使用できます。次の例は、マスターブランチがポインタになっているコミットのハッシュを返します。

git rev-parse master

これは、コミット参照を受け入れるカスタムスクリプトを記述するときに特に便利です。手動でコミット参照を解析する代わりに、git rev-parse で入力を正規化させることができます。

Ref

ref は、コミットを参照するための間接的な方法です。コミットハッシュの使いやすい別名と考えることができます。これは、ブランチおよびタグを表す Git の内部機構です。

ref は .git/refs ディレクトリに通常のテキストファイルとして保存されます。このディレクトリでは、.git は通常 .git と呼ばれます。リポジトリの1つで ref を調べるには、.git/refs に移動します。次の構造が表示されるはずですが、リポジトリに含まれるブランチ、タグ、およびリモートに応じて異なるファイルが含まれます。

.git/refs/
heads/
master
some-feature
remotes/
origin/
master
tags/
v0.9

The heads directory defines all of the local branches in your repository. Each filename matches the name of the corresponding branch, and inside the file you’ll find a commit hash. This commit hash is the location of the tip of the branch. To verify this, try running the following two commands from the root of the Git repository:

# Output the contents of `refs/heads/master` file:
cat .git/refs/heads/master

# Inspect the commit at the tip of the `master` branch:
git log -1 master

The commit hash returned by the cat command should match the commit ID displayed by git log.

マスターブランチの場所を変更するために Git が実行する必要があるのは、refs/heads/master ファイルのコンテンツの変更のみです。同様に、新しいブランチの作成とは、単に、新しいファイルに対してコミットハッシュを記述することです。これは Git ブランチが SVN に比べて非常に軽量である理由の一つです。

tags ディレクトリはまったく同じ方法で動作しますが、ブランチの代わりにタグが含まれています。remotes ディレクトリには、git remote で個別のサブディレクトリとして作成したすべてのリモートリポジトリが一覧表示されます。各リポジトリ内部に、ご使用のリポジトリにフェッチしたすべてのリモートブランチがあります。

ref を指定する

ref を Git コマンドに渡す場合、その ref のフルネームを定義するか、ショートネームを使用して一致する ref を Git に検索させます。, ref のショートネームは名前でブランチを参照するたびに使用するものなので、既に精通しているはずです。

git show some-feature

上記コマンドの some-feature 引数は実際にはブランチのショートネームです。Git はこれを使用する前に refs/heads/some-feature に変換します。次のようにコマンド行で絶対 ref を指定することもできます。

git show refs/heads/some-feature

これは、ref の場所に関する曖昧さを回避します。たとえば、some-feature と呼ばれるタグとブランチの両方が存在する場合などに必要です。ただし、適切な命名規則使用している場合、タグとブランチ間の曖昧さは一般的に問題にならないはずです。

Refspecs セクションでは、さらに多くの ref のフルネームを学習します。

Packed Refs

大規模なリポジトリの場合、Git はパフォーマンスの効率を高めるために、定期的にガーベッジ・コレクションを実行して不要なオブジェクトを削除し、複数の ref を単一のファイルに圧縮します。この圧縮は、ガーベッジ・コレクションコマンドを使用して強制できます。

git gc

これにより、refs フォルダ内の個々のブランチとタグファイルはすべて、.git ディレクトリの最上部の packed-refs という単一のファイルに移動します。このファイルを開くと、ref に対応するコミットハッシュのマッピングがあります。

00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

外部では、Git の通常の機能は何ら影響を受けません。しかし、.git/refs フォルダーが空であることに戸惑うかもしれません。refs の場所は次のとおりです。

特別な ref

refs ディレクトリに加えて、.git ディレクトリの最上部に特別な ref がいくつかあります。以下に一覧を示します。

  • HEAD – 現在チェックアウト中のコミット/ブランチ。
  • FETCH_HEAD – リモートリポジトリからフェッチされた最新のブランチ。
  • ORIG_HEAD – 大幅な変更前の HEAD へのバックアップ参照。
  • MERGE_HEADgit merge を使用して現在のブランチにマージしようとしているコミット。
  • CHERRY_PICK_HEAD – 選択したコミット

これらの ref はすべて必要に応じて Git により作成や更新が行われます。たとえば、git pull コマンドは最初に git fetch を実行し、これが FETCH_HEAD 参照を更新します。次に、git merge FETCH_HEAD を実行して、リポジトリへのフェッチ済みブランチのプルを完了します。HEAD で実行したことがあると思いますが、もちろん、これらはすべてその他の ref と同様に使用できます。

これらのファイルには、ファイルのタイプとリポジトリの状態に応じて様々なコンテンツが含まれます。HEAD ref にはシンボリック ref (コミットハッシュではなく、単に別の ref への参照) またはコミットハッシュのいずれかを含めることができます。たとえば、マスターブランチにいる場合の HEAD のコンテンツを調べてみましょう。

git checkout master
cat .git/HEAD

これは、ref: refs/heads/master を出力します。つまり、HEADrefs/heads/master ref へのポインターです。このようにして、Git はマスターブランチが現在チェックアウト中であることを認識します。別のブランチに切り替えた場合、HEAD のコンテンツは新しいブランチを反映するために更新されます。しかし、ブランチの代わりにコミットをチェックアウトした場合、HEAD にはシンボリック ref の代わりにコミットハッシュが含まれます。このようにして、Git は分離した HEAD 階層にいることを認識します。

ほとんどの場合、HEAD は直接使用することになる参照にすぎません。それ以外は、一般的に Git の内部機構に接続する必要がある低レベルのスクリプトを記述する場合にのみ役立ちます。

Refspecs

refspec は、ローカルリポジトリのブランチをリモートリポジトリのブランチにマッピングします。これにより、ローカル Git コマンドを使用してリモートブランチを管理したり、ある種の高度な git pushgit fetch の動作を設定したりすることが可能になります。

refspec は [+]<src>:<dst> と指定されます。<src> パラメータはローカルリポジトリのソースブランチであり、<dst> パラメータはリモートリポジトリの宛先ブランチです。オプションの + 記号は、リモートリポジトリに強制的に非ファストフォワード更新を実行させる場合に指定します。

refspec は git push コマンドとともに使用して、リモートブランチに別の名前を付けることができます。たとえば、次のコマンドは通常の git push と同様にマスターブランチを origin リモートリポジトリにプッシュしますが、origin リポジトリのブランチの名前として qa-master を使用します。これは、QA チームがリモートリポジトリに独自のブランチをプッシュする必要がある場合に役立ちます。

git push origin master:refs/heads/qa-master

また、refspec はリモートブランチの削除にも使用できます。これは、フィーチャーブランチを (バックアップの目的などで) リモートリポジトリにプッシュするフィーチャーブランチ ワークフローでは一般的なシチュエーションです。リモートフィーチャーブランチは、ローカルリポジトリからフィーチャーブランチが削除された後もリモートリポジトリ内に存在するため、プロジェクトが進行するにつれて、不要になったフィーチャーブランチが蓄積されることになります。これらは、次のように空の <src> パラメーターを持つ refspec をプッシュすることで削除できます。

git push origin :some-feature

リモートリポジトリにログインして手動でリモートブランチを削除する必要がないため、これは非常に便利です。ただし、Git v1.7.0 以降は、上記の方法の代わりに --delete フラグを使用できます。次のコマンドは、上記のコマンドと同じ効果があります。

git push origin --delete some-feature

Git 設定ファイルに数行追加することで、refspec を使用して git fetch の動作を変更できます。既定では、git fetch はリモートリポジトリのブランチをすべてフェッチします。その理由は、.git/config ファイルの次のセクションにあります。

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/*:refs/remotes/origin/*

fetch 行は、git fetchorigin リポジトリからブランチをすべてダウンロードするように指示します。しかし、一部のワークフローはすべてのブランチが必要なわけではありません。たとえば、多くの継続的インテグレーションワークフローでは、マスター ブランチのみが重要です。マスター ブランチのみをフェッチするには、fetch 行を次のように変更します。

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master

同様の方法で、git push も設定できます。たとえば、(上記で実行したように) 常にマスターブランチを origin リモートの qa-master にプッシュする必要がある場合、設定ファイルを次のように変更します。

[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master
push = refs/heads/master:refs/heads/qa-master

Refspec を使用すると、様々な Git コマンドがリポジトリ間でブランチを転送する方法について完全に制御できます。ローカルリポジトリからブランチ名の変更やブランチの削除を行ったり、別の名前でブランチをフェッチ/プッシュしたり、git pushgit fetch を設定して、必要なブランチのみで作業したりすることができます。

相対参照

別のコミットに関連するコミットを参照することもできます。~ 文字を使用すると、親コミットに移動できます。たとえば、次の例は、HEAD の祖父母を表示します。

git show HEAD~2

しかし、マージコミットを使用して作業すると、問題は多少複雑になります。マージコミットには2つ以上の親があるため、追跡できるパスが複数になります。3 段階のマージの場合、最初の親はマージの実行時にあなたがいたブランチから派生します。2 番目の親は、git merge コマンドに渡したブランチから派生します。

~ 文字は、常にマージコミットの最初の親の後に続きます。別の親の後にする必要がある場合、^ 文字を使用して対象の親を指定する必要があります。たとえば、HEAD がマージコミットの場合、次の例は HEAD の 2 番目の親を返します。

git show HEAD^2

^ 文字を複数使用して複数の世代を移動できます。たとえば、マージコミットの場合、以下は HEAD の祖父母を表示します。この HEAD は 2 番目の親から生じています。

git show HEAD^2^1

~^ の動作を明確にするために、相対参照を使用して A から生じるいずれかのコミットに移動する方法を次の図に示します。場合によっては、コミットに移動する方法が複数あります。

相対参照を使用してコミットにアクセスする

相対 ref には、通常の ref で使用できるコマンドと同じコマンドを使用できます。たとえば、次のコマンドはすべて相対参照を使用します。

# Only list commits that are parent of the second parent of a merge commit
git log HEAD^2

# Remove the last 3 commits from the current branch
git reset HEAD~3

# Interactively rebase the last 3 commits on the current branch
git rebase -i HEAD~3

Reflog

The reflog is Git’s safety net. It records almost every change you make in your repository, regardless of whether you committed a snapshot or not. You can think of it as a chronological history of everything you’ve done in your local repo. To view the reflog, run the git reflog command. It should output something that looks like the following:

400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2
0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `master`
00f5425 HEAD@{2}: commit (merge): Merge branch ';feature';
ad8621a HEAD@{3}: commit: Finish the feature

これは、次のように変換されます。

  • HEAD~2 を今チェックアウトしました。
  • その前にコミットメッセージを修正しました。
  • その前に、フィーチャーブランチをマスターにマージしました。
  • その前にスナップショットをコミットしました。

HEAD{<n>} 構文により、reflog に保存されているコミットを参照できます。これは、前のセクションの HEAD~<n> 参照と同様にいろいろと役立ちますが、<n> はコミット履歴ではなく reflog 内のエントリーを表します。

これを使用して、ある状態が失われてしまわないようにその状態に戻すことができます。たとえば、たった今 git reset を使用して新しいフィーチャーを破棄したとしましょう。あなたの reflog は次のようになります。

ad8621a HEAD@{0}: reset: moving to HEAD~3
298eb9f HEAD@{1}: commit: Some other commit message
bbe9012 HEAD@{2}: commit: Continue the feature
9cb79fa HEAD@{3}: commit: Start a new feature

git reset の前の3つのコミットは中ぶらりんの状態です。つまり、reflog を使用しない限り、これらを参照する方法はないということです。ここで、あなたは作業のすべてを破棄すべきではなかったことに気付きます。あなたがやらなければならないことは、HEAD@{1} コミットをチェックアウトして git reset を実行する前のリポジトリの状態に戻ることのみです。

git checkout HEAD@{1}

これにより、あなたは分離した HEAD の階層に移動します。ここから、新しいブランチを作成して自分のフィーチャーで作業を続行できます。

Summary

これで、皆さんは Git リポジトリ内のコミットを快適に参照できるようになりました。ここでは、ブランチとタグが .git サブディレクトリに ref として保存される仕組み、packed-refs ファイルを読む込む方法、HEAD の表現方法、高度なプッシュおよびフェッチのための refspec の使用方法、~ および ^ 比較演算子を使用してブランチ履歴をトラバースする方法を学習しました。

また、reflog についても検討します。これは、他のどんな方法でも利用できないコミットを参照する方法です。些細な「しまった、こんなことしなければよかった」という状況から復旧する優れた方法です。

ここまでの要点は、特定の開発シナリオで必要なコミットを正確に選別できるようになることでした。最も一般的なコマンドの一部は、ref を引数として受け入れるため、既存の Git の知識があれば、この記事で学習したスキルを非常に容易に活用することができます。このようなコマンドには、git loggit showgit checkoutgit resetgit revertgit rebase などの多くのコマンドがあります。