git switchとcheckoutの違い:枝を切り替える操作と、detached HEAD

公開 書いた人 大林 諒

調べ物やAIの回答に git checkout と git switch が混ざって出てきて、どちらを打てばいいのか分からない人向けの話です。ブランチ(枝)は、本筋を止めずに作業を分ける線のことです。サンプル料理教室のホームページを例に、対応する2つの書き方を実際に動かして並べます。あわせて、古いコミットを見に行ったときに出る detached HEAD という長い英語の意味と、そこから戻る方法も確かめます。読み終わると、古い記事のコマンドを読み替えられて、detached HEADが出ても慌てずに済みます。

この記事の内容
  1. 先に答えると
  2. 対応の表
  3. 切り替えられないとき
  4. git checkout — ファイルの危なさ
  5. detached HEADとは
  6. リモートの枝に切り替える
  7. できないことと、その代わり
  8. よく詰まるところ
  9. 次に読む

先に答えると

枝の切り替えは git switch、ファイルを戻すのは git restore を使います。git checkout はこの2つの役を1つで持っていた古い入口で、今も動きます。自分で打つなら新しいほうが誤解がありません。

  • git checkout -b 枝 は git switch -c 枝 と同じです
  • git checkout -- ファイル は git restore ファイル と同じで、コミットしていない変更が消えます
  • git switch はGit 2.23で追加されました。それより古いGitにはありません
  • 移れないときの選択肢は、コミット、git stash、--discard-changes の3つです。最後のものは変更が消えます
  • detached HEADは枝の外にいる状態で、git switch - で戻り、git switch -c 枝 で作業を救えます

対応の表

やること新しい書き方古い書き方
枝を移るgit switch maingit checkout main
枝を作って移るgit switch -c fix-menu-pricegit checkout -b fix-menu-price
指定の場所から枝を作るgit switch -c 枝 main~1git checkout -b 枝 main~1
直前の枝に戻るgit switch -git checkout -
変更を捨てて移るgit switch --discard-changes 枝git checkout -f 枝
古いコミットを見に行くgit switch --detach コミットgit checkout コミット
ファイルの変更を捨てるgit restore menu.htmlgit checkout -- menu.html
ファイルを昔の版にするgit restore --source コミット menu.htmlgit checkout コミット -- menu.html

main~1 は main の1つ前のコミットです。コミットは、変更を1回分の記録として残したものです。ここからの表示は、一時フォルダに作った練習用のリポジトリで動かしたものです。

$ git --version
git version 2.50.1 (Apple Git-155)

$ git switch -c fix-menu-price
Switched to a new branch 'fix-menu-price'

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git checkout -b add-summer-class
Switched to a new branch 'add-summer-class'

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git switch -
Switched to branch 'add-summer-class'

(最後の行は、直前にいた枝の名前が出ます。)

同じ結果になるので、古い記事の手順はこの表で読み替えれば動きます。違いが出るのは次の3点です。

違いgit switch・git restoregit checkout
枝の名前を間違えたfatal: invalid referenceerror: pathspec ... did not match
コミットの番号を渡した--detach がないと止まるそのまま枝の外に出る
昔の版のファイルを取り出した作業中のファイルだけ変わるgit add 済みの状態にもなる

最後の行は、git status --short の印の位置で分かります。左に寄った M が git add 済みです。

$ git restore --source e8d23cd~1 menu.html

$ git status --short
 M menu.html

$ git checkout e8d23cd~1 -- menu.html

$ git status --short
M  menu.html

もともと git checkout が1つで2つの仕事をしていたので、分けられました。公式の説明でも、git checkout には枝を切り替える役と、別の版のファイルに戻す役の2つがあるとされています。Git 2.23のリリースノートに、この2つを git switch と git restore に分けて導入したとあります。

古いGitでは git switch を打ってもコマンドが無いというエラーになります。git --version で2.23より古いと出たら、Gitを更新するか、表の右の列を使います。

1つだった git checkout が、枝は switch、ファイルは restore に分かれた
1つだった git checkout が、枝は switch、ファイルは restore に分かれた

切り替えられないとき

menu.html を編集中で、移り先の枝でも同じファイルの内容が違っていると、止まります。

$ git status --short
 M menu.html

$ git switch fix-menu-price
error: Your local changes to the following files would be overwritten by checkout:
	menu.html
Please commit your changes or stash them before you switch branches.
Aborting

変更はまだ消えていません。Gitが上書きを止めてくれた状態です。選択肢は3つです。

