すでにPRを出してマージまで終わっているブランチの上で、そのまま次の作業を始めてしまった。作業ツリーには未コミットの変更が残っている状態で気づいたので、mainを最新化した新しいブランチへ移し替えた。
状況
- 作業中のブランチ(
feature/align-node-version)は、すでにPRがマージされ役目を終えていた - そのブランチの上で
src/utils/example.tsを編集してしまい、未コミットの変更として残っていた
手順
-
git stash pushで該当ファイルの変更だけを退避するgit stash push -m "WIP" -- src/utils/example.ts -
mainをfetch・pullして最新化する(マージ済みのPRの内容もここで取り込まれる)git switch main git pull origin main -
最新化した
mainから新しいブランチを作成するgit switch -c feature/simplify-pr-template -
退避しておいた変更を戻す
git stash pop
これで、最新のmainを土台にした新しいブランチ feature/simplify-pr-templateの上に、編集中だった変更だけが乗った状態になる。
git stash pushで何が起きているか
「一時領域に退避する」というイメージだが、実体は特別な退避スペースがあるわけではなく、変更内容を通常のcommitオブジェクトとして保存する仕組み。
- 現在のインデックス(ステージ済み変更)とワーキングツリー(未ステージ変更)の状態を、それぞれ独立したcommitオブジェクトとして作成する
- そのコミットへの参照を
refs/stashというreflog形式のrefに積む。stash@{0}が最新で、退避するたびにstash@{1},stash@{2}…と番号がずれていく - 保存が完了すると、ワーキングツリーを
HEADの状態まで戻す(=指定した変更が作業ツリー上からは消える)
つまり通常のブランチ履歴(refs/heads/*)とは別のrefs/stashというポインタに、変更内容をコミットとして積んでいるだけ。git stash listで一覧を確認でき、git stash popで最新の退避内容を作業ツリーに戻して積んだ分を消費し、git stash applyなら消費せず適用だけできる。
今回のようにstash push -- <path>とパスを指定すると、そのパスに一致する変更だけを対象にコミットを作り、他の変更はワーキングツリーに残したまま退避できる。
コマンド中の--について
stash push -m "..." -- <path>の--は、多くのCLIコマンドに共通する慣習で「これより後ろはオプションではなく引数として扱う」という区切りを明示するもの。
- ファイル名が
-で始まる場合(例:-foo.txt)にオプションと誤認されるのを防ぐ git stash pushのヘルプでも書式が[--] [<pathspec>...]となっており、--の後ろがpathspec(対象パス)であることを明示する役割を持つ
git checkout -- <file>やgit diff -- <file>など、パスを指定する他のgitコマンドでも同じ意味で使われる。パス指定を伴うコマンドでは慣習的に--を挟んでおくと安全。
メモ
git switchはブランチをまたいでも作業ツリーの変更を保持しようとしてくれるので、変更内容がブランチ間で競合しなければstashを使わずそのままswitchできることもある。今回はgit pullが「unstaged changesがある」で止まったため、stashを使う流れになった- 変更ファイルを指定して
stash push -- <path>のようにすると、他の変更と混ざらず対象を絞って退避できる