git checkout が分かるまで — HEAD・ブランチ・コミット

git checkout が分かりにくいのは、このコマンドが2つの別の仕事をしているからです。1つは「HEAD を動かす」こと、もう1つは「ファイルを戻す」こと。どちらの仕事かは、後ろに何を書くかで決まります。この回では、前提の「3つの場所」「ブランチ」「HEAD」を短くおさらいしてから、checkout で何が起きるかを1コマンドずつ見ていきます。

画面の見方
  • 上のターミナルは、実際に打ったときと同じ出力です(Git は英語で答えます)
  • 下の中で起きていることは、コマンドの結果、Git の中の何が変わったかを図にしたものです。丸がコミット、黒い札がブランチ、ピンクの札が HEAD です
  • 右の説明をスクロールして読み進めると、左のターミナルでコマンドが1つずつ打たれます。→ キーでも1つずつ進みます(← で1つ戻ります。スマホでは「次へ」ボタン)。最後まで進んでから → を押すと次の区間へ移ります。黄色く光っているのが、そのコマンドで変わった場所です

1. おさらい — 3つの場所

ファイルは作業ツリー・ステージ・コミットの3つの場所にあります。git add で作業ツリーからステージへ、git commit でステージから写真(コミット)へ写ります。詳しくは1-1 3つの場所とコミットで扱いました。checkout の「ファイルを戻す」仕事は、この3つの場所の間で中身を写す話です。

コミットは「前回からの差分」ではなく、その時点の全ファイルの写真です。そして、1つ前のコミット(親)を指しています。グラフの線は、この「親を指す」関係です。

3つの場所とコミット

  1. 買い物メモ todo.txt だけのリポジトリです。いまは 作業ツリー・ステージ・HEAD のコミット の3つが、どれも同じ中身です。

  2. エディタでファイルを書き換えると、変わるのは 作業ツリーだけ です。ステージにもコミットにも、まだ何も起きていません。

    $ echo "パンを買う" >> todo.txt
  3. git status は「作業ツリーとステージの差」と「ステージとコミットの差」を見せるコマンドです。赤字の modified は、作業ツリーだけが変わっている印です。

    $ git status
    On branch main
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
    	modified:   todo.txt
    
    no changes added to commit (use "git add" and/or "git commit -a")
  4. git add は、作業ツリーの中身を ステージに写す コマンドです。「次のコミットにはこの中身を入れる」という予約にあたります。

    $ git add todo.txt
  5. 今度は緑の Changes to be committed に出ました。ステージと HEAD のコミットの間に差がある、という意味です。

    $ git status
    On branch main
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
    	modified:   todo.txt
    
  6. git commit は、ステージの中身をそのまま写真に撮って 新しいコミットを作ります。新しいコミットは、1つ前のコミットを親として指します。そして main が新しいコミットへ進みます。

    $ git commit -m "パンを追加"
    [main c43e6a8] パンを追加
     1 file changed, 1 insertion(+)
  7. git log --oneline で、コミットが新しい順に1行ずつ出ます。左の7文字がコミットの ID です。(HEAD -> main) の意味は次の場面で見ます。

    $ git log --oneline
    c43e6a8 (HEAD -> main) パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加

2. おさらい — ブランチと HEAD

ブランチはコミットに貼った名前つきの付箋で、HEAD は「いま自分が乗っているブランチ」を指す付箋です。HEAD → ブランチ → コミット と2段で指しています。詳しくは1-3 ブランチと HEADで扱いました。checkout の「HEAD を動かす」仕事は、この付箋の付け替えです。

