図解Gitコミットの修正まとめ git reset, cherry pick, revert, commit --amend, rebase 

オライリー・ジャパン『実用Git』を最近読み終えたので、
コミットの修正方法について、簡単に実例を交えてつらつらとまとめておく。

過去にマージについて「[Gitマージの基本](http://qiita.com/LOUIS_rui/items/8e66f6f8f1a536830081)」としてまとめたので興味があれば参考まで。

#コミットの修正
SVNと違ってGitではローカル環境にレポジトリを保有するので、未公開のコミットをローカルレポジトリに対して行う事ができる。おそらく、SVNの時代と違ってこまめにコミットしたりする事が増えるので、コミットコメントの粒度が細か過ぎてわかりにくくなっていたり、コメントにタイポがあったりと、共同開発者を混乱させるようなコミットログを残してしまう可能性がある。そこで、Gitで開発するのであれば、適切なコミットログを残せるよう心がけるとともに、未公開分のコミットの修正方法を学んでおく必要がある。（公開後のコミットは、共同開発者の混乱を招きかねないので原則変更しない方が良い）

##git reset
HEADの参照を指定されたコミットに変更し、インデックスや作業ディレクトリの状態も変更する事ができる。git resetのオプションに--hard、--soft、--mixed（デフォルトオプション）があり、それぞれ使い方が異なるので覚えておく。個人的にはgit resetは良く使うので重点的に説明する。

とりあえず以下に示す通りGitレポジトリを作成して、このレポジトリを参考に各オプションの説明をして行く。

```bash
$ mkdir git-reset-test
$ cd git-reset-test/
$ git init
$ echo A > A; git add A; git commit -m "add A" A
$ echo B > B; git add B; git commit -m "add B" B
$ echo C > C; git add C;
$ echo D > D; 
# indexにあるファイルを確認（ABCはindexにある）
$ git ls-files 
A
B
C
$ git status （Cは更新のあったindexファイル、Dは未追跡）
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   C

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	D
```
この状態を図に示してみる。◯で示したのをcommit、△をindex、□を作業ディレクトリとした。最新のコミット(HEAD)は親コミット(HEAD^)を参照している。

![git.jpg](https://qiita-image-store.s3.amazonaws.com/0/50185/8066da76-c0f0-1515-2be1-c4a9e2a5342a.jpeg)

以下に、git reset --${いずれかのオプション} HEAD^を実行した結果を解説する。

###git reset --soft
HEADが指し示すコミットのみ移動する。indexはHEADが指し示すindexと比較してB,Cが新たに作られているので、次のコミットで変更されるものとしてマークされる。ちなみにこれはHEADを戻して、コミットコメント等を修正してコミットしたい場合等に使う。

```bash
$ ls
A	B	C	D
$ git ls-files
A
B
C
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   B
	new file:   C

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	D

```
![git (1).jpg](https://qiita-image-store.s3.amazonaws.com/0/50185/1a354686-0910-880c-3d31-7d1c8bb13822.jpeg)

###git reset --mixed
この--mixedは指定せずとも、git resetのデフォルトの挙動がmixedになっている。HEADが指し示すコミットを移動する。indexは移動後のHEADが指し示すindexに一致させ、移動後のHEADの後にindexされたB,Cをuntrackとしてindexから外す。また、例えばAを編集していた場合は次回コミットで変更される者としてマークされる。コミットをやり直して、なにかしらの変更を加えたい場合にこのオプションを実行する。（--softの場合は既にindexされているファイルはそのままコミット出来るのでコミットコメント等の修正のみに適したオプションであるといえる）

```bash
$ git reset --mixed HEAD^
$ ls
A	B	C	D
$ git ls-files
A
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	B
	C
	D

nothing added to commit but untracked files present (use "git add" to track)
```

![git (2).jpg](https://qiita-image-store.s3.amazonaws.com/0/50185/8092cc0e-d68a-66e2-4d63-b132d824dea3.jpeg)

###git reset --hard
HEADが指し示すコミットを移動する。indexは移動後のHEADが指し示すindexに一致させ、移動後のHEADの後にindexされたB,Cは削除される。ただ、untrackedのファイルのみは残される。このオプションはとにかく、作業ディレクトリを前の状態に戻したい時に使う。ただし、untrackedのファイルは残ってしまうので、僕はgit reset --hardと一緒に、git clean -fdx を使用します。これは追跡対象外ファイル・ディレクトリ等を削除してくれます。

```bash
$ git reset --hard HEAD^
HEAD is now at 918d4b7 add A
$ ls
A	D
$ git ls-files
A
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	D

nothing added to commit but untracked files present (use "git add" to track)
```

![git (3).jpg](https://qiita-image-store.s3.amazonaws.com/0/50185/06565e00-9cbf-1f0b-6491-60baeb527d82.jpeg)

##git cherry pick
別ブランチのコミットを指定して、そのコミットで変更された部分をカレントブランチに取り込むといった事が出来る。

```bash
#初めにmasterでodd(奇数),even(偶数)のファイルを作成後、subブランチを作り、
#evenに偶数加算し、最後にodd,evenの順でファイルを削除した
$ git show-branch
* [master] odd,even
 ! [sub] delete odd
--
 + [sub] delete odd
 + [sub^] delete even
 + [sub~2] even add 6
 + [sub~3] even add 4
*+ [master] odd,even
#subブランチのevenのファイルの推移
$ git cat-file sub~2:even -p
2
4
6
$ git cat-file sub~3:even -p
2
4
$ git cat-file sub~4:even -p
2
#masterのeven
$ git cat-file master:even -p
2
#masterのevenにsub~3を取り込んだ結果
$ git cherry-pick sub~3
$ git cat-file master:even -p
2
4
$ git show-branch
* [master] even add 4
 ! [sub] delete odd
--
*  [master] even add 4
 + [sub] delete odd
 + [sub^] delete even
 + [sub~2] even add 6
 + [sub~3] even add 4
*+ [master^] odd,even
#一旦戻して
git reset --hard HEAD^
$ git reset --hard HEAD^
HEAD is now at f19df97 odd,even
$ git show-branch
* [master] odd,even
 ! [sub] delete odd
--
 + [sub] delete odd
 + [sub^] delete even
 + [sub~2] even add 6
 + [sub~3] even add 4
*+ [master] odd,even
#sub~2を取り込むと、、、
$ git cherry-pick sub~2
error: could not apply 9c329d8... even add 6
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit
$ cat even 
2
<<<<<<< HEAD
=======
4
6
>>>>>>> 9c329d8... even add 6
```
上記のとおり、cherry-pickで差分を取り込もうとしたファイルが、取り込み先のブランチでも更新されていると競合が発生する可能性がある。

```bash
#修正して
$ cat even 
2
6
$ git add even 
$ git commit -m "even add 6"
#evenを削除したコミットをcherry-pickすると、、
$ git cherry-pick sub^
error: could not apply bf08237... delete even
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
#その前のコミットでevenをいじってたのでコンフリクトが発生
#結局手作業で削除してコミット
$ git rm even
$ git commit -m "delete even"
#oddのファイルを削除するコミットはoddを何も触っていないので問題なく削除コミットを取り込める
$ git cherry-pick sub
```

##git revert
revertは過去のコミットを指定して、そのコミットを打ち消す新たなコミットを行う。打ち消されるコミット以降、同一ファイルを更新している場合は競合が発生する場合がある。

```bash
$ mkdir git-revert-test
$ cd git-rvert-test/
$ git init
$ echo 1 >> A
$ git add A
$ git commit -m "add A" A
$ echo 1 >> B
$ git add B
$ git commit -m "add B" B
$ echo 2 >> A
$ git commit -m "add A 2" A
$ echo 2 >> B
$ git commit -m "add B 2" B
$ echo 3 >> A
$ git commit -m "add A 3" A
$ git log --pretty=oneline
ea2e16ce7f0bb118b859f4adcda634a146f587cb add A 3
9678c14e73c6757be50005210269cb2320d318a4 add B 2
237073b25ec4f99abf92761a0696db249bea180b add A 2
47f6423a27fde8595e1f29328d7c2f94658cc25f add B
ee214b90e590b87c99b825f3d1dc2b81e52c9947 add A
$ cat A
1
2
3
$ cat B
1
2
#この状態で「add B 2」のコミットを打ち消す
$ git revert 9678c14e73c6757be50005210269cb2320d318a4
[master 49ffeca] Revert "add B 2"
 1 file changed, 1 deletion(-)
$ cat B
1
$ git log --pretty=oneline
49ffecaf23e915e553a83911be7b46a165eef7db Revert "add B 2"
ea2e16ce7f0bb118b859f4adcda634a146f587cb add A 3
9678c14e73c6757be50005210269cb2320d318a4 add B 2
237073b25ec4f99abf92761a0696db249bea180b add A 2
47f6423a27fde8595e1f29328d7c2f94658cc25f add B
ee214b90e590b87c99b825f3d1dc2b81e52c9947 add A
#問題なく「add B 2」のコミットは打ち消されている。次に「add A 2」を打ち消す
$ git revert 237073b25ec4f99abf92761a0696db249bea180b
error: could not revert 237073b... add A 2
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
#conflict発生
$ cat A
1
<<<<<<< HEAD
2
3
=======
>>>>>>> parent of 237073b... add A 2
# 修正してコミット
$ vi A
$ cat A
1
3
$ git add A
#エディタでコメント編集してコミット
$ git commit 
[master d520e73] Revert "add A 2"
 1 file changed, 1 deletion(-)
$ git log --pretty=oneline
d520e736ecf19ffcdc96ac2cee20c204a2877f33 Revert "add A 2"
49ffecaf23e915e553a83911be7b46a165eef7db Revert "add B 2"
ea2e16ce7f0bb118b859f4adcda634a146f587cb add A 3
9678c14e73c6757be50005210269cb2320d318a4 add B 2
237073b25ec4f99abf92761a0696db249bea180b add A 2
47f6423a27fde8595e1f29328d7c2f94658cc25f add B
ee214b90e590b87c99b825f3d1dc2b81e52c9947 add A
```

##git commit --amend
前回のコミットをもう一度やり直したい場合に使用する。コミットコメント等の編集や、前回のコミットに新たな更新を含める（または修正する）事が出来る。

```
$ mkdir git-amend-test
$ cd git-amend-test/
$ git init
$ echo 1 >> A
$ git add A
$ git commit -m "add A" A
$ echo 2 >> A
$ git commit -m "add A 2" A
$ git log --pretty=oneline
82b0e773706e54eed45c490870382959a75357bf add A 2
ef21af5fbc63a127b9e25cd6be5f4ce0bc87069b add A
$ cat A
1
2
$ git log --pretty=oneline
dbba0b46f0752b6b063a780e601c357888133169 add A 2,3
ef21af5fbc63a127b9e25cd6be5f4ce0bc87069b add A
$ cat A
1
2
3
```

##git rebase
ブランチの基点(交差点)の再配置を行う際に使用する。
masterとsubブランチで開発しているときに、masterの更新をsubブランチに取り込む時にrebaseしたりする。

```
$ mkdir git-rebase-test
$ cd git-rebase-test/
$ git init 
$ echo a > a; git add a; git commit -m "add a from master"
$ git checkout -b sub
$ echo b > b; git add b; git commit -m "add b from sub"
$ echo c > c; git add c; git commit -m "add c from sub"
#masterに戻る
$ git checkout - 
$ echo d > d; git add d; git commit -m "add d from master"
$ git show-branch --sha1-name
* [master] add d from master
 ! [sub] add c from sub
--
*  [8ff37fd] add d from master
 + [0f33c95] add c from sub
 + [5ffa949] add b from sub
*+ [eecf948] add a from master
#rebaseでsubをmasterに前方移植する
$ git rebase master sub
$ git show-branch
! [master] add d from master
 * [sub] add c from sub
--
 * [2af2b2a] add c from sub
 * [08e98cc] add b from sub
+* [8ff37fd] add d from master
```
この例ではsubをmasterに前方移植したが、元々subにあったコミット「add c from sub」「add b from sub」のコミットは新たなものとして行われ、コミットのリビジョンも変更される。ただし、移植する際に、旧基点から新基点の間で更新したファイルと、旧基点から切り離され、新基点に追加されて行くコミット群で同じファイルを編集していた場合は競合が発生する場合があるので注意。

###git rebase -i
git rebase -iオプションを使ってブランチ上のコミットの修正をインタラクティブに行う事が出来る。

```
$ mkdir git-rebase-test
$ cd git-rebase-test/
$ git init
$ echo a > a; git add a; git commit -m "add a"
$ echo b > b; git add b; git commit -m "add b"
$ echo c > c; git add c; git commit -m "add c"

$ git log --pretty=oneline
1dd5901c2e000a48a1bf5195d15c05935c23b2a0 add c
0985a341471045ce43a2739ce7c3842eaa41d942 add b
ca791d73a0c8c75d10b6f8c0ab9c294640dc88f2 add a

#並び替え
git rebase -i HEAD~2

#ここでadd bとadd cの順序を入れ替えて保存
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
pick 0985a34 add b
pick 1dd5901 add c

# Rebase ca791d7..1dd5901 onto ca791d7
#
# 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
#
# 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
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

$ git log --pretty=oneline
f9a0aabca574cf82d30b14501946cff22aae4499 add b
b9ae7bb85e984d508d6b59407c575a5c4add8bf2 add c
ca791d73a0c8c75d10b6f8c0ab9c294640dc88f2 add a

#コミットをまとめる(squash)
git rebase -i HEAD~2

#add cのpickをsquashに変更して保存
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
pick 0985a34 add b
pick 1dd5901 add c 

# Rebase ca791d7..1dd5901 onto ca791d7
#
# 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
#
# 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
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

#この後にコミットコメントどうするか聞かれる（今回はそのまま保存してみる）
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# This is a combination of 2 commits.
# The first commit's message is:

add b

# This is the 2nd commit message:

add c

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto ca791d7
# You are currently editing a commit while rebasing branch 'master' on 'ca791d7'.
#
# Changes to be committed:
#       new file:   b
#       new file:   c
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

#2つのコミットがまとめられている
$ git log
commit dbf9ef716d6325eb6060977c355f7c6ff1ee41c8
Author: hoge <hoge@fuga.co.jp>
Date:   Wed Feb 11 12:02:15 2015 +0900

    add b
    
    add c

commit ca791d73a0c8c75d10b6f8c0ab9c294640dc88f2
Author: hoge <fuga@cyberagent.co.jp>
Date:   Wed Feb 11 12:02:05 2015 +0900

    add a
```