選択肢変更はどうなるか向いている場面
コミットする枝に残る区切りがついている、残したい
git stash一時的によけて、あとで戻せる書きかけで、割り込みを先に片づけたい
--discard-changes消える。戻せない捨てると決めている

コミットする

今いる枝に入れたくないなら、枝を作ってからコミットします。

$ git switch -c save-work
Switched to a new branch 'save-work'

$ git commit -m "作業の途中"
[save-work 750b86b] 作業の途中
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git switch fix-menu-price
Switched to branch 'fix-menu-price'

git stashでよける

git stash は、書きかけの変更を一時的によける操作です。戻す先の枝で git stash pop を打ちます。

$ git stash
Saved working directory and index state WIP on main: 363c383 問い合わせの電話番号を修正

$ git switch fix-menu-price
Switched to branch 'fix-menu-price'

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git stash pop
On branch main
Your branch is up to date with 'origin/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:   menu.html

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (64f54133f74b556d180ccfa0bd059042cee1a200)

–discard-changesで捨てる

変更を捨てて移ります。確認は出ません。コミットしていないので、Gitからは取り戻せません。先に git diff で中身を見てから打ちます。

$ git switch --discard-changes fix-menu-price
Switched to branch 'fix-menu-price'

$ git status --short

$ grep -n 和食 menu.html
2:<p>和食の基本 6,300円</p>

ぶつからないときは持ったまま移れる

移り先の枝と内容がぶつからなければ、Gitは止めず、編集を持ったまま移ります。ファイルの横に M が出ているときがこれです。

$ git switch add-summer-class
Switched to branch 'add-summer-class'
M	index.html

書きかけが別の枝に入ってしまう原因になるので、移ったら git status を見ます。

git checkout — ファイルの危なさ

git checkout -- menu.html は枝を移る操作に見えますが、コミットしていない変更を捨てる操作です。確認は出ず、何も表示されません。

$ git diff --stat
 menu.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git checkout -- menu.html

$ git diff --stat

$ grep -n 和食 menu.html
2:<p>和食の基本 6,200円</p>

編集した9,999円が消えました。git restore menu.html も同じ結果になります。役割の名前から読み取れる分だけ、restore のほうが打ち間違いに気づけます。

-- はファイル名の始まりの印です。枝とファイルが同じ名前のとき、これがないとGitは枝だと判断します。次の例は fix-note という枝とファイルが両方ある場面です。

$ git checkout fix-note
Switched to branch 'fix-note'
M	fix-note

変更を捨てるつもりで打つと、枝が移ってしまいます。ファイルの話なら git restore にしておけば、この心配がありません。

同じ git checkout でも、main なら枝を移るだけ、ファイル名を付けると変更が消える
同じ git checkout でも、main なら枝を移るだけ、ファイル名を付けると変更が消える

detached HEADとは

HEAD は「今いる場所」の札です。ふつうは枝を指し、枝が先頭のコミットを指しています。古いコミットを直接開くと、HEAD が枝を通さずコミットを指します。この状態がdetached HEAD(枝から外れた状態)です。壊れたわけではありません。

git switch は、コミットの番号を渡すと止めてくれます。

$ git log --oneline -4
f9d5133 メモを消す
d6850bf メモを追加
363c383 問い合わせの電話番号を修正
e8d23cd 料金表を修正

$ git switch HEAD~2
fatal: a branch is expected, got commit 'HEAD~2'
hint: If you want to detach HEAD at the commit, try again with the --detach option.

$ git switch --detach HEAD~2
HEAD is now at 363c383 問い合わせの電話番号を修正

$ git status
HEAD detached at 363c383
nothing to commit, working tree clean

git checkout は止めません。代わりに次の全文が出ます。

$ git checkout 363c383
Note: switching to '363c383'.

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>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 363c383 問い合わせの電話番号を修正

日本語にすると、見て回っても試してもよく、枝に戻れば影響は残らない、残したければ git switch -c、やめるなら git switch -、という案内です。

見るだけなら戻るだけ

古い料金を確かめて、元の枝に戻ります。

$ grep -n 和食 menu.html
2:<p>和食の基本 6,200円</p>

$ git switch -
Previous HEAD position was 363c383 問い合わせの電話番号を修正
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

そこでコミットするとどうなるか

コミットはできます。ただし枝に属さないので、離れると一覧から見えなくなります。

$ git commit -m "古い版に案内を追加"
[detached HEAD 5b58794] 古い版に案内を追加
 1 file changed, 1 insertion(+)

$ git status -sb
## HEAD (no branch)

