コミットとプッシュの先へ:知っておくべき5つのGitコマンド
commitとpush以外のGitコマンド5選: stash、reflog、cherry-pick、rebase -i、bisectで復元、履歴整理、バグ特定を行う。
多くの開発者は add、commit、push、pull の中で日々を過ごしていますが、Gitの真の威力は、日常のループでは決して触れることのない復旧・履歴編集・デバッグのコマンドにあります。
残りのコマンドとは、たいてい痛い目を見てから出会うことになります。reset --hard が午後いっぱいの作業を飲み込んでしまったり、200個のコミットのどこかにバグが紛れ込んで、どれが原因なのか誰にもわからなかったり。以下に挙げる5つは、基本コマンドでは解決できない問題に対処します。作りかけの作業を棚上げする、消えたと思ったコミットを復元する、単一の修正をブランチ間で移動する、PR前に散らかった履歴を整理する、そしてバグを混入させた正確なコミットを特定する——これらです。それぞれについて、使いどころを判断するトリガー、正確な構文、そして多くの人がつまずく落とし穴を1つずつ紹介します。
要点
git stashは作りかけの作業を棚上げしてクリーンなワーキングツリーを返してくれるので、使い捨てのコミットを作らずにコンテキストを切り替えられます。git reflogはHEADが指し示したすべての位置を記録しているため、間違ったresetやrebaseの後でも、失われたコミットのハッシュを見つけてgit reset --hard <ref>で復元できます。git cherry-pick <hash>は単一のコミットを現在のブランチにコピーします。他のすべてをマージすることなく、1つのホットフィックスをリリースブランチへ移す用途に最適です。git rebase -iやgit reset --hardのような履歴を書き換えるコマンドは、ローカルで未プッシュの作業に対しては安全ですが、共有ブランチでは危険です。うまくいかなかったときの復旧手段がgit reflogです。git bisectは履歴を二分探索し、バグを混入させた正確なコミットを log₂(n) ステップで見つけ出します。
Discover how at OpenReplay.com.
git stash:作業を棚上げして素早くコンテキストを切り替える
使うタイミング: ワーキングツリーが作りかけで散らかっている最中にバグ報告が飛び込んできて、今すぐブランチを切り替える必要があるとき。git stash は未コミットの変更を安全な場所に退避させ、直前のコミットと一致するクリーンなワーキングツリーを返してくれます。使い捨ての「WIP」コミットは不要です。
git stash # shelve tracked changes, clean the tree
git stash --include-untracked # also shelve new, untracked files
git stash list # see all stashes: stash@{0}, stash@{1}, ...
git stash pop # reapply the latest stash and delete it
git stash apply stash@{1} # reapply a specific stash, keep it in the list
落とし穴: pop と apply は交換可能ではありません。git stash pop は退避した変更を適用してstashを削除します。一方 git stash apply は適用しつつstashを残すため、再適用時に競合が起きそうな場合はこちらのほうが安全です。pop がマージ競合に遭遇した場合、stashは削除され ません。まず競合を解決し、消えたと決めつける前に確認しましょう。
git reflog:失ったと思った作業を復元する
使うタイミング: 間違った reset --hard、失敗したrebase、削除したブランチによってコミットが消え去り、パニックに陥ったとき。git reflog はローカルリポジトリでHEADが保持したすべての位置を記録しているため、「失われた」コミットはほぼ確実にまだ存在しています。必要なのはそのハッシュだけです。
git reflog
# ab12cd3 HEAD@{0}: reset: moving to HEAD~1
# ef45gh6 HEAD@{1}: commit: the work you thought vanished
git reset --hard ef45gh6 # restore HEAD to that commit
# or, to inspect it on a new branch first:
git switch -c recovery ef45gh6
戻りたい状態のrefを見つけ、git reset --hard <ref> でそこへジャンプするか、新しいブランチにチェックアウトして安全に内容を確認しましょう。これはツールキットの中でも「しまった、作業を失った」というときのヒーロー的存在です。
落とし穴: reflogは厳密にローカルかつクローンごとのものです。リモートには伝播せず、新規クローンには存在しません。したがって「作業を取り戻す」ために再クローンしても無意味です。エントリはガベージコレクションによって時間とともに削除されるため、復旧は早いに越したことはありません。
git cherry-pick:1つのコミットをブランチ間で移動する
使うタイミング: 単一のホットフィックスコミットをリリースブランチに反映させたいが、それが含まれるフィーチャーブランチ全体をマージしたくないとき。git cherry-pick <hash> は特定の1コミットだけを現在のブランチにコピーし、それ以外はすべて置き去りにします。
git switch release-2.4
git cherry-pick 9f3c1a7 # apply one commit here
git cherry-pick 9f3c1a7 3b8e2d0 # apply several, in order
落とし穴: cherry-pickは 新しい ハッシュを持つ 新しい コミットを作成します。元のコミットは保持されも移動されもしません。同じコミットが後から通常のマージ経由でこのブランチに到達すると、履歴に重複した変更が生じる可能性があります。マージの日常的な代替としてではなく、意図をもって使いましょう。
git rebase -i:PR前に履歴を整理する
使うタイミング: フィーチャーブランチに「fix typo」や「wip」といったコミットが5つ並んでいて、プルリクエストを出す前にレビューしやすい整った履歴にしたいとき。git rebase -i <base-branch> はエディタを開き、コミットの並べ替え、squash、編集、削除ができます。
git rebase -i main
# pick a1b2c3d Add feature scaffold
# squash e4f5g6h fix typo
# reword h7i8j9k Wire up handler
# drop k0l1m2n debug logging
コミットに squash を指定するとその上のコミットに統合され、reword はメッセージのみを変更し、drop は結果から完全に除外します。ブランチ全体を1つのステージ済み変更にまとめたいだけなら、git merge --squash <branch> が対になるツールです。これは統合された変更をステージするだけでコミットは作成しないため、git commit は自分で実行する必要があります。
落とし穴、そして黄金律: ローカルにある間は自由に履歴を書き換えて構いませんが、共有ブランチにすでにプッシュしたコミットは決してrebaseしないでください。共有された履歴を書き換えると、他の全員が苦痛な整合作業を強いられます。これはPro Gitが「リベースの危険性」と呼ぶ問題です。
git bisect:バグを混入させたコミットを見つける
使うタイミング: 先週は動いていたものが今は壊れていて、200個のコミットのどれが原因かまったく見当がつかないとき。git bisect は履歴を二分探索します。壊れているコミットと正常なコミットを1つずつマークし、Gitがチェックアウトする中間点をテストしていくと、log₂(n) ステップで最初の不良コミットを特定できます。
git bisect start
git bisect bad # current commit is broken
git bisect good v2.3.0 # this older tag worked
# Git checks out a midpoint — test it, then tell Git:
git bisect good # this one is fine
git bisect bad # this one is broken
# ...repeat until Git reports "<hash> is the first bad commit"
git bisect reset # return HEAD to where you started
探索全体を自動化することもできます。git bisect run <script> はスクリプトの終了ステータスを使って、手動入力なしに各コミットをgoodまたはbadとしてマークします。セッションの最後には必ず git bisect reset を実行し、探索開始前の位置にHEADを戻しましょう。
落とし穴: bisectは、チェックアウトされる各コミットがビルドでき、テストが正常に実行されることを前提としています。無関係な理由でコンパイルに失敗するコミットがあると結果が歪みます。そうしたコミットは、goodかbadかを当てずっぽうで判断せず、git bisect skip でマークしてください。
破壊的なコマンドのためのセーフティネット
ここで紹介したうち2つは履歴を書き換えるコマンドで、どちらにも同じルールが当てはまります。git rebase -i と git reset --hard は、ローカルで未プッシュの作業に対しては安全ですが、共有ブランチでは危険です。他の人がすでにプルしているブランチで 安全に 取り消したい場合は、git revert を選びましょう。既存の履歴を改変せず、変更を打ち消す新しいコミットを記録します。そしてローカルでの書き換えが失敗したときは、git reflog がコミットを取り戻す非常口になります。
これら5つが名を連ねるに値するのは、それぞれが commit と push では答えられない問いに応えるからです。これを保存する、あれを復元する、1つを移す、片付ける、バグを追い詰める。それぞれをトリガーと結びつけておき、その状況が現れた瞬間に手を伸ばせるようにしておきましょう。
FAQ
git reset --hard と git revert の違いは何ですか?
git reset --hard はHEADを巻き戻してコミットを破棄し、履歴を書き換えます。そのため、他の人がプルしているブランチではどれであっても危険です。一方 git revert は、既存の履歴をそのまま残したうえで対象の変更を打ち消す新しいコミットを記録します。reset --hard はローカルで未プッシュの作業にのみ使い、共有ブランチでの安全な取り消しには revert を使いましょう。resetが失敗した場合は、git reflog によって破棄されたコミットをハッシュから復元できます。
git reset --hard で削除したコミットを復元するにはどうすればよいですか?
git reflog を実行してください。これはローカルリポジトリでHEADが保持したすべての位置を記録しています。戻したい状態のエントリを見つけてハッシュを控え、git reset --hard <hash> で復元するか、まずは git switch -c recovery <hash> で新しいブランチにチェックアウトして内容を確認します。reflogはローカルかつクローンごとのものなので、再クローンしても作業は復元できません。また、エントリはガベージコレクションによって時間とともに削除されるため、復旧は早めに行いましょう。
ブランチの切り替えにおいて、git switch は git checkout を置き換えるものですか?
git switch と git restore は、git checkout に集中しすぎた役割を分割するために Git 2.23(2019年8月)で導入されました。switch はブランチ間の移動を、restore はファイルの復元を担います。どちらのマニュアルページにも長年experimentalの警告が付いていましたが、Git 2.55.0 でその警告は削除され、現在ではこれらの操作における推奨コマンドとなっています。git checkout は引き続き動作し、非推奨にもなっていないため、既存のワークフローもそのまま有効です。
マージではなく git cherry-pick を使うべきなのはどんなときですか?
ソースブランチの残りの履歴を取り込むことなく、他のブランチにちょうど1つのコミットだけが必要なとき(たとえば単一のホットフィックスをリリースブランチへ移す場合)に git cherry-pick を使います。マージはブランチ全体の変更を持ち込むため、1つの修正を切り出す用途には適していません。なお、cherry-pickは新しいハッシュを持つ新しいコミットを作成するため、後から同じ変更が通常のマージ経由で到達すると、履歴に重複した変更が生じる可能性がある点に注意してください。