🌳

gitのcheckoutコマンドには3つ使い方がある

2022/09/07に公開

「gitのcheckoutって結局どういうコマンドなんですか?」と聞かれたのですが、うまく答えられなかったので調べてみました。調べてみると、checkoutには3つの使い方があることが分かったので、まとめてみようと思います。

そもそもチェックアウトとはどういう意味か?

バージョン管理システムにおいてチェックアウトとは、バージョン管理システムから履歴を取り出すことをいいます。反対に、バージョン管理システムに履歴を登録することをチェックインといいます。

つまり、gitのチェックアウトはリポジトリから履歴を取り出すという意味です。また、gitのチェックインは、ファイルをステージしてからコミットするまでの一連の流れだといえます。

gitに登場する3つのスナップショット

checkoutの説明をする前に、gitの基本的な用語を整理してみます。gitには作業ディレクトリ・インデックス・コミットという3つのスナップショットがあります。図にするとこんな感じです。

作業ディレクトリの内容を、addコマンドでインデックスに追加します。そして、インデックスの内容をもとに、commitコマンドでコミットを作成します。

作業ディレクトリ

作業ディレクトリは、gitで管理しているディレクトリとその中身のことです。

インデックス

インデックスは、次のコミットに含めるファイルの一覧です。addを実行したときに、インデックスにファイルエントリが追加・更新・削除されます。インデックスの内容はgit ls-files —-stageで確認できます。

$ git ls-files --stage | head -n 5
100644 fc8a0c358e43a0c27627a6a7e959a10f16fc24ae 0       .devcontainer/devcontainer.json
100755 d680dd229a385eddb7d5f9058fcde4683d551b9a 0       .github/workflows/on_push.yml
100644 a4334d01a6971e6cb5a9605a19be5a7d6755acf5 0       .gitignore
100644 4ac4de4b4e6047d73ef693f781c3cca2c3983fcc 0       Gemfile
100644 e85ad5c2b54b9492969b64a2d74ac7f64c7035f3 0       Gruntfile.js

インデックスの各行には、ファイルパスやパーミッション、ハッシュ値が記録されています。このハッシュ値は、ファイルを表すBlobオブジェクトのハッシュ値です。

gitのオブジェクトの中身はcat-fileコマンドで見れます。インデックスの先頭行のBlobオブジェクトの中身を見てみると、こうなりました。

$ git cat-file -p fc8a0c # インデックスにあるdevcontainer.jsonの中身を見る
{
        "image": "vvakame/review:5.3",
        "settings": {
                "editor.wordWrap": "on"
        },
        "extensions": [
                "yuqquu.review-starter-syntax-highlight"
        ],
        "postCreateCommand": [
                "npm",
                "ci",
        ],
        "remoteUser": "root"
}

コミット

コミットは、リポジトリに保存されている、ある時点のすべてのファイルの状態です。コミットはインデックスをもとに作成されます。

$ git cat-file -p main # Commitオブジェクトの中身を見る
tree 402cdcd2c6104f6db593083e572d640ede3025b1
parent 83f6d1efb89af2e0d66e02f067744610a389e3a1
author tekihei2317 <tekihei2317@gmail.com> 1661824847 +0900
committer tekihei2317 <tekihei2317@gmail.com> 1661824847 +0900

fix: コマンドの表記が崩れていたため修正

コミットはインデックスと違って、ファイルの状態を木構造で表現しています。コミットはディレクトリを表す1つのTreeオブジェクトをもちます。Treeオブジェクトは、他のTreeオブジェクトやBlobオブジェクトをもちます。

$ git cat-file -p 402cdc | head -n 5 # コミットがもつTreeオブジェクトの中身を見る
040000 tree d2db8aea5a3e5d8691eedc38d2039875d6cf2344    .devcontainer
040000 tree d3cf164a2e0b1e99c5f37583f919375bfe6d3c2f    .github
100644 blob a4334d01a6971e6cb5a9605a19be5a7d6755acf5    .gitignore
100644 blob 4ac4de4b4e6047d73ef693f781c3cca2c3983fcc    Gemfile
100644 blob e85ad5c2b54b9492969b64a2d74ac7f64c7035f3    Gruntfile.js