$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  5b58794 古い版に案内を追加

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> 5b58794

Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git log --oneline --all --grep=古い版

最後の検索で何も出ません。全部の枝を探しても、もう見つからないという意味です。参照のないコミットはいずれGitの掃除で消されるので、早めに救います。

通常は HEAD が枝を指すが、detached HEAD では古いコミットを直接指す
通常は HEAD が枝を指すが、detached HEAD では古いコミットを直接指す

git switch -cで救う

まだその場にいるなら、git switch -c で枝を作るだけです。

$ git switch -c try-note
Switched to a new branch 'try-note'

離れたあとなら、git reflog(手元で動いた記録)で番号を探して枝を作ります。Warningの中の番号が分かっていれば、それを使います。

$ git reflog -2
f9d5133 HEAD@{0}: checkout: moving from 5b587947d202aecacac544a27ffcf8ab45cc94f7 to main
5b58794 HEAD@{1}: commit: 古い版に案内を追加

$ git switch -c rescue-old-note 5b58794
Switched to a new branch 'rescue-old-note'

$ git log --oneline -2
5b58794 古い版に案内を追加
363c383 問い合わせの電話番号を修正

リモートの枝に切り替える

外注エンジニアBが add-summer-class を送ったという連絡が来た場面です。リモート(GitHubのような共有の置き場)の枝は、手元に受け取るまで見えません。

$ git switch add-summer-class
fatal: invalid reference: add-summer-class

$ git fetch
From ../remote/website
 * [new branch]      add-summer-class -> origin/add-summer-class

$ git branch -a
  fix-menu-price
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/add-summer-class
  remotes/origin/main

$ git switch add-summer-class
Switched to a new branch 'add-summer-class'
branch 'add-summer-class' set up to track 'origin/add-summer-class'.

$ git status -sb
## add-summer-class...origin/add-summer-class

git fetch は送り先の更新を受け取るだけの操作です。手元に同じ名前の枝がなく、リモートに1つだけあると、git switch が同じ名前の枝を作って、リモートの枝と結びつけます。結びつけた枝を追跡枝と呼びます。以後は git pull と git push が枝の名前なしで動きます。git checkout add-summer-class でも同じです。

$ git checkout add-summer-class
Switched to a new branch 'add-summer-class'
branch 'add-summer-class' set up to track 'origin/add-summer-class'.
git fetch でリモートの枝の札が手元にでき、git switch で同じ名前の枝が結びつく
git fetch でリモートの枝の札が手元にでき、git switch で同じ名前の枝が結びつく

できないことと、その代わり

できないことその代わり
1つのフォルダで2つの枝を同時に開くもう1つ別のフォルダにcloneする
git switch でファイルを戻すgit restore を使う
--discard-changes で捨てた変更を戻す戻せない。迷うなら git stash
コミットの番号を git switch だけで開く--detach を付ける
2.23より古いGitで git switch を使うGitを更新する。できないなら git checkout

よく詰まるところ

症状原因直し方
fatal: invalid reference: 枝打ち間違い、またはまだ受け取っていないgit fetch のあと git branch -a で名前を確かめる
error: pathspec '枝' did not match any file(s) known to gitgit checkout での同じ間違い同じ
fatal: a branch named '枝' already exists-c でもう一度作ろうとした-c を外して git switch 枝
fatal: a branch is expected, got commitgit switch にコミットの番号を渡した見るだけなら --detach を付ける
HEAD detached at と出る古いコミットを開いているgit switch - か git switch main。残すなら先に git switch -c 枝
would be overwritten by checkout変更が移り先とぶつかるコミットか git stash。捨てるなら git diff を見てから --discard-changes
別の枝で変更が消えたように見える移ったのでその枝の状態になった元の枝に戻る。枝を移っても、コミットは消えない

AIが git checkout を提案してきたら、この記事の表で読み替えて意味を確かめます。-- か -f が付いていれば、変更が消える操作です。実際に運用していると、AIが古いコミットを git checkout で開いたまま作業を続け、枝に入っていないコミットが積み上がることがあります。作業の前に git status で枝の名前を見させます。

次に読む

枝を分けて合流する流れは「git branchとmergeの使い方:枝を分けて合流する、GitHub Flowまで」、変更やコミットを取り消すときは「Gitで元に戻す:revert・reset・restoreの違いと使い分け」にあります。リモートの枝を受け取る話は「git clone・pull・fetchの違い:受け取る操作の使い分け」、ほかのコマンドを引くなら「Gitコマンド一覧:場面ごとによく使うものと、危ないもの」を開きます。

あわせて読む