See the Elephant

1992生まれのプログラマが書くエンジニアブログ

git revertについて

git revertを理解する上で必要な知識

前回, 前々回でgit reset, git rebaseについて触れた.
ここからはgit resetを理解してることを前提に話を進めていく.
参考ページ Git チュートリアル: 変更を元に戻す | アトラシアン

git revertの概要

git revertは, コミットを打ち消すような動作をする.
コミットの編集履歴を辿り, 前回コミットで変更された部分(差分)のみを"打ち消す"ようなコミットを行う.

git reset と git revertの違い

例として, 前回コミットした内容に間違いが含まれていたため,
前回のコミットを取り消して, 改めてコミットしたい場合を考える.

# コミット A,B,Cがあり, Cに間違いが含まれている. 
# そこで, Cを取り消して, 改めてコミットを行いたい. 

A -> B -> C (間違いを含むコミット) 
          ↑
         HEAD 

これを実現する方法は, 2つある.
それがgit resetgit revertである.

git resetを使った方法

git reset怖くないgit reset - namu_r21の日記でも触れたように, working tree, index, headを過去コミットの状態に完全に戻すコマンドである.
つまり, そもそも"コミットなんてなかった"という状態にすることができる.

# git resetでCをなかったことにする
 A -> B -> C (間違いを含むコミット) 
           ↑
          HEAD 
$ git reset --hard HEAD~1
 A -> B # Cなんてなかった 
      ↑ 
     HEAD 

この状態で改めて変更をコミットすると以下のような状態になる.
ここで, 注意すべきは改めてコミットしたものは新しいコミットとして見なされることである.

# git resetでCをなかったことにした後 
A -> B
     ↑
    HEAD 

# 改めてcommit
$ git commit 
A -> B -> D (Cとは異なるコミットとして扱われる.)
          ↑
         HEAD 

git revertを使った方法

対して, git revertは, コミットを打ち消すような動作をする.
前回コミットで変更された部分(差分)のみを"打ち消す".
コミットCはそのまま残し, コミットCによって行われた変更部分を打ち消すコミットを発行する.

# git revertでCで行われた変更を打ち消す
A -> B -> C (間違いを含むコミット)
          ↑
          HEAD 

$ git revert
A -> B -> C -> C' ( Cによって行われた変更を打ち消すコミット) 
               ↑ 
              HEAD 

この時, working treeはコミットBと同じ状態になっている. 改めて, コミットを行う場合は以下のような状態になる.

# git revertでCで行われた変更を打ち消した後, 
A -> B -> C -> C' 
               ↑ 
              HEAD 

# 改めてcommit 
$ git commit 
A -> B -> C -> C' -> D 
                     ↑ 
                    HEAD 

このようにすることで,
git revertによってログを残しながらコミットCを"なかったこと"にすることができる. ここがgit resetgit revertの違いである.

なぜgit revertが必要なのか

個人でgitを利用し, ローカルリポジトリだけで開発を行っている場合はこの2つを混同しても特に問題はない. しかし, リモートリポジトリが関係すると話は別になる.

例えば, ローカルでコミットCまで開発しリモート(origin)にpushしてしまった場合を考える.

# 間違いが含むコミットCをoriginにpushしてしまった. 
A -> B -> C (間違いを含むコミット) 
          ↑ 
         HEAD 
       origin/HEAD 

git resetを使って, コミットCをなかったことにして
改めてコミットを行うと以下のような状態になる.

# 間違いが含むコミットCを``git reset``によってなかったことにして 
# 改めてコミットを行う. 
A -> B ->  ->  D 
         ↑     ↑ 
  origin/HEAD HEAD 

$ git push origin
CONFLICT!

この状態でgit push originを行うと, コンフリクトが起きる.
origin側からすると,

  • Bと繋がっているはずのCコミットがなくなっている
  • Bと繋がっていないはずのコミットDが送られてくる

という状態になり, エラーを吐く.

これをgit revertを使うことで繋がりを守ったまま
コミットCを処理できるため, エラーを吐かない.

# 間違いが含むコミットCを``git revert``によって"打ち消し"て 
# 改めてコミットを行う. 
A -> B -> C -> C' -> D 
          ↑          ↑ 
     origin/HEAD    HEAD 

$ git push origin
A -> B -> C -> C' -> D 
                     ↑ 
                   HEAD 
                origin/HEAD

チーム開発における git revertの有効性と git resetのヤバさ

チームで開発しているリポジトリのログをgit resetで汚してし,それを共有すると全員のリポジトリでコンフリクトが起きる.

なおかつ, logが残らないので原因追及ができなくて死ぬ.

チーム開発するときは,

  • リモートにpush後のコミットを訂正する際は, git revertを使う( 不用意にgit resetしない)
  • 間違っても, git resetでログをめちゃくちゃにした後にリモートリポジトリに反映しない
  • pushする前に本当にpushしても問題ないかを確認する.

まとめ

  • git revertは,コミットを残しつつコミットの変更を打ち消す.
  • git resetは, コミットそのものを無かったことにする.