あまり知られていない便利なGitサブコマンド4選

## この記事について

Livesense(その2) Advent Calendar7日目の記事です。
6日目は[I/OスケジューラがMySQLにどの程度影響するのか確認してみる](http://qiita.com/hideji2/items/384598b517dacad2e885)と、[Simplenote でブックマークレットで GitHub Flavored Markdown (GFM)](http://qiita.com/stackline/items/dc13d4902d1ce5226540)でした。

## はじめに

Git便利ですよね。

触り始めた当初は、`git status`、`git add`、`git commit`、`git log`くらいしか使ってませんでした。
いろんなサブコマンドに興味を持ち始めたのは、`git rebase`を知ってからだと思います。
CVSやSubversionからGitに移った私としては、`git rebase -i`により気軽にコミットの順番を入れ替えたり、メッセージを変更できることに衝撃を受けた記憶があります。

今回は、あまり知られてないけど(個人調べ)、知っておくと何かと役立つサブコマンド(+オプション)をご紹介します。

## 便利なGitサブコマンド4選

- `git reflog`
- `git log -S/-G`
- `git alias`
- `git name-rev`

### git reflog

Gitのいわゆる操作履歴を時系列に表示してくれるコマンドです。
操作とはcommitはもちろん、他ブランチのcheckout、rebase、mergeなどを含みます。

とても便利ですが、rebaseやblame、stashなどに比べてあまり広く知られていない気がします。
通常作業時に使うことはほとんどないですが、ミスをしてしまったときに役立ちます。

たとえば、developブランチで開発していて、いくつかコミットしているとします。
極端な例ですが、`git reset --hard orgin/develop`を間違えて叩いてしまい、青ざめてしまったり。
`git log`で確認しても、無情にもコミットが消えています。

またイチから作るのも面倒臭い、なんとか復旧はできないものでしょうか。
こんなときに`git reflog`です。

さっそく叩いてみましょう。

```bash
$ git reflog --date=local -n 10
b51fc6a HEAD@{Mon Dec 7 08:29:10 2015}: reset: moving to origin/develop
787dedf HEAD@{Mon Dec 7 08:28:52 2015}: commit: 機能2
b51fc6a HEAD@{Mon Dec 7 08:27:45 2015}: commit: 機能1
5da05c3 HEAD@{Mon Dec 7 08:20:58 2015}: checkout: moving from master to develop
```

おお、見覚えのあるコミットログが。
戻したい時点のハッシュ値を確認し、resetしてみましょう。

```bash
$ git reset --hard 787dedf
```

これで元どおりです。
つまり履歴上表示されなくてもオブジェクト自体は残っているということですね。
.gitディクトリを覗くと、実際のオブジェクトを確認できます。

```bash
# .git/objects/(ハッシュ値の先頭2桁)/(ハッシュ値残り)
$ ls .git/objects/78/7dedf4532fddf57acdde17a615c9c395ed63ed
```

blob、tree、objectsなどGitの内部的な仕組みは[Gitの.git/objectsの中身を追ってみる](http://d.hatena.ne.jp/kk_Ataka/20131214/1386947348)がとてもわかりやすいです。

このコマンドを知ってから、rebaseやmergeなどのGit操作が大胆に行えるようになった気がします。
なにかあれば、任意の時点まで戻せるという安心感からでしょうか。

便利な`git reflog`ですが、デフォルトの保持期間は90日間のようです。`git gc`によって削除されます。
この挙動を変えたい場合は、`git config`でgc.reflogexpireをneverなどに設定することができるようです(未検証)

https://git-scm.com/docs/git-gc
> The optional configuration variable gc.reflogExpire can be set to indicate how long historical entries within each branch’s reflog should remain available in this repository. The setting is expressed as a length of time, for example 90 days or 3 months. It defaults to 90 days.


補足ですが、コミット自体ではなく、一部のファイルだけ戻したいケースもあると思います。
そのときは`git show --stat 787dedf`でファイルを調べて、
`git checkout 787dedf -- hoge1.rb hoge2.rb`とするだけです。

### git log

現在のコード(ワークツリー)を対象に検索する場合、`git grep`が使えます。
では過去のコミットの**差分**に対して検索する場合はどうすればいいでしょうか。

例えば、hogehogeというメソッドがあるコミットで消されており、それによりメソッドが見つからずエラーが出てしまったとします。

まずは不具合を引き起こしてしまったコミットを特定したいですよね。
そんなときは`git log -S --pickaxe-regex`や`git log -G`が使えます。

#### -S --pickaxe-regex

-S単体では文字列検索、--pickaxe-regexをつけると正規表現による検索になります。
さっそく使ってみましょう。

```bash
$ git log -S hogehoge -p
commit 93de1b1021c974b9dd5ceee7166698af902cf34c
Author: hoge <hoge@gmail.com>
Date:   Sun Dec 6 14:06:07 2015 +0100
--- a/hoge.rb
+++ b/hoge.rb
@@ -37,9 +37,7 @@ EOT
-      def hogehoge
```

差分の中から検索できてますね。93de1b1021c97で不具合が紛れ込んでしまったようです。
ノイズが多い場合は-diff-filterや、--sinceと--after指定で期間を絞ってもいいかもしれません。


#### -G

-Gは-Sとほぼ同様にコミットの差分を検索してくれます。
違いは`git log --help`に記載されています。

```
+    return !regexec(regexp, two->ptr, 1, &regmatch, 0);
...
-    hit = !regexec(regexp, mf2.ptr, 1, &regmatch, 0);
```

> While git log -G"regexec\(regexp" will show this commit, git log -S"regexec\(regexp" --pickaxe-regex will not (because the number of occurrences of that string did not change)

-Gは指定した正規表現が差分の中に含まれていたら対象として表示しますが、-Sはそれだけではなく、**出現回数の差分**も見て判断しています。上記の場合はregexec(regexpという文字列が1つのコミットの中に+と-があり、差し引きゼロのため表示されません。

具体的な例としては、リファクタリングでメソッドを移動したときなどは-Sだと表示されません。
hogehogeというメソッドが消えたコミットを特定したいという今回の目的に対しては、ノイズが減るため-Sの方が適していそうです。

### git alias

すいません、現状このようなコマンドはありません。
ただ、Gitにはエイリアスを定義する機能があり、下記のようにgitconfigファイルに設定すると他のサブコマンドと同様に実行することができます。


```:~/.gitconfig
[alias]
  alias = confg --get-regexp \"alias.*\"
  br = branch
  bra = branch -a -vvv
  co = checkout
  ls = ls-files
  cm commit -m
  tree log -n 15 --graph --pretty='format:%C(yellow)%h%Creset %C(green)%cd%Creset %s %C(cyan)(%an)%Creset %Cred%d%Creset'
  log1 log -n 1
  log3 log -n 3
  ss stash save
  sp stash pop
  sl stash list
  ref = reflog --date=local -n 10
```

これで、git aliasと叩いた結果はこのようになります。たくさん定義していて、忘れてしまってもこれで一目瞭然ですね。

```bash
$ git alias
alias.st status
alias.br branch
‥
$ git co HEAD
$ git ref -n 30 #このようにエイリアスで指定したオプションを上書きもできる
```

### git name-rev

これも影の薄いコマンドですが、役立つ場面が結構あります。
ハッシュ値を渡すとhuman-readableな形式で教えてくれます。


```bash
$ git name-rev 1ee87e4
1ee87e4 master~4^2
```

1ee87e4はmasterブランチから数えて、4つ前のコミットの2つ目の親を表しています。
そのコミットが現状どのような状態か、さっと確認するときに便利ですね。

補足ですが、あるコミットがどのブランチに属しているかはbranchコマンドでわかります。

```bash
$ git branch --contains 1ee87e4
* master
```

## さいごに

以上、他のエンジニアと話してて、あまり知られてないと感じたコマンドを紹介しました。

Gitもバージョンがあがるにつれ、ますます便利になってきています。
今回の記事を書くときに知りましたが、`git rebase --autostash`により、`git pull`する際にいちいち`git stash`する必要がなくなりました。
2.5では同一Gitリポジトリ内に作業ツリーを追加して作成することのできる`git worktree`が追加され、[2.7](https://github.com/git/git/blob/master/Documentation/RelNotes/2.7.0.txt)も絶賛開発中のようです。

これからも、Gitの動向に目が離せそうにありませんね！