Git の Reset, Checkout, Revert の違い


Git で使われる 3 つの主要なコマンド Reset, Checkout, Revert の違いを紹介します. どれも変更を修正したりするために使われる似たような働きをするので, どれををどのような状況で使うべきか混乱しがちです.

git reset, git checkout, git revert これら 3 つのコマンドはそれなりの頻度で使われるものですが, どれも変更を修正したり取り消したりする目的で使われるので, どれをどんな時に使うべきか迷ってしまい少々混乱を招いてしまうかもしれません.

なので今回はそれら 3 つのコマンドの具体的な違いと使いどころを紹介することができればと思います.

まず最初の 2 つのコマンド git reset, git checkout はコミットレベルとファイルレベルで使われる 2 パターンがそれぞれ存在し, git revert はコミットレベルでのみ使われます.

そしてこれらの違いというのは, Working Directory, Staging Area, Git directory という 3 つのセクション内での違いにもなります.

  • Working Directory – ステージされる前のファイルが置かれる場所
  • Staging Area – ステージされたファイルが置かれる場所であり, その場所に置かれたファイルは次のコミットに含まれる
  • Git directory – コミットされたファイルが保存される場所

これらの 3 つのセクション内での違いに留意していただくと, それぞれのコマンドの働きの違いというものを見分けることができるのではないかと思います.

それぞれ詳しく見ていきたいと思います.

Reseting

コミットレベルの場合

git reset がコミットレベルで使われる場合, コミットヒストリを書き換えてしまいますので, 他の人と共有されないプライベートレポジトリで使われることを想定されます.

構文は次のようになり:

git reset [--soft|--mixed|--hard] <commit>

指定された <commit> まで HEAD を移動させ, それまでのコミットを破棄します.

なのでいくつかコミットをしてきて, それらのコミットをなんらかの理由で破棄してしまって, 指定したコミットからやり直したいといった時に使われます.

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

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

CD を破棄して B からやり直したいという場合, 次のようになります:

git reset HEAD~2

HEAD~2HEAD の親の親 B を指定しています.

するとこのようになります:

     HEAD
      |
      v
     master
      |
      v
A --- B

今回は --soft, --mixed, --hard いずれかのオプションを付け加えなかったので, デフォルトの --mixed が暗黙的に付け加えられます.

--mixed は Working Directory は D の状態のまま維持しますが, Staging Area は C の状態に変更します.

それぞれのオプションの違いは次のようになります:

Working Directory Staging Area
--soft Keeped Keeped
--mixed Keeped Changed
--hard Changed Changed

状態が保持される場合は Keeped, 変更される場合は Changed とします.

より詳しい違いは git reset –soft, –mixed, –hard の違い を参照していただければと思います.

ファイルレベルの場合

git reset がファイルレベルで使われると, コミットレベルで使われる場合とうって変わり, プライベートレポジトリでも使うことができるようになります.

また --soft, --mixed, --hard といったオプションを付け加えることができなくなります.

構文は次のようになり:

git reset <commit> <file>

指定した <commit><file> を現在の Staging Area に持ってきます.

ステージしたファイルをアンステージする場合

よく使われる場面では Working Directory で変更したファイルを Staging Area にステージして, やっぱりアンステージするという場合です.

例えば README.md というファイルをステージしてから git status で現在の状態を確認すると次のようなメッセージが表示されます:

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

        modified:   README.md

そのメッセージの次の行に書かれているように:

  (use "git reset HEAD <file>..." to unstage)

git reset HEAD <file>... という構文を使ってアンステージすることができます.

なので README.md をアンステージする場合, 次のようになります:

git reset HEAD README.md

そして 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

Staging Area からアンステージされてしまった README.md はその居場所を Working Directory に押し戻されてしまいました.

このように git reset がファイルレベルで使われる場合, 1 つの使われ方として, ステージしたファイルをやっぱりアンステージするという使われ方があります.

前のコミットのファイルを Staging Area に持ってくる

ファイルレベルの git reset のもう一つの使われ方として, 前のコミットのファイルを Staging Area に持ってくるというものがあります.

例えば現在の Working Directory と Staging Area が次のようにクリーンで:

$ git status
On branch master
nothing to commit, working tree clean

HEAD^ から README.md を持ってくる場合, 次のようになります:

git reset HEAD^ README.md

git status で確認すると次のように表示されます:

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

        modified:   README.md

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

なぜこのようになるのかというのは, あくまで現在の Staging Area のみ変更したからです.

Working Directory の README.md は元のままですが, Staging Area に持ってこられた README.md と内容が異なるので Working Directory の README.mdmodified (修正された) となり, その Staging Area に持ってこられた README.md も HEAD と内容が異なるので modified となったわけです.

なのでこの状態で README.md をアンステージし:

git reset HEAD README.md

git status で確認すると次のようになります:

On branch master
nothing to commit, working tree clean