ブランチは付箋、HEAD は「今いる場所」

  1. 黒い札の main が ブランチ です。ブランチの正体は「このコミットを見て」と書いた 付箋 で、中身はコミット ID が1つ書いてあるだけです。ピンクの HEAD は「いま自分が乗っているブランチ」を指す付箋です。

  2. git branch feature は、今いるコミットに付箋を1枚増やす だけです。ファイルは1つもコピーされません。main と feature が同じコミットを指しています。

    $ git branch feature
  3. ブランチの一覧です。* が付いているのが HEAD の乗っているブランチ。付箋を増やしても、HEAD は main に乗ったままです。

    $ git branch
      feature
    * main
  4. (HEAD -> main, feature) は「このコミットには main と feature の付箋が貼ってあり、HEAD は main に乗っている」と読みます。

    $ git log --oneline
    e5c0a62 (HEAD -> main, feature) パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  5. ここでファイルを書き換えて、コミットしてみます。

    $ echo "バターを買う" >> todo.txt
  6. コミットすると、HEAD が乗っているブランチ(main)だけ が新しいコミットへ進みます。feature は元のコミットに残ります。-a は「変更したファイルを add してからコミットする」の省略形です。

    $ git commit -am "バターを追加"
    [main fbb4973] バターを追加
     1 file changed, 1 insertion(+)
  7. --all を付けると、すべての付箋から辿れるコミットが出ます。HEAD は main ごと1つ先へ進みました。

    $ git log --oneline --all
    fbb4973 (HEAD -> main) バターを追加
    e5c0a62 (feature) パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加

3. git checkout ブランチ名 — HEAD を乗り換える

ここまで来れば、git checkout ブランチ名 は2行で説明できます。

  1. HEAD を、指定したブランチに乗り換える
  2. 作業ツリーとステージを、そのブランチが指すコミットの中身に入れ替える

ファイルの中身が変わるので「何かが消えた」ように見えますが、コミットもブランチも1つも消えていません。付箋を貼り替えて、その時点の写真をファイルに戻しただけです。

git checkout ブランチ名

  1. 前の場面の続きです。main は「バターを追加」、feature は1つ手前の「パンを追加」を指しています。HEAD は main に乗っています。

  2. git checkout feature で起きることは2つです。①HEAD を feature に乗り換える。②作業ツリーとステージを、feature が指すコミットの中身に入れ替える。作業ツリーから「バターを買う」が消えました。

    $ git checkout feature
    Switched to branch 'feature'
  3. ファイルの中身も「パンを追加」の時点に戻っています。コミットが消えたのではありません。main の付箋は「バターを追加」に貼られたままです。

    $ cat todo.txt
    牛乳を買う
    卵を買う
    パンを買う
  4. この状態で書き換えてコミットすると、どうなるでしょう。

    $ echo "ジャムを買う" >> todo.txt
  5. HEAD が乗っている feature だけが進みます。親は「パンを追加」なので、ここで 歴史が二股に分かれ ました。これがブランチを分けて作業する、ということです。

    $ git commit -am "ジャムを追加"
    [feature bee85e4] ジャムを追加
     1 file changed, 1 insertion(+)
  6. main に戻ります。HEAD が main に乗り換わり、作業ツリーは「バターを追加」の中身に入れ替わります。

    $ git checkout main
    Switched to branch 'main'
  7. main 側には「ジャム」はありません。ブランチを乗り換えると、ファイルはそのブランチの中身になります。

    $ cat todo.txt
    牛乳を買う
    卵を買う
    パンを買う
    バターを買う
  8. まとめると、git checkout ブランチ名 は HEAD の付箋を貼り替えて、ファイルをその時点の中身にする コマンドです。コミットもブランチも、1つも消えていません。

    $ git log --oneline --all
    bee85e4 (feature) ジャムを追加
    71d9b3e (HEAD -> main) バターを追加
    e5c0a62 パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加

書きかけの変更があるとき

コミットしていない変更があるまま checkout すると、どうなるでしょうか。答えは「変更が上書きされるなら止まる。上書きされないなら持ったまま移る」です。checkout は、コミットしていない変更を勝手に消さないように作られています。