ブランチとHEAD

ブランチは、履歴を分岐させるときに使用する機能です。ブランチはコミットへの参照です。たとえば、ローカルのmainブランチの実体は.git/refs/heads/mainです。このファイルの中身を見ると、参照しているコミットのハッシュ値が書かれています。

$ cat .git/refs/heads/main
1044da81f4db0bdd66daaa0f59432d86eee6a517

HEADは、現在の作業位置を表す参照です。実体は.git/HEADにあります。通常はブランチを指しており、HEADが指すブランチが現在のブランチを表します。

$ cat .git/HEAD
ref: refs/heads/main # 現在のブランチはmain

HEADが直接コミットを参照している場合もあります。この状態のことをdetached HEADといいます。

checkoutの3つの使い方

ようやくですが本題です。git checkout -—helpを見ると、7つの使い方が書かれています。

1. git checkout [-q] [-f] [-m] [<branch>]
2. git checkout [-q] [-f] [-m] --detach [<branch>]
3. git checkout [-q] [-f] [-m] [--detach] <commit>
4. git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
5. git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...
6. git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
7. git checkout (-p|--patch) [<tree-ish>] [--] [<pathspec>...]

これらの挙動は、以下の3つに分類できます。5~7は、コミットを指定するかどうかで挙動が少し変わります。

  • ブランチまたはコミットを指定する場合(1~4)→ブランチを切り替える
  • コミットとパスを指定する場合(5~7)→過去のファイルの状態を復元する
  • パスを指定する場合(5~7)→ステージしていない変更を取り消す

ブランチを切り替える(HEADを更新する)

checkoutにブランチまたはコミットを指定すると、指定したコミットまたはブランチでHEADを更新します。おなじみのブランチを切り替えるコマンドです。

git checkout main
git checkout -b feature
git checkout 4d63d4 など

この機能がcheckoutコマンドに含まれているのは、HEADを更新するだけではなく、作業ディレクトリとインデックスを指定したコミットの内容で復元するからだと思います。

checkoutにコミットを直接指定することもできます。この場合は、先述のdetached HEAD状態になります。

過去のファイルの状態を復元する

checkoutにコミットとパスを指定した場合、指定したコミットの指定したパスに該当するファイルを、インデックスと作業ディレクトリにコピーします。つまり、過去のファイルの状態を復元します。

git checkout 4d63d4 app.js # 4d63d4のapp.jsを復元

この使い方が、バージョン管理システムにおけるチェックアウトの意味に最も近いと思います。

ステージしていない変更を取り消す

checkoutにパスのみを指定した場合、インデックスの指定したパスの内容を、作業ディレクトリにコピーします。つまり、ステージしていない変更を取り消します。

この機能がcheckoutに割り当てられているのは違和感があります。なぜかというと、コミットを省略した場合はHEADが使われるのが自然だと思うからです。つまり、HEADの内容がインデックスと作業ディレクトリにコピーされるのが、この使い方の自然な挙動だと思います。

これは、git restoreコマンドが追加された1つの要因なのかなと考えています。

まとめ

gitの基本的な用語と、checkoutコマンドに複数の使い方があることを説明しました。

1つのコマンドに複数の役割があるのは分かりづらいため、Git2.23でswitchコマンドとrestoreコマンドが実験的に追加されています。

git switchとrestoreの役割と機能について - Qiita

restoreは、checkoutの一部とresetの一部をあわせたようなコマンドです。resetcheckoutと同様に複数の使い方がある少し厄介なコマンドなので、次はresetコマンドついて説明できればと思います。

告知

技術書典13で「ゼロからわかるgit入門」という本を出します。gitを使ってみたい方・gitをなんとなく使っている方が、自信をもってgitを使えるようになることを目指して書きました。

多くの人に読んでいただきたいので、この記事やツイートを拡散していだけるとうれしいです。よろしくお願いします。

https://twitter.com/tekihei2317_/status/1565836244566757376

参考

GitHubで編集を提案

Discussion