ちょっと驚かれるかもしれないですが, もともと変更されていたのは Staging Area だけで, その Staging Area を HEAD と同じ状態に戻したのでこのようになります.

このように前のコミットのファイルを好きなように現在の Staging Area に持ってくることができます.

あのコミットのあのファイルを Staging Area に持ってきたいという時に使えます.

Staging Area に持ってきたファイルはそのままコミットすることによって次の HEAD のコミットとなるので, 今まで存在していたコミットヒストリを書き換えてしまうこともなく, 共有のレポジトリでも気兼ねなく使うことができます.

このように同じ git reset でもコミットレベルとファイルレベルで随分働きが変わってきます.

Checking Out

コミットレベルの場合

git checkout をコミットレベルで使う場合, HEAD を指定したコミットやブランチに移動させることができます.

そして Working Directory を移動先の HEAD のものに変更します.

構文は次のようになります:

git checkout <commit-or-branch>

指定したコミットに HEAD を移動させる

次のようなコミットをしてきたとして:

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

次のようにすると:

git checkout HEAD~2

次のようになります:

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

Working Directory の状態が B に変更され, B のファイルを調べることができます.

また HEADmaster から離れ B に移動したことによって ‘detached HEAD’ という状態になりました.

この状態だと実験的なコミットを作って破棄したり, 新しくブランチを作って, そこから作る新たなコミットを保持することもできます.

指定したブランチに HEAD を移動させる

HEADHEAD~2 といったコミットではく, master などとブランチを指定するとその指定したブランチに HEAD を移動させることができます.

そしてコミットを指定した時と同じように Working Directory を指定したブランチのものに変更します.

例えば master ブランチに HEAD を移動させる場合は次のようになります:

git checkout master

ファイルレベルの場合

git checkout をファイルレベルで使う場合, 構文は次のようになり:

git checkout <commit> <file>...

コミットレベルの場合と異なりファイルレベルの checkoutHEAD が移動しません.

指定した <commit> の指定した <file> を現在の Working Directory と Staging Area に持ってきます.

例えば次のような場合:

                      A (v1: README.md)
                      |
                      |
                      B (v2: README.md)
                      |
                      |
                      C (v3: README.md)
                      |
                      |
    HEAD -> master -> D (v4: README.md)

コミット A から D にかけて README.md というファイルのバージョンを v1 から v4 まで修正したとします.

現在の HEADD で Working Directory, Staging Area, HEAD 共に同一でクリーンな状態とします:

$ git status
On branch master
nothing to commit, working tree clean

そして Bv2README.md を現在の D の Working Directory と Staging Area に持ってきたいという場合, 次のようになります:

git checkout HEAD~2 README.md

そして git status で確認すると, このようになります:

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

        modified:   README.md

このように BREADME.mdD の Working Directory と Staging Area に持ってくることができました.

git reset のファイルレベルも同じような働きをしますが, そっちの方は Staging Area のみ変更し, git checkout のファイルレベルは Staging Area に加えて Working Directory も変更します.

Reverting

コミットレベルの場合

git revert はコミットレベルでのみ使われます.

構文は次のようになり:

git revert <commit>

指定された <commit> の内容を取り消すコミットを新たに作ります.

もし新たなコミットが作られる時にコンフリクトが発生した場合, マージのコンフリクトが発生した時のように自分で手直ししてあげる必要があります.

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

                      A (v1: README.md)
                      |
                      |
                      B (v2: README.md)
                      |
                      |
    HEAD -> master -> C (v3: README.md)

A から C までのコミットで README.md を修正してきたとします.

そして C のコミットをなんらかの理由で取り消してしまいたい場合, 次のコマンドを打つと:

git revert HEAD

このようになり:

                      A (v1: README.md)
                      |
                      |
                      B (v2: README.md)
                      |
                      |
                      C (v3: README.md)
                      |
                      |
    HEAD -> master -> D (v2: README.md)

Cv3README.md の修正を取り消して, Bv2README.md と同じ内容のコミット D が新たに作られます.

このように指定したコミットの内容のみを新たなコミットによって, 実質的に取り消すことができます.

git reset と異なり, このコミットを取り消しましたという新たなコミットを作るので, ちゃんと記録に残るため共有のレポジトリでも使うことができます.

また git reset は指定したコミットに遡るまでの一連のコミットを破棄し, 完全にその時の状態に戻るというのに対し, git revert は指定したコミットのみ元に戻すので, よりピンポイントにコミットを破棄することができます.

いくつかコミットをしてきて, いくつか前のコミットに不具合が見つかり, そのコミットのみ取り消したいという時に使えます.

まとめ

git reset, git checkout, git revert それぞれコミットレベルで使われる場合と, ファイルレベルで使われる場合とで働きが変わってきます.

それぞれコミットやファイルの内容を修正したり, 取り消したりする目的で使われ, 似たような働きをするので, どれをどんな時に使うんだっけとやや混乱を招きがちかと思いますが, そのような混乱を少しでも取り除くことができましたら幸いです.

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