変更があると checkout は止まる

  1. 今度はファイルが2つあります。notes.txt は main と feature で同じ中身、todo.txt は違う中身です。

  2. まず、どちらのブランチでも同じ中身の notes.txt を書き換えます。

    $ echo "10時に出る" >> notes.txt
  3. 乗り換えられました。M notes.txt は「書きかけの変更を持ったまま移った」という意味です。乗り換え先でも notes.txt の中身は同じなので、書きかけを上書きせずに済むからです。

    $ git checkout feature
    M	notes.txt
    Switched to branch 'feature'
  4. いったん main に戻ります(書きかけはそのまま付いてきます)。

    $ git checkout main
    M	notes.txt
    Switched to branch 'main'
  5. 次に、ブランチによって中身が違う todo.txt を書き換えます。

    $ echo "チーズを買う" >> todo.txt
  6. 止まりました。 feature に移ると todo.txt は feature の中身に入れ替わるので、書きかけの「チーズを買う」が消えてしまいます。Git はそれを防ぐために、何もせずに止まります。HEAD も動いていません。

    $ git checkout feature
    error: Your local changes to the following files would be overwritten by checkout:
    	todo.txt
    Please commit your changes or stash them before you switch branches.
    Aborting
  7. エラー文のとおり、先にコミットして変更を残します(どこかに退避する git stash という方法もあります)。

    $ git commit -am "チーズとメモを追加"
    [main c25dd2a] チーズとメモを追加
     2 files changed, 2 insertions(+)
  8. 今度は乗り換えられました。書きかけはコミットとして main 側に残っています。

    $ git checkout feature
    Switched to branch 'feature'

4. git checkout コミットID — detached HEAD

checkout にはブランチ名の代わりに、コミット ID(や HEAD~2 のような「2つ前」という書き方)を渡せます。すると HEAD は、ブランチを経由せずにコミットを直接指します。この状態を detached HEAD(切り離された HEAD)と呼びます。

過去の状態を見に行くには便利ですが、この状態でコミットすると、そのコミットには付箋が貼られません。ブランチに戻ると、どこからも辿れない「置き去り」になります。

git checkout コミットID(detached HEAD)

  1. checkout にはブランチ名ではなく、コミット ID を渡すこともできます。2つ前の「卵を追加」(8b41d07) に行ってみます。

    $ git log --oneline
    e5c0a62 (HEAD -> main) パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  2. 長い英文が出ました。HEAD が ブランチを経由せず、コミットを直接指している 状態を detached HEAD(切り離された HEAD)と呼びます。グラフでは、HEAD の札がブランチの札から離れて、直接コミットに付いています。

    $ git checkout 8b41d07
    Note: switching to '8b41d07'.
    
    You are in 'detached HEAD' state. You can look around, make experimental
    changes and commit them, and you can discard any commits you make in this
    state without impacting any branches by switching back to a branch.
    
    If you want to create a new branch to retain commits you create, you may
    do so (now or later) by using -c with the switch command. Example:
    
      git switch -c <new-branch-name>
    
    HEAD is now at 8b41d07 卵を追加
  3. 作業ツリーは「卵を追加」の時点の中身です。過去の状態を見に行くときの使い方です。

    $ cat todo.txt
    牛乳を買う
    卵を買う
  4. git status の1行目も HEAD detached at 8b41d07 になります。

    $ git status
    HEAD detached at 8b41d07
    nothing to commit, working tree clean
  5. この状態でもコミットはできます。試してみます。

    $ echo "試しに書く" >> todo.txt
  6. コミットが作られ、HEAD がそのコミットへ進みました。ただし このコミットにはブランチの付箋が1枚も貼られていません(丸が点線なのはその印)。進んだのは HEAD だけです。

    $ git commit -am "試し"
    [detached HEAD 84ba258] 試し
     1 file changed, 1 insertion(+)
  7. main に戻ると警告が出ます。「1つのコミットを、どのブランチからも辿れない場所に置き去りにする」という意味です。グラフで薄く表示しているのが、その置き去りのコミットです。

    $ git checkout main
    Warning: you are leaving 1 commit behind, not connected to
    any of your branches:
    
      84ba258 試し
    
    If you want to keep it by creating a new branch, this may be a good time
    to do so with:
    
     git branch <new-branch-name> 84ba258
    
    Switched to branch 'main'
  8. まだ取り戻せます。HEAD@{1} は「HEAD が1つ前にいた場所」(reflog)です。そこに try という付箋を貼れば、置き去りのコミットはブランチから辿れるようになり、消えなくなります。

    $ git branch try HEAD@{1}
  9. try から辿れるようになりました。detached HEAD は怖い状態ではなく、「付箋を貼らずに作業している」状態です。作ったものを残したいときだけ、ブランチを貼ればよいのです。

    $ git log --oneline --all
    84ba258 (try) 試し
    e5c0a62 (HEAD -> main) パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
