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つの場所とコミット
買い物メモ
todo.txtだけのリポジトリです。いまは 作業ツリー・ステージ・HEAD のコミット の3つが、どれも同じ中身です。エディタでファイルを書き換えると、変わるのは 作業ツリーだけ です。ステージにもコミットにも、まだ何も起きていません。
$ echo "パンを買う" >> todo.txtgit 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")git addは、作業ツリーの中身を ステージに写す コマンドです。「次のコミットにはこの中身を入れる」という予約にあたります。$ git add todo.txt今度は緑の
Changes to be committedに出ました。ステージと HEAD のコミットの間に差がある、という意味です。$ git status On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: todo.txtgit commitは、ステージの中身をそのまま写真に撮って 新しいコミットを作ります。新しいコミットは、1つ前のコミットを親として指します。そしてmainが新しいコミットへ進みます。$ git commit -m "パンを追加" [main c43e6a8] パンを追加 1 file changed, 1 insertion(+)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 は「今いる場所」
黒い札の
mainが ブランチ です。ブランチの正体は「このコミットを見て」と書いた 付箋 で、中身はコミット ID が1つ書いてあるだけです。ピンクのHEADは「いま自分が乗っているブランチ」を指す付箋です。git branch featureは、今いるコミットに付箋を1枚増やす だけです。ファイルは1つもコピーされません。mainとfeatureが同じコミットを指しています。$ git branch featureブランチの一覧です。
*が付いているのが HEAD の乗っているブランチ。付箋を増やしても、HEAD はmainに乗ったままです。$ git branch feature * main(HEAD -> main, feature)は「このコミットにはmainとfeatureの付箋が貼ってあり、HEAD はmainに乗っている」と読みます。$ git log --oneline e5c0a62 (HEAD -> main, feature) パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加ここでファイルを書き換えて、コミットしてみます。
$ echo "バターを買う" >> todo.txtコミットすると、HEAD が乗っているブランチ(
main)だけ が新しいコミットへ進みます。featureは元のコミットに残ります。-aは「変更したファイルを add してからコミットする」の省略形です。$ git commit -am "バターを追加" [main fbb4973] バターを追加 1 file changed, 1 insertion(+)--allを付けると、すべての付箋から辿れるコミットが出ます。HEAD はmainごと1つ先へ進みました。$ git log --oneline --all fbb4973 (HEAD -> main) バターを追加 e5c0a62 (feature) パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加
3. git checkout ブランチ名 — HEAD を乗り換える
ここまで来れば、git checkout ブランチ名 は2行で説明できます。
- HEAD を、指定したブランチに乗り換える
- 作業ツリーとステージを、そのブランチが指すコミットの中身に入れ替える
ファイルの中身が変わるので「何かが消えた」ように見えますが、コミットもブランチも1つも消えていません。付箋を貼り替えて、その時点の写真をファイルに戻しただけです。
git checkout ブランチ名
前の場面の続きです。
mainは「バターを追加」、featureは1つ手前の「パンを追加」を指しています。HEAD はmainに乗っています。git checkout featureで起きることは2つです。①HEAD をfeatureに乗り換える。②作業ツリーとステージを、featureが指すコミットの中身に入れ替える。作業ツリーから「バターを買う」が消えました。$ git checkout feature Switched to branch 'feature'ファイルの中身も「パンを追加」の時点に戻っています。コミットが消えたのではありません。
mainの付箋は「バターを追加」に貼られたままです。$ cat todo.txt 牛乳を買う 卵を買う パンを買うこの状態で書き換えてコミットすると、どうなるでしょう。
$ echo "ジャムを買う" >> todo.txtHEAD が乗っている
featureだけが進みます。親は「パンを追加」なので、ここで 歴史が二股に分かれ ました。これがブランチを分けて作業する、ということです。$ git commit -am "ジャムを追加" [feature bee85e4] ジャムを追加 1 file changed, 1 insertion(+)mainに戻ります。HEAD がmainに乗り換わり、作業ツリーは「バターを追加」の中身に入れ替わります。$ git checkout main Switched to branch 'main'main側には「ジャム」はありません。ブランチを乗り換えると、ファイルはそのブランチの中身になります。$ cat todo.txt 牛乳を買う 卵を買う パンを買う バターを買うまとめると、
git checkout ブランチ名は HEAD の付箋を貼り替えて、ファイルをその時点の中身にする コマンドです。コミットもブランチも、1つも消えていません。$ git log --oneline --all bee85e4 (feature) ジャムを追加 71d9b3e (HEAD -> main) バターを追加 e5c0a62 パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加
書きかけの変更があるとき
コミットしていない変更があるまま checkout すると、どうなるでしょうか。答えは「変更が上書きされるなら止まる。上書きされないなら持ったまま移る」です。checkout は、コミットしていない変更を勝手に消さないように作られています。
変更があると checkout は止まる
今度はファイルが2つあります。
notes.txtはmainとfeatureで同じ中身、todo.txtは違う中身です。まず、どちらのブランチでも同じ中身の
notes.txtを書き換えます。$ echo "10時に出る" >> notes.txt乗り換えられました。
M notes.txtは「書きかけの変更を持ったまま移った」という意味です。乗り換え先でもnotes.txtの中身は同じなので、書きかけを上書きせずに済むからです。$ git checkout feature M notes.txt Switched to branch 'feature'いったん
mainに戻ります(書きかけはそのまま付いてきます)。$ git checkout main M notes.txt Switched to branch 'main'次に、ブランチによって中身が違う
todo.txtを書き換えます。$ echo "チーズを買う" >> todo.txt止まりました。
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エラー文のとおり、先にコミットして変更を残します(どこかに退避する
git stashという方法もあります)。$ git commit -am "チーズとメモを追加" [main c25dd2a] チーズとメモを追加 2 files changed, 2 insertions(+)今度は乗り換えられました。書きかけはコミットとして
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)
checkoutにはブランチ名ではなく、コミット ID を渡すこともできます。2つ前の「卵を追加」(8b41d07) に行ってみます。$ git log --oneline e5c0a62 (HEAD -> main) パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加長い英文が出ました。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 卵を追加作業ツリーは「卵を追加」の時点の中身です。過去の状態を見に行くときの使い方です。
$ cat todo.txt 牛乳を買う 卵を買うgit statusの1行目もHEAD detached at 8b41d07になります。$ git status HEAD detached at 8b41d07 nothing to commit, working tree cleanこの状態でもコミットはできます。試してみます。
$ echo "試しに書く" >> todo.txtコミットが作られ、HEAD がそのコミットへ進みました。ただし このコミットにはブランチの付箋が1枚も貼られていません(丸が点線なのはその印)。進んだのは HEAD だけです。
$ git commit -am "試し" [detached HEAD 84ba258] 試し 1 file changed, 1 insertion(+)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'まだ取り戻せます。
HEAD@{1}は「HEAD が1つ前にいた場所」(reflog)です。そこにtryという付箋を貼れば、置き去りのコミットはブランチから辿れるようになり、消えなくなります。$ git branch try HEAD@{1}tryから辿れるようになりました。detached HEAD は怖い状態ではなく、「付箋を貼らずに作業している」状態です。作ったものを残したいときだけ、ブランチを貼ればよいのです。$ git log --oneline --all 84ba258 (try) 試し e5c0a62 (HEAD -> main) パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加
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 -- ファイル(変更を捨てる)
ここからは
checkoutの もう1つの仕事 です。まず、間違った変更を作業ツリーに入れます。$ echo "間違えて書いた" >> todo.txt--の後ろにファイル名を書くと、checkoutは HEAD を動かさず、そのファイルを ステージの中身で上書き します。グラフは1つも動いていません。作業ツリーの「間違えて書いた」が消えました。$ git checkout -- todo.txt Updated 1 path from the index元に戻りました。便利ですが、消した変更はどこにも残りません。コミットしていない変更なので、reflog でも取り戻せません。
$ cat todo.txt 牛乳を買う 卵を買う パンを買う次は、ステージ済みの変更があるときです。1行書いて add します。
$ echo "これはステージする" >> todo.txtステージにも「これはステージする」が入りました。
$ git add todo.txtさらに作業ツリーだけに1行足します。3つの場所の中身が、すべて違う状態です。
$ echo "さらに書いた" >> todo.txtgit checkout -- ファイルが戻す先は ステージ です。なので「さらに書いた」だけが消え、「これはステージする」は残ります。$ git checkout -- todo.txt Updated 1 path from the indexHEADを挟むと、戻す先が HEAD のコミット になり、ステージと作業ツリーの両方 が上書きされます。ステージした変更もまとめて消えます。Claude Code の許可画面で見かけるgit checkout HEAD -- ファイルは、この「そのファイルの未コミットの変更を全部捨てる」操作です。$ git checkout HEAD -- todo.txt Updated 1 path from e5c0a62最後に、
--を省いた形も確かめます。もう一度書き換えます。$ echo "もう一度書いた" >> todo.txt--が無くても、todo.txtはブランチ名ではないので、Git はファイルを戻す側の仕事をします。--の有無ではなく、ファイル名が入っているかどうか で見分けます。$ git checkout todo.txt Updated 1 path from the index3つの場所がまた同じ中身に戻りました。
$ 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 feature | git switch feature |
| ブランチを作って乗り換える | git checkout -b feature | git switch -c feature |
| 過去のコミットを見に行く | git checkout 8b41d07 | git switch --detach 8b41d07 |
| 作業ツリーの変更を捨てる | git checkout -- todo.txt | git restore todo.txt |
| add を取り消す | git reset HEAD todo.txt | git restore --staged todo.txt |
| add した分も含めて捨てる | git checkout HEAD -- todo.txt | git restore --staged --worktree todo.txt |
switch と restore で書くと
git switchは「ブランチを乗り換える」だけを受け持つコマンドです。git checkout featureと同じことが起きます。$ git switch feature Switched to branch 'feature'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.わざと detached HEAD にしたいときは
--detachを付けます。$ git switch --detach 3f2a9c1 HEAD is now at 3f2a9c1 牛乳を追加mainに戻ります。$ git switch main Previous HEAD position was 3f2a9c1 牛乳を追加 Switched to branch 'main'ファイルを戻す側は
git restoreです。$ echo "間違えて書いた" >> todo.txtgit restore ファイルはgit checkout -- ファイルと同じで、作業ツリーをステージの中身に戻します。$ git restore todo.txtcheckoutが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 switch | HEAD を乗り換え、ファイルを入れ替える | なし(上書きされる変更があれば止まる) |
git checkout コミットID | detached 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