Corredor

ウェブ、プログラミングの勉強メモ。

Git Reset・Revert・Rebase を実際に叩いて覚えてみた

git resetgit revertgit rebase といった、過去のコミットを操作するコマンドを実際に叩いて勉強した結果を残す。

git init … お試しブランチを作る

# 適当な作業用ディレクトリを作る
$ mkdir Test
$ cd Test
# Git でのバージョン管理を始める
$ git init
# master ブランチを作成する
$ git checkout -b master

これで適当なディレクトリにローカルブランチを作ることができた。以降はこのローカルブランチ内でアレコレ叩くだけなので、どこにも影響ない。気が済んだら「Test」ディレクトリごと消してしまえばいい。

git reset … 過去のコミットを削除してなかったことにする

# 適当にテキストファイルを作ったり変更したりしてコミット履歴をいくつか作る。

# ローカルブランチでこういう状態だとして、以下を進める。
$ git log --oneline
2297cd1 2017-03-08 Edit 2 [Neos21] (HEAD -> master)
8a15f33 2017-03-08 Edit 1 [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

git reset は過去のコミットを削除し、そのコミットをなかったことにできる。

$ git reset --soft HEAD^

これで1つ前のコミット (= 先程のコミット履歴でいうと「Edit 2」) が削除され、そのコミットの内容が取り消される

--soft オプションを指定しているので、「Edit 2」にコミットしていた分のファイルの変更は保持され、現在のローカルブランチ内に残っている。直後に git status を確認すると、「Edit 2」でコミットしていたはずの内容が差分として見える。

このとき、未コミット状態となるファイルは git add した状態になっているので、このまままた git commit すれば、「Edit 2」と同じ内容を再度コミットできる。

つまり、git reset --soft で直前のコミットを削除すれば、コミットコメントだけ直して再コミットしたり、コミット漏れしていたファイルを追加で git add して再コミットしたり、といった操作ができる。

一方、--hard オプションを付けてやると、戻したコミット時点のファイルたちに書き換えられる。つまり「Edit 2」でコミットしていたファイルの変更は全て破棄され、「Edit 1」をコミットした直後の時点に完全に戻る。

--soft--hard は取り消したコミットの変更内容をステージングに保持するか否かの違いだけで、どちらの場合もコミット自体は削除されるので、以下のようなログになっているはずである。

# git reset したあとは以下のようになっているはず
$ git log --oneline
8a15f33 2017-03-08 Edit 1 [Neos21] (HEAD -> master)
d5dc6fe 2017-03-08 First Commit [Neos21]

別のやり方を紹介。先程と同じ「Edit 2」までコミットしていた状態だとして…。

# またこの状態にしたとして。
$ git log --oneline
2297cd1 2017-03-08 Edit 2 [Neos21] (HEAD -> master)
8a15f33 2017-03-08 Edit 1 [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

# 「Edit 1」のコミット ID を書く
$ git reset --soft 8a15f33

このようにコミット ID を指定してやることでも、「Edit 2」を取り消した同じ状態にできる。「8a15f33」は「Edit 1」のハッシュなので、「Edit 1」のコミット直後の時点まで戻す = 「Edit 2」のコミットを削除する、という動きになる。

「Edit 2」のコミットを消そうとして、「Edit 2」自体のコミット ID を入力してもうまくいかないので注意。「Edit 2」は「HEAD」自体なので、「1つ前のコミット『Edit 1』のコミット ID を指定する」=「『HEAD^』で1つ前のコミットを指定する」と覚えよう。

直前より以前のコミットを指定したらどうなる?

「Edit 2」までのコミットがある状態で、その2つ前の「First Commit」のコミットまで戻すとどうなるか。

# またこの状態にしたとして。
$ git log --oneline
2297cd1 2017-03-08 Edit 2 [Neos21] (HEAD -> master)
8a15f33 2017-03-08 Edit 1 [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

# 以下の2つのコマンドは同じ動きをする

# 2つ前のコミット (=「First Commit」) まで戻す
$ git reset --soft HEAD^^

# 2つ前のコミットのコミット ID (=「First Commit」のコミット ID) を指定してそこまで戻す
$ git reset --soft d5dc6fe

このようにすると、「Edit 2」と「Edit 1」のコミットがなかったことにされている。--soft 指定の時はステージングに「Edit 2」と「Edit 1」でそれぞれコミットしていたファイルが混在した状態になる。

# 2つ前のコミットまで戻すとこんなコミットログになる
$ git log --oneline
d5dc6fe 2017-03-08 First Commit [Neos21]

git reset はココまで。

git revert … リセットしたコミットを残す

git reset はコミット自体が削除されるのに対し、git revert は「あるコミットの内容を取り消して、その前の時点に戻しましたよ」というコミットを新たに作り出す。

「戻したい時点のソースコードの状態に手動で戻して、変更を相殺するためのコミットを打つ」というのと同じことをしてくれる。

特にチーム開発などしている時に、「あのコミットを取りやめた」という履歴を残すために使えるだろう。

git revert --no-commit とすると、「戻したい時点のソースコードの状態に手動で戻して、」という部分を自動的にやってくれる。つまり、相殺する差分だけを作ってくれる。

個人でやっている分にはそう使わないかな?と思って調べただけで特に試さなかった。スンマセン;;

git rebase … コミットログを詳細に変更する

今度は例のために「Edit 3」までのコミット履歴を作った。

# コミット履歴がこんな状態だとして。
$ git log --oneline
657475f 2017-03-08 Edit 3 [Neos21] (HEAD -> master)
ec65417 2017-03-08 Edit 2 [Neos21]
a87e4d1 2017-03-08 Edit 1 [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

git rebase はコマンドライン上で対話形式で過去のコミットを操作できるコミット自体の削除だけでなく、コミット内容の変更やコミットコメントの訂正だけなど、様々な操作ができる。

ここでは、上記のコミットログのうち「Edit 1」のコミット内容を修正し、それ以外のコミットはそのまま保持するといったことをやってみようと思う。

「Edit 1」のコミットを修正するには、その手前のコミット「First Commit」のハッシュを選択する。

# 「First Commit」のハッシュを指定する
$ git rebase -i d5dc6fe

するとテキストエディタ (たいていは Vim) が開き、以下のような内容が表示される。

pick a87e4d1 Edit 1
pick ec65417 Edit 2
pick 657475f Edit 3

# Rebase d5dc6fe..657475f onto d5dc6fe (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

最初の「pick」から始まる3行が、コミットログを操作するための場所で、それ以下はコメントなので無視。この画面では、「どのコミットに何の操作をするのか」をテキスト編集によって指定する

今回は「Edit 1」のコミットコメントを変えようと思うので、以下のように「Edit 1」の「pick」部分を「reword」に書き換える。Vim の操作なので、「a」で挿入モードにして書き換える。

reword a87e4d1 Edit 1
pick ec65417 Edit 2
pick 657475f Edit 3

書き換えたら「Esc:wq」で閉じる。するとまた Vim が起動し、以下のような内容が表示される。

Edit 1

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Wed Mar 8 09:56:57 2017 +0900
#
# interactive rebase in progress; onto d5dc6fe
# Last command done (1 command done):
#    reword a87e4d1 Edit 1
# Next commands to do (2 remaining commands):
#    pick ec65417 Edit 2
#    pick 657475f Edit 3
# You are currently editing a commit while rebasing branch 'master' on 'd5dc6fe'.
#
# Changes to be committed:
#       modified:   Test1.txt
#       new file:   Test2.txt
#

この画面で、「Edit 1」のコミットコメントを書き換えられる。ここでも、# 始まりの行はコメントなので無視して良い。

1行目の「Edit 1」が実際のコミットコメント部分なので、ここを「Edit 1 Rebase!」のように書き換える。改行も入れられると思う。

:wq」で保存終了すると元のターミナルに戻り、以下のように表示されている。

$ git rebase -i d5dc6fe
[detached HEAD 4992001] Edit 1 Rebase!
 Date: Wed Mar 8 09:56:57 2017 +0900
 2 files changed, 4 insertions(+)
 create mode 100644 Test2.txt
Successfully rebased and updated refs/heads/master.

# コミットログを確認する
$ git log --oneline
7110995 2017-03-08 Edit 3 [Neos21] (HEAD -> master)
bbccb70 2017-03-08 Edit 2 [Neos21]
4992001 2017-03-08 Edit 1 Rebase! [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

「Edit 1」だったコミットコメントが「Edit 1 Rebase!」になっている。

ただし、1つ注意点がある。

# 以下は rebase 前のログ。rebase 後のコミットログと見比べてみると…?
657475f 2017-03-08 Edit 3 [Neos21] (HEAD -> master)
ec65417 2017-03-08 Edit 2 [Neos21]
a87e4d1 2017-03-08 Edit 1 [Neos21]
d5dc6fe 2017-03-08 First Commit [Neos21]

git rebase する前のログと比べると、変更した「Edit 1」のコミット以降のハッシュが全て書き変わっている

コミット内容の編集によって「Edit 1」のハッシュが変わり、その後のコミットは手前のコミットとの繋がりの情報を保持しているので、変更された「Edit 1」のハッシュを保持するために後ろのコミットもハッシュが変わる。GitHub などにプッシュしてしまったコミットを後から編集すると、色々と不整合が起こるので、プッシュ済みのコミットの Rebase は避けた方が良い。

以上

実際に操作してみてかなり感覚が掴めた。「こうしたいんだけどどうやるんだろう?」というインデックスは出来た気がする。

参考