Claude Code の worktree も detached HEAD を使う

Claude Code が並行作業のために打つ git worktree add --detach は、「別のフォルダに、ブランチを付けずにコミットを取り出す」コマンドです。そのフォルダの中は detached HEAD になっています。作業を残すときは、最後にブランチを付けるかマージします。

5. git checkout -- ファイル — もう1つの仕事

ここまでの checkout は「HEAD を動かす」仕事でした。ところが、-- の後ろにファイル名を書くと、checkout はHEAD を動かさずに、ファイルの中身だけを戻すコマンドに変わります。

コマンド戻す先消えるもの
git checkout -- ファイルステージの中身そのファイルの、まだ add していない変更
git checkout HEAD -- ファイルHEAD のコミットの中身そのファイルの、add したものも含めたすべての未コミットの変更

-- は「ここから後ろはファイル名」という区切りで、省略もできます。git checkout todo.txt や git checkout HEAD todo.txt と書いても、Git は todo.txt がブランチ名ではなくファイル名だと判断して、同じようにファイルを戻します。

ブランチの checkout と違って、こちらはコミットしていない変更を黙って消します。確認も、元に戻す手段もありません。

git checkout -- ファイル(変更を捨てる)

  1. ここからは checkout の もう1つの仕事 です。まず、間違った変更を作業ツリーに入れます。

    $ echo "間違えて書いた" >> todo.txt
  2. -- の後ろにファイル名を書くと、checkout は HEAD を動かさず、そのファイルを ステージの中身で上書き します。グラフは1つも動いていません。作業ツリーの「間違えて書いた」が消えました。

    $ git checkout -- todo.txt
    Updated 1 path from the index
  3. 元に戻りました。便利ですが、消した変更はどこにも残りません。コミットしていない変更なので、reflog でも取り戻せません。

    $ cat todo.txt
    牛乳を買う
    卵を買う
    パンを買う
  4. 次は、ステージ済みの変更があるときです。1行書いて add します。

    $ echo "これはステージする" >> todo.txt
  5. ステージにも「これはステージする」が入りました。

    $ git add todo.txt
  6. さらに作業ツリーだけに1行足します。3つの場所の中身が、すべて違う状態です。

    $ echo "さらに書いた" >> todo.txt
  7. git checkout -- ファイル が戻す先は ステージ です。なので「さらに書いた」だけが消え、「これはステージする」は残ります。

    $ git checkout -- todo.txt
    Updated 1 path from the index
  8. HEAD を挟むと、戻す先が HEAD のコミット になり、ステージと作業ツリーの両方 が上書きされます。ステージした変更もまとめて消えます。Claude Code の許可画面で見かける git checkout HEAD -- ファイル は、この「そのファイルの未コミットの変更を全部捨てる」操作です。

    $ git checkout HEAD -- todo.txt
    Updated 1 path from e5c0a62
  9. 最後に、-- を省いた形も確かめます。もう一度書き換えます。

    $ echo "もう一度書いた" >> todo.txt
  10. -- が無くても、todo.txt はブランチ名ではないので、Git はファイルを戻す側の仕事をします。-- の有無ではなく、ファイル名が入っているかどうか で見分けます。

    $ git checkout todo.txt
    Updated 1 path from the index
  11. 3つの場所がまた同じ中身に戻りました。

    $ git status
    On branch main
    nothing to commit, working tree clean

6. switch と restore — 2つの仕事を分けたコマンド

