Git を使用していると特定のコミットを指定したいことが多々あります。
例えば:
git diff)、git show)、git log -p)、git reset 等)git rebase -i 等)git rebase -i 等)等々です。
この時、一番確実なのは git log でコミットのIDを調べて、そのIDで指定することです。
ただ、これは確実ではあるものの、40文字もの英数字の羅列をコピーするのは面倒です。
どうにかして簡単に指定できないものでしょうか。
HEAD で指定できます。
これ単品だと面白くありませんが、他の記法と組み合わせることで真価を発揮します。
単品で意味がある例としては以下の通りです:
git diff HEAD で git add された変更点とされていない変更点をまとめて確認できます(git commit -a する際に便利)。git reset HEAD で git add された変更点をなかったことにできます。ブランチ名を書くとそのブランチの最新のコミットを指定したことになります。
例えば:
git log topic で topic ブランチの変更履歴を確認したり、git diff master topic で master と topic の内容の差を確認したりできます。<rev>~<n> で指定できます。例えば
HEAD~1 で HEAD の親(1つ前のコミット)を、HEAD~3 で HEAD の親の親の親(3つ前のコミット)を表すことができます。応用例としては:
git reset --hard HEAD~3 で最後に行った3コミットをなかったことにしたり、git rebase -i HEAD~5 で最後に行った5コミットの順序を入れ替えたり編集したりアレコレできます。余談:git diff -b でインデント量以外の変更点を表示できます。
ところがこの -b は git diff でしか使えないものだと思い込んでいて、
わざわざ git diff -b HEAD~1 HEAD を使っていた人が筆者の身の回りにいました。git diff のオプションは差分を表示する他のコマンドでも利用できるので、
この場合は git show -b HEAD を使った方が簡単です。
マージの結果できたコミットの親コミットは2個以上存在します。
この場合、 <rev>^<n> で「<rev> の <n> 個目の親コミット」を指定できます。
例えば以下のような状態を仮定すると:
$ git checkout master
$ git merge topic-a
$ git merge topic-b
$ git log --oneline --graph
* 0000001 (master) A
|\
| * 0000002 (topic-b) B
| * 0000003 C
* \ 0000004 (topic-a) D
|\ |
| * | 0000005 E
| * | 0000006 F
: : :HEAD は A を、HEAD^1 は D を、HEAD^2 は B に相当します。<rev>~<n> に比べると <rev>^<n> を活用する機会はそう多くありません。
筆者の個人的な経験で言えば、以下のような事態が発生したときに、git log の結果をにらめっこしてIDのコピーをしなくて済むので便利です。
git branch -d topic-b で不要になったトピックブランチを削除した。topic-b に修正漏れがあったことに気付いた。git branch topic-b HEAD^2 で topic-b を復活。git reset --hard ORIG_HEAD で topic-b のマージを取り消し。topic-b で適宜修正を行って master へ再度マージ。なお、 <rev>~<n> は <rev> に ^1 を n 個付けたものに相当します。
git reset を行う前のコミットを指定する(1)ORIG_HEAD で指定できます。
git merge や git reset 等、HEAD を大幅に変更するような操作を行った場合、
その操作を行う前のコミットが ORIG_HEAD に記録されています。
git reset --hard ORIG_HEAD で git merge や git reset を行う前の状態に戻せます。git reset を行う前のコミットを指定する(2)git reset は便利な反面、
ブランチの指す先を自由に変更できるため、
操作を間違えた際に大変なことになります。
例えば最後に行ったコミットをなかったことにしようと
$ git reset --hard HEAD~1 # (1)を実行するつもりが指が震えて
$ git reset --hard HEAD~11 # (2)を実行してしまい、何を慌てたのかさらに
$ git reset --hard HEAD~111 # (3)を実行してしまったとしましょう。
| (0) | (1) | (2) | (3) | (4)
| | | | |
A | HEAD | ORIG_HEAD | | |
^ | | | | |
| | | | | |
B | | HEAD | ORIG_HEAD | |
^ | | | | |
| | | | | |
C | | | HEAD | ORIG_HEAD | HEAD
^ | | | | |
| | | | | |
D | | | | HEAD | ORIG_HEAD(3) まで実行した段階で git reset --hard ORIG_HEAD をすると (4) のようになり、HEAD は D から C に変わるものの、
今度はこのアンドゥのための git reset で ORIG_HEAD が更新されるので、ORIG_HEAD は C から D に変わります。
つまり、 git reset --hard ORIG_HEAD を2回以上繰り返しても同じ状態を行ったり来たりするだけで、
(2) や (1) の状態には戻せないという訳です。
幸い、 Git には reflog という機構があり、
ブランチに対する変更はある程度まで記録されています。
なので「 HEAD が n 回変更される前のコミット」もその記録から指定することができます。
これは <rev>@{<n>} の形で指定できます。
例えば HEAD@{3} で HEAD が 3 回変更される前のコミットを指定できます。
ですので (3) の状態からは git reset --hard HEAD@{3} で元の状態に戻すことができるという訳です。
なお、 ORIG_HEAD は git merge や git reset 等の大幅に HEAD を変更し得るコマンドでしか更新されませんが、
reflog はありとあらゆるコマンドで更新されます。例えば git commit 等のコマンドでも reflog は更新されます。
普通にコミットしている分には使いませんが、 git commit --amend でガンガン書き換えている場合、git reset HEAD@{1} で「 --amend する前」に戻したいことはあるでしょう。
先述したもの以外にもコミットを指定する様々な記法が存在しますが、
いずれも滅多に使わないものなので、覚えなくても支障はありません。
どうしても気になる場合は以下のリファレンスマニュアル等を参照してください: