Git実務ワークフロー #6 stash・cherry-pick・bisect — 日常ツールセット

読了 7分

ここまで扱ってきたブランチ戦略、rebase、コミット整理、コンフリクト解決、PR運用がワークフローの骨格だとすれば、この記事の3つのツールはその隙間を埋める補助ツールです。毎日使うわけではありませんが、必要な瞬間に知らないと困ります。作業の途中に急ぎの仕事が割り込んできたとき、コミットひとつだけを別のブランチに移す必要があるとき、いつからバグが生じたのか誰も知らないとき。それぞれの状況に答えるのがstash、cherry-pick、bisectです。

全7回で構成します。

  • #1 ブランチ戦略 — GitHub Flowとtrunk-based
  • #2 rebase vs merge — 判断基準と禁則
  • #3 interactive rebase — squash・fixupでコミット整理
  • #4 コンフリクト解決 — 構造とmergetool・rerere
  • #5 PR運用 — レビュー単位・コミットメッセージ・draft PR
  • #6 stash・cherry-pick・bisect — 日常ツールセット ← この記事
  • #7 モノレポとGit — sparse-checkout・サブモジュール・LFS

3つのツールを、状況、基本の使い方、よくある落とし穴の順にひとつずつ整理します。

git stash — 作業を一時的に片付ける #

機能ブランチで作業の真っ最中に、急ぎのレビュー依頼やhotfixが割り込んできます。コミットするには中途半端で、捨てるには惜しい修正が作業ディレクトリに散らばっています。stashはこの中途半端な修正を一時保管場所に押し込み、作業ディレクトリをきれいにしてくれます。

stashで片付けて復元する
git stash push -m "カート検証ロジック作業中"
# 作業ディレクトリがきれいになる — ブランチ切り替えもhotfixも自由

git stash list
# stash@{0}: On feature/cart: カート検証ロジック作業中

git stash pop        # 復元しつつ保管場所から削除

メッセージなしでgit stashだけ実行しても動きますが、保管場所に2つ以上積まれた瞬間、名前のないstashは互いに区別が付かなくなります。最初から-mでメッセージを付ける習慣にしておくのがよいです。

復元コマンドは2つあります。popは適用と同時に保管場所から削除し、applyは適用だけして保管場所に残します。同じ修正を複数のブランチで試したいときはapplyが適しています。ひとつ注意すべき点は、popの途中でコンフリクトが起きるとstashは自動では削除されないことです。コンフリクトを解決した後、git stash dropで自分で整理してこそ保管場所がきれいに保たれます。

基本のstashは追跡中のファイルだけを対象にします。新しく作ったuntrackedファイルまで一緒に片付けるには-uオプションを付けます。stashした内容が大きくなって元のブランチと大きくずれてしまったなら、git stash branch <ブランチ名>でstash時点を基準にした新しいブランチを作り、安全に復元する道もあります。

ヒント
ブランチの切り替えが頻繁なら、stashの代わりにgit worktreeを検討する価値があります。git worktree add ../hotfix mainのように実行すると、同じリポジトリの別ブランチを別のディレクトリにチェックアウトしてくれます。作業中のディレクトリに触れずに隣のフォルダでhotfixを処理する方式なので、片付けて復元するという手順自体がなくなります。

git cherry-pick — コミットひとつだけを移す #

mergeがブランチの履歴全体を合流させるツールだとすれば、cherry-pickは特定のコミットひとつの変更だけを選び、現在のブランチに新しいコミットとして再適用するツールです。

代表的なシナリオはリリースブランチへのhotfixバックポートです。mainで直したバグがすでにリリース済みのブランチにも必要なとき、mainの他の変更まで丸ごとマージするわけにはいきません。修正コミットひとつだけを拾ってきます。

hotfixのバックポート
git switch release/2.3
git cherry-pick -x 4f9a2c1
# [release/2.3 8d3e5b7] fix(auth): トークン有効期限検証エラーを修正
#  (cherry picked from commit 4f9a2c1...)

-xオプションは、コミットメッセージに元コミットへの参照を自動で残します。この1行があってこそ、後から「この修正がどのブランチに入ったのか」を追跡できます。公開ブランチへバックポートするときは付けるのを基本にしておくのがよいです。

連続する複数のコミットは範囲で拾えます。git cherry-pick A..BはAの次のコミットからBまでを順に適用します。途中でコンフリクトが起きたら前回扱った手順どおりに解決してgit cherry-pick --continueで進め、全体をやめるなら--abortで開始前の状態に戻ります。

注意すべきは乱用です。cherry-pickは同じ変更を、異なるハッシュのコミットとして2つの履歴に複製します。ブランチ間のcherry-pickが日常になると、どのブランチに何が反映されたのかの追跡が曖昧になり、後のマージで同じ変更が2回登場して混乱を生むこともあります。正規のルートはあくまでmergeやrebaseであり、cherry-pickはバックポートのような例外状況のツールとして残しておくのが安全です。

git bisect — バグが混入したコミットを探す #

「先週までは動いていたのに今は動かない」というバグがあります。その間のコミットは数十個あり、どのコミットが原因なのか誰も知りません。コミットをひとつずつ確認すれば数十回の確認が必要ですが、bisectはこの問題を二分探索で解きます。正常と故障の中間のコミットを確認し、結果に応じて範囲を半分ずつ狭めていきます。

bisectの手動フロー
git bisect start
git bisect bad                  # 現在のコミットは故障
git bisect good v2.3.0          # このタグの時点は正常
# Bisecting: 10 revisions left to test after this (roughly 4 steps)

# Gitが中間のコミットをチェックアウトしてくれる — 動作確認後に判定を入力
git bisect good                 # このコミットは正常
# Bisecting: 5 revisions left to test after this (roughly 3 steps)
git bisect bad                  # このコミットは故障
# ...繰り返し...
# 4f9a2c1 is the first bad commit

コミット20個の間に隠れた原因も、判定5回以内で特定されます。コミットが数百個あっても確認回数は10回前後です。ここで、これまでの記事で積み上げた原則の効果がそのまま現れます。コミットが小さく、それぞれビルド可能な状態に保たれていれば、原因コミットを特定した瞬間にdiffが小さいので、原因のコードまですぐに見えます。

判定はコマンドで自動化することもできます。成功なら0、失敗なら0以外を返すスクリプトがあれば、全過程が無人で回ります。

bisectの自動化
git bisect start HEAD v2.3.0
git bisect run pytest tests/test_auth.py
# Gitがチェックアウトと判定を繰り返し、最初のbadコミットを報告

探索が終わったらgit bisect resetで元のブランチに戻ります。bisectの間、HEADは過去のコミットに移動している状態なので、resetを忘れると見当違いの時点から作業を続けてしまう事故につながります。

3つのツールを貫く共通点 #

3つを並べてみると、共通点がひとつ見えてきます。履歴がきれいなほどツールの精度が上がるという点です。

  • cherry-pickは、コミットがひとつの変更だけを含んでいるときにきれいに移せます。複数の変更が混ざったコミットは、不要な修正まで一緒に連れていきます。
  • bisectは、すべてのコミットがビルド可能なときに滑らかに回ります。「作業中」コミットが混ざっていると、判定不能な地点で探索が何度も止まります。
  • stashでさえ、コミット単位が小さいチームでは登場頻度が下がります。中途半端な修正の量自体が少ないからです。

コミットを意味単位に整理して小さなPRで流していく習慣は、それ自体で終わらず、こうしたツールの性能になって返ってきます。

まとめ #

この記事の要点は3つです。

  • stashは中途半端な修正を片付けておく一時保管場所です。メッセージを付けること、popのコンフリクト時は手動整理が必要なことを覚えておきます。切り替えが頻繁ならworktreeが代案です。
  • cherry-pickは特定コミットだけを再適用するバックポートのツールです。-xで出どころを残し、正規ルートのmergeを置き換えないよう節度を持って使います。
  • bisectは二分探索でバグ混入コミットを特定します。スクリプトがあればgit bisect runで全過程が自動化されます。

次回の「Git実務ワークフロー #7 モノレポとGit — sparse-checkout・サブモジュール・LFS」はシリーズの最終回です。リポジトリが大きくなるときに出会う問題、つまり巨大リポジトリの部分チェックアウト、リポジトリの中のリポジトリ、大容量ファイルの管理を整理して、シリーズを締めくくります。

X