1つのコマンドに逆向きの仕事が同居しているのは紛らわしいので、Git 2.23(2019年)から git switch と git restore が加わりました。checkout もそのまま使えます。

やりたいことcheckout で書くと新しい書き方
ブランチを乗り換えるgit checkout featuregit switch feature
ブランチを作って乗り換えるgit checkout -b featuregit switch -c feature
過去のコミットを見に行くgit checkout 8b41d07git switch --detach 8b41d07
作業ツリーの変更を捨てるgit checkout -- todo.txtgit restore todo.txt
add を取り消すgit reset HEAD todo.txtgit restore --staged todo.txt
add した分も含めて捨てるgit checkout HEAD -- todo.txtgit restore --staged --worktree todo.txt

switch と restore で書くと

  1. git switch は「ブランチを乗り換える」だけを受け持つコマンドです。git checkout feature と同じことが起きます。

    $ git switch feature
    Switched to branch 'feature'
  2. switch にコミット ID を渡すと断られます。うっかり detached HEAD になるのを防ぐためです。

    $ git switch 3f2a9c1
    fatal: a branch is expected, got commit '3f2a9c1'
    hint: If you want to detach HEAD at the commit, try again with the --detach option.
  3. わざと detached HEAD にしたいときは --detach を付けます。

    $ git switch --detach 3f2a9c1
    
    
    
    
    HEAD is now at 3f2a9c1 牛乳を追加
  4. main に戻ります。

    $ git switch main
    Previous HEAD position was 3f2a9c1 牛乳を追加
    Switched to branch 'main'
  5. ファイルを戻す側は git restore です。

    $ echo "間違えて書いた" >> todo.txt
  6. git restore ファイル は git checkout -- ファイル と同じで、作業ツリーをステージの中身に戻します。

    $ git restore todo.txt
  7. checkout が1つで2つの仕事をしていたのを、Git 2.23(2019年)から switch(HEAD を動かす)と restore(ファイルを戻す) に分けました。git status のヒントも restore で案内しています。

    $ git status
    On branch main
    nothing to commit, working tree clean

7. Claude Code の許可画面で見かけたら

Claude Code は作業の途中で Git のコマンドを打ち、許可を求めてきます。この回で見たコマンドを、「何が消えうるか」で並べるとこうなります。

コマンド何が起きるか消えうるもの
git status / git log / git diff見るだけなし
git checkout ブランチ名 / git switchHEAD を乗り換え、ファイルを入れ替えるなし(上書きされる変更があれば止まる)
git checkout コミットIDdetached HEAD になる注意 そこで作ったコミットを付箋なしで置き去りにしやすい
git restore --staged ファイルステージを HEAD の中身に戻す(add の取り消し。作業ツリーはそのまま)注意 add した後にさらに書き換えていたら、add した時点の版(ステージにだけあった中身)
git checkout -- ファイル / git checkout ファイル / git restore ファイル作業ツリーをステージの中身に戻す未 add の変更
git checkout HEAD -- ファイル / git checkout HEAD ファイルステージと作業ツリーを HEAD の中身に戻すそのファイルの未コミットの変更すべて

見分け方は、checkout の後ろにファイル名(パス)が入っているかどうかです。-- が付いていなくても、ファイル名が入っていれば「ファイルを戻す」側です。許可する前に、そのファイルに自分の書きかけが残っていないかを確かめてください。

次の回では、同じ「戻す」でもブランチの付箋ごと動かす git reset(--soft / --mixed / --hard)を扱います。

まとめ

  • ファイルは作業ツリー・ステージ・コミットの3つの場所にある。add は作業ツリー→ステージ、commit はステージ→コミット
  • ブランチはコミットに貼った付箋。HEAD は「今乗っているブランチ」を指す付箋
  • git checkout ブランチ名 は HEAD を乗り換えて、ファイルをその時点の中身にする。何も消えない
  • git checkout コミットID は detached HEAD。そこで作ったコミットは、ブランチを貼らないと置き去りになる
  • git checkout -- ファイル は別の仕事で、未コミットの変更を消す。新しい書き方は git restore