git reset --soft, --mixed, --hard の違い


Git の reset コマンドに付け加えられる, ちょっとややこしい 3 つのオプション "--soft", "--mixed", "--hard" の違いを紹介します.

Git の reset コマンドはコミットレベルで使われる場合とファイルレベルで使われる場合がありますが, 今回はコミットレベルで使われることを想定された --soft, --mixed, --hard の 3 つのオプションの違いを紹介したいと思います.

Git の reset コマンドをコミットレベルで使うということは, 特定の古いコミットまでコミットヒストリを遡り, 遡るまでのコミットを破棄してしまうということです.

例えば次のような A, B, C, D という一連のコミットがあったとして:

                 HEAD
                  |
                  v
                 master
                  |
                  v
A --- B --- C --- D

次の reset コマンドを打ち:

git reset HEAD~2

次のように CD を破棄して B まで遡りたいといった時に使われます:

     HEAD
      |
      v
     master
      |
      v
A --- B

ただそのように reset コマンドは今まで存在していたいくつかのコミットを破棄して, コミットヒストリを書き換えてしまうので, 他の contributor と共有のパブリックレポジトリで使うことは混乱を招くので控えるべきとされています.

なので reset コマンドを使うときは, 他の contributor と共有していない contributor が自分一人だけのプライベートレポジトリやブランチでのみ使うようにしましょう.

なので reset コマンドを使うときは, 注意して使われる必要があるかと思います.

ということで reset コマンドの基本的な説明をさせていただきましたが, 本題の --soft, --mixed, --hard という 3 つのオプションのそれぞれの違いを紹介したいと思います.

それぞれのオプションに共通することは HEAD が指し示すブランチの先端を移動させるということです.

例えば次の 3 つのコマンドのどれを実行しても:

# 1
git reset --soft HEAD~2

# 2
git reset --mixed HEAD~2

# 3
git reset --hard HEAD~2

このようなものが:

                 HEAD
                  |
                  v
                 master
                  |
                  v
A --- B --- C --- D

このようになります:

     HEAD
      |
      v
     master
      |
      v
A --- B

冒頭で示した 2 つの図と全く同じですね.

では一体それら 3 つのコマンドはどういう違いがあるのでしょうか.

答えは Working Directory と Index という 2 つの領域内での違いになります.

ちなみに Working Directory, Index, HEAD これら 3 つは Git の 3 つのツリーと言われています.

Working Directory というのは, ファイルの現在の状態が保存されている領域で, Index というのは git add コマンドで Index に移されたファイルの状態が保存されている領域です.

Index は Staging Area とも言われます.

例えば次のようなコミットをしてきたとします:

                 HEAD
                  |
                  v
                 master
                  |
                  v
A --- B --- C --- D

ご覧のように現在の HEAD は master ブランチで D を指しています.

それぞれのコミットの内容は次のようになります:

  1. A
    1. ファイル README.md を作成した
  2. B
    1. ファイル README.md に変更を加えた
    2. ファイル example.txt を作成した
  3. C
    1. ファイル README.md にさらに変更を加えた
  4. D
    1. ファイル example.txt に変更を加えた

こういったコミットをしてきたという前提でそれぞれ 3 つのオプションの違いを見ていきたいと思います.

  1. git reset --soft HEAD~2

    --soft が使われると reset コマンドで古いコミットに遡ったとしても, 遡る前の Working Directory と Index に保持されていたファイルの状態を引き継ぎます.

    つまりは HEAD~2 というのは HEAD から 2 つ前のコミットに遡り B というコミットに移動するということですが, B に移動して CD のコミットを破棄したとしても C も含まれた D の Working Directory と Index の状態を保ったまま B に移動するということで, B の Working Directory と Index の状態にならないということです.

    このコマンドを実行して HEAD を B に移動させてから git status で現在の Wokring Directory と Index の状態を確認すると次のようになります:

    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
            modified:   README.md
            modified:   example.txt
    

    このように --soft を使うと B にリセットしても CD でコミットした内容が Working Directory と Index の両方で保持されているのがわかります.

    Working Directory に加えて Index の状態も保持するということでソフトということなのでしょう.

  2. git reset [--mixed] HEAD~2

    --mixed を使うと Working Directory は D のままで変更されず, Index のみ HEAD~2 が表す B のコミットの Index に変更させます.

    --mixed はデフォルトで使われるオプションなので, 次のように省略することもできます:

    git reset HEAD~2
    

    このコマンド実行後, git status の結果は次のようになります:

    On branch master
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
            modified:   README.md
            modified:   example.txt
    

    このように D の Working Directory が保持されています. Index は B の Index に変更されたので Changes to be committed: という行が表示されていません. これは Index と HEAD のファイルの状態が B と完全に一致しているためです.

    Working Directory だけ D のものなので, Changes not staged for commit: という行のあとに BD の Working Directory のファイルの差分の変更が表示されています.

    つまり B の Working Directory のファイルの状態と異なるので modified されたものとして, その差分が表示されています.

    --soft は Working Directory と Index の両方の状態を保持するのでソフトと名付けられました.

    次に紹介する --hard というオプションは Working Directory と Index の両方を状態を保持せず, リセット先のコミットの Working Directory と Idnex の状態に変更します.

    このように --hard でリセットすると --soft と異なり何も引き継がないでコミットヒストリを遡るので復元がきかず厳格ということでハードと名付けられているのでしょう.

    なので --mixed というのは --soft--hard を足して 2 で割ったように Working Directory の内容は保持するけれども Index の内容は保持しないということでミックスと名付けられているのでしょうね.

  3. git --hard HEAD~2

    --mixed のときに説明したことと繰り返しになりまずが --hard はリセット前の Working Directory と Index の内容を引き継がないで指定された古いコミットに遡ります.

    このコマンドの場合は D の Working Directory と Index の状態を B に引き継がないということです.

    なので一度このコマンドを実行してから git status を打つと:

    On branch master
    nothing to commit, working tree clean
    

    このように表示され D のファイル情報が Working Directory と Index のどちらにもないので D の状態を復元する余地がありません.

    完全に Working Directory, Index 共に HEAD と同じ B の状態になっています.

    --soft--mixed と違い, もう D の時の情報が残されていないので不可逆的な操作ということです.

    ただ完全に不可逆的かというと実はそういうこともありません.

    git reflog というコマンドを使ってリセットする前のコミット ID を確認してそのコミット ID を git reset --hard <commit id> に渡して実行するとリセットする前の状態に戻すことができます.

    ただ基本的にはやり直しがきかない不可逆的な操作として --hard オプシヨンを reset コマンドで使うときは慎重に使われることをお勧めします.

    なんたってハードですからね.

--soft, --mixed, --hard のそれぞれの違いを説明させていただきました.

それぞれ Working Directory と Index のファイルの状態が変更されるかされないかということを, 変更されるは Yes, 変更されないは No として表にまとめると次のようになります:

Option Working Directory Index
--soft No No
--mixed No Yes
--hard Yes Yes

これら 3 つのオプションはこのように具体的に挙動が異なりますので, それぞれ状況に応じて使い分けていただければと思います.

最後までお読みくださいましてありがとうございました.