Gitでローカルおよびリモートのコミットを安全に取り消す方法
Gitを扱う上で、間違いを修正する能力はスムーズな開発ワークフローの基本です。早すぎるコミット、機密データの誤った含め込み、あるいは一連の変更を元に戻す必要がある場合など、ローカルおよびリモートの操作を安全に取り消す方法を理解することは非常に重要です。このガイドでは、リビジョン管理のクリーンアップにおける主要なツールであるgit reset、git revert、git reflogについて解説します。これらのコマンドを習得することで、自信を持って履歴を管理し、貴重な作業を失うことなく変更を消去または元に戻すことができます。
ローカル履歴の変更(通常は安全)と、共有されたリモート履歴の書き換え(共同作業者に重大な問題を引き起こす可能性がある)を区別することが不可欠です。本ガイドでは、両方のシナリオで最も安全な方法に焦点を当てます。
変更を元に戻すための主要ツールを理解する
Gitには、削除または変更したいコミットを処理するためのいくつかのメカニズムがあります。どのツールを選択するかは、そのコミットが共有リポジトリにプッシュされているかどうか、そしてコミットを履歴から完全に消去したいのか、それともその効果を打ち消す新しいコミットを導入したいのかによって完全に異なります。
1. git reset: ローカル履歴の書き換え
git resetは、ローカルコミット履歴を操作するための最も強力な(そして潜在的に危険な)ツールです。これは現在のブランチポインタ(HEAD)を別のコミットに移動させます。git resetの重要な側面は、--soft、--mixed、--hardオプションによって制御される、ステージングエリアとワーキングディレクトリの扱い方です。
git reset のモード
| モード | HEADへの影響 | ステージングエリア(インデックス)への影響 | ワーキングディレクトリへの影響 |
|---|---|---|---|
--soft |
HEADポインタを移動する。 | 変更なし。 | 変更なし。 |
--mixed (デフォルト) |
HEADポインタを移動する。 | 新しいHEADと一致するようにリセットされる。 | 変更なし。 |
--hard |
HEADポインタを移動する。 | 新しいHEADと一致するようにリセットされる。 | 新しいHEADと一致するようにリセットされる(危険:未コミットの変更が失われる)。 |
使用例: git resetは、ローカルでコミットしてしまったが、コミットすべきではなかったと気づいた変更がある場合、または変更をワーキングディレクトリに残したままステージングから外したい場合に使用します。
git reset の実践例
A. コミットを取り消す(変更をステージングに残す):
コミットしたが、プッシュする前にもっとファイルを追加する必要があることに気づいた場合は、--softを使用します。
# HEADを1つ前のコミットに戻すが、変更はステージングに残す(次のコミットに追加する準備ができている状態)
git reset --soft HEAD~1
B. ステージングから外し、変更をローカルに残す:
コミットしたが、すべてをステージングから外し、ファイルへの変更はワーキングディレクトリに残したい場合は、次のようにします。
# HEADを移動し、変更をステージングから外すが、ファイルをワークスペースで変更された状態に保つ
git reset --mixed HEAD~1
# またはシンプルに:
git reset HEAD~1
C. 完全な消去(最近のコミットに対しては危険):
最後のコミットを完全に破棄し、そのコミット以降に行われたすべてのローカル変更も破棄したい場合(以前のコミットの状態に戻る場合)は、次のようにします。
# 警告:これは指定されたコミット以降のすべての作業を破棄します。
git reset --hard HEAD~1
⚠️ ベストプラクティス警告: 共有リモートリポジトリにすでにプッシュされているコミットに対して
git reset --hardを決して使用しないでください。ただし、誰もそのコミットに基づいて作業していないと絶対的に確信している場合は除きます。共有履歴を書き換えると、共同作業者に問題を引き起こします。
2. git revert: プッシュされたコミットを安全に取り消す
git revertは、すでに公開されている変更を元に戻すための推奨される方法です。履歴を書き換えるのではなく、git revertは特定の以前のコミットによって導入された変更を打ち消す新しいコミットを作成します。履歴はそのまま残り、共同作業に適しています。
使用例: git revertは、すでにリモートサーバー上にある変更(例:バグを導入した機能など)を元に戻す必要がある場合に使用します。
git revert の実践例
コミットハッシュa1b2c3d4がバグを導入したと仮定します。その影響を打ち消すコミットを作成するには、次のようにします。
# a1b2c3d4で導入された変更を元に戻す新しいコミットを作成する
git revert a1b2c3d4
# リバートコミットメッセージのエディタを開きたくない場合:
git revert -n a1b2c3d4
# その後、手動で変更をコミットする
git commit -m "Revert: Fixed issue introduced by a1b2c3d4"
3. git reflog: 安全網
破壊的なgit reset --hardを実行してしまい、数時間分の作業を削除してしまったことに気づいたらどうなるでしょうか?ここでgit reflogが登場します。参照ログ(reflog)は、ローカルリポジトリにおけるHEADのすべての変更—すべてのコミット、リセット、マージ、チェックアウト—を追跡します。これはあなたのローカルの「元に戻す」履歴です。
使用例: 積極的なgit resetによって失われたコミットを回復したり、以前に訪れた一時的な状態に戻ったりする場合。
git reflogでの表示と回復
まず、HEADの移動履歴を表示します。
$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~2
4f5e6d7 HEAD@{1}: commit: Finished feature X
b8a9c0d HEAD@{2}: commit: Started implementation of feature X
...
もしコミット4f5e6d7(HEAD@{1}だったもの)を誤ってリセットしてしまった場合でも、簡単に復元できます。
# 破壊的な操作の1つ前の状態に戻る
git reset --hard HEAD@{1}
ヒント:
git reflogのエントリは通常、ローカルで90日間保持されます。これはresetやブランチ削除を伴う操作における究極のローカル安全網です。
リモートの変更を取り消す(強制プッシュ)
すでにリモートにプッシュされているコミットをgit resetで削除した場合、ローカル履歴はリモート履歴から分岐します。Gitは、非fast-forward更新を伴うため、標準のgit pushを阻止します。
リモートリポジトリを新しい、書き換えられたローカル履歴と同期させるには、強制プッシュを使用する必要があります。
# --force のより安全な代替手段として --force-with-lease を使用する
git push origin <branch-name> --force-with-lease
なぜ--force-with-leaseを使用するのか?
--force-with-leaseは、無骨な--forceオプションよりも安全です。これは、最後にプルして以来、誰もリモートブランチに新しいコミットをプッシュしていないことを確認します。もしその間に誰かがリモートを更新していた場合、プッシュは拒否され、意図せず彼らの作業を消去してしまうことを防ぎます。
どのコマンドを使用するかについてのまとめ
適切なツールの選択は、コミットの状態と送信先によって異なります。
- ローカルでプッシュされていないコミット:
git reset(soft、mixed、またはhard)を使用して、ステージングを調整するか、履歴を完全に消去します。 - プッシュ済み、共有コミット:
git revertを使用して、対となるコミットを作成し、公開履歴を保持します。 - 偶発的な履歴の損失:
git reflogを使用して、以前に失われたHEADの状態を見つけて復元します。 - リモート更新の強制: プッシュされたコミットに対して
git resetを使用してローカルで履歴を安全に書き換えた後にのみ、git push --force-with-leaseを使用します。