Gitのコマンドはなぜ使いにくいのか

[Gitのコマンドは分かり辛く使いにくい](https://svnvsgit.com/#git-leaky-abstraction)。

これに異論を唱える人はあまりいないと思うが、もしいたらその人は多分すでにGitの悟りを開いた人で、この記事を読むメリットは全くないので、無駄な時間を過ごさぬようそっ閉じしてもらえればと思う。

Gitのコマンドが分かり辛いのは、Subversionと違い過ぎるからとか、低レベルな操作を中心に設計されているからとか、オプションが多過ぎるからとか言われるが、この記事では、もう少し具体的で卑近で嵌りがちな個別の3つの点を挙げ、それぞれについて解説したい。

# ① コマンドの引数に何を指定したらいいか分からない
Gitのコマンドの引数は非常に柔軟に指定でき、悟りを開いた人にとっては便利だが、初心者には複雑で分かりにくい。
例えば、`git reset -h`を実行すると以下のようなusageが表示される。

```sh
$ git reset -h
usage: git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]
   or: git reset [-q] <tree-ish> [--] <paths>...
   or: git reset --patch [<tree-ish>] [--] [<paths>...]
```

これによると`git reset`は、`<commit>`や`<tree-ish>`や`<paths>`を引数にとるらしい。
`<paths>`は簡単だ。ワーキングディレクトリ内のファイルやディレクトリのパスだ。
`<commit>`はコミットのことに違いないだろうが、ちょっと引っかかる。確かにコミットIDを使った`git reset d921970`みたいなのも見たことはあるけど、どちらかというと`git reset HEAD`や`git reset master`のようなのをよく見る。`HEAD`や`master`がコミット？？？
`<tree-ish>`なんて何のことやらさっぱり。

こんな感じで、ヘルプやリファレンスで仕様を正確に理解することをあきらめ、使うたびにググって場当たり的に対症的にコマンドを使い、`git reset`はコミットを取り消すコマンドだというような勘違いを深めてしまう。

このような初心者の域から脱するためには、まず[Gitのリポジトリ構造(オブジェクトモデル)](http://qiita.com/kaitoy/items/ce6cd3426a16268389d9)を理解すべし。
少なくとも、`<commit>`と`<tree-ish>`を理解するには、ブランチやタグは単一のコミットへの参照(i.e. リファレンス)、`HEAD`は(通常は)ブランチへの参照、コミットはそのコミット時点でのプロジェクトのルートディレクトリへの参照であるということを理解しておく必要がある。

## `<commit>`の意味するもの
ヘルプやリファレンスに`<commit>`と書いてあったら、それはコミットそのものと、コミットに再帰的にデリファレンスできるものと考えていい。デリファレンスとは、参照先のもので置き換えることだ。
ブランチは一回デリファレンスするとコミットになる。`HEAD`は一回デリファレンスするとブランチになり、もう一回デリファレンスするとコミットになる。
といった具合なので、`<commit>`の部分には、コミットID、ブランチ、タグ、`HEAD`、タグオブジェクトIDなどを指定できる。

ついでに言えば、`<branch>`と書いてあっても実際には`<commit>`と同じ範囲を指す場合が多い。
例えば[git worktree](https://git-scm.com/docs/git-worktree)のヘルプは以下の様なものだが、この`<branch>`は実際にはタグでもコミットIDでも`HEAD`とかでもいい。

```sh
$ git worktree -h
usage: git worktree add [<options>] <path> [<branch>]
   or: git worktree prune [<options>]
   or: git worktree list [<options>]
```

たまに`<commit-ish>`というのも見かけるが、これは「コミットっぽいもの」という意味で、`<commit>`と同様の範囲を指していると考えていい。

```sh
$ git revert -h
usage: git revert [<options>] <commit-ish>...
   or: git revert <subcommand>
```

## `<tree-ish>`の意味するもの
`<tree-ish>`は、上記の`<commit-ish>`と同様「ツリーっぽいもの」だ。
ツリーとは、Gitのリポジトリ内で単一のディレクトリを表すツリーオブジェクトのこと。「ツリーっぽいもの」はツリーまたはツリーに再帰的にデリファレンスできるもの。
よって`<tree-ish>`の部分には、ツリーオブジェクトID、コミットID、ブランチ、タグ、`HEAD`、タグオブジェクトIDなどを指定できる。
ただ、普通ツリーオブジェクトIDはユーザからは見えないので、これ以外のものと考えると、実用的には`<tree-ish>`イコール`<commit-ish>`と考えていい。
`<tree-ish>`と書いてあるときはファイル群に対する操作で、`<commit-ish>`と書いてあるときはコミットに対する操作というニュアンスの違いはある気がする。

# ② 複数の機能をもつコマンドがある
[創造神](https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%BC%E3%83%8A%E3%82%B9%E3%83%BB%E3%83%88%E3%83%BC%E3%83%90%E3%83%AB%E3%82%BA)の愛するUNIXの「一つのことを、うまくやれ」という哲学に反し、Gitには引数の与え方によって使い分ける複数の機能をもつコマンドがある。
このことはコマンドやコマンドリファレンスを無暗に複雑にしてしまっていて、Gitのコマンドを覚えにくく使いにくくしている一因だ。

例えば`git reset`。

```sh
$ git reset -h
usage: git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]
   or: git reset [-q] <tree-ish> [--] <paths>...
   or: git reset --patch [<tree-ish>] [--] [<paths>...]
```

`git reset`は、引数に`<paths>`を与えるか与えないかにより、二つの異なる機能を使い分けなければいけない。
`<paths>`を与えた場合はインデックスを更新する機能で、与えない場合は`HEAD`を付け替える機能。詳しくは[ここ](https://tbd.kaitoy.xyz/2016/01/01/git-revert-reset/#git-reset)に書いた。

もともと`git reset`は後者の機能だけを持っていたが、あとで前者の機能が追加された。
これは蛇足だったと思う。コマンドの一貫性を崩してしまった。

`git reset <paths>`はたいてい`git add`を取り消す用途で使われるので、代わりに`git unadd`みたいなコマンドにしておけば分かりやすかったのに。
もっといえば、そもそも`git add`も名前が微妙なので、`git stage`と`git unstage`だったらよかったのに。

などと愚痴を言ってもしょうがないので、`git reset`は二つの機能を持っていると知り、自分がどちらの機能を使っているのかを意識しながらコマンドをたたけば、そうでない場合に比べて格段に速く深く身につくはずだ。

`git reset`の他にも、`git checkout`もなんだかもやっとする挙動をして分かり辛いと感じる人が多いだろう。
このコマンドも[3つの異なる機能を持つ](https://www.atlassian.com/ja/git/tutorial/undoing-changes#!checkout)と説明されることがある。
その説明も理に適ったものだが、この記事では別の形で説明してみたい(次節)。

これらの他にも複数の機能を持ったコマンドがあるかもしれないので、使っていて違和感を覚えたらリファレンスに当たるべし。

# ③ 巷の解釈と実装がずれている
Gitのコマンド体系はGitのオブジェクトモデルを念頭に構築されているが、初心者向けにオブジェクトモデルの説明を避けてコマンドの説明をするサイトが多く、不適切な解釈が巷に広まっているケースがあるように思う。
前節で取り上げた`git reset`に対する、コミットを取り消すという解釈もその一つだが、ここでは`git checkout`を例に挙げる。

`git checkout`はほとんどのサイトでブランチを切り替えるコマンドと説明される。
Gitにおけるブランチの切り替えとは`HEAD`の付け替えのことなので、つまり`git checkout`は`HEAD`を付け替えるコマンドと解釈されているわけだが、[リファレンス](https://git-scm.com/docs/git-checkout#_description)には以下の様に書かれている。

> Updates files in the working tree to match the version in the index or the specified tree.
> If no paths are given, git checkout will also update HEAD to set the specified branch as the current branch.

つまり、`git checkout`の主たる機能は、リポジトリに合わせてワーキングディレクトリを更新するものだ。
もう少し噛み砕けば、リポジトリからワーキングディレクトリにファイルを取り出すものだ。
これがGitにおける「チェックアウト」であって、`HEAD`の付け替えは二次的なオプショナルな作用である。
詳しくは[ここ](https://tbd.kaitoy.xyz/2016/10/08/git-checkout/)に書いた。

`git checkout`をブランチを切り替えるコマンドだと覚えてしまうと、`git checkout .`を実行したときに`add`していない変更が破棄される挙動にもやっとし、そのもやっに阻まれてなかなかコマンドが手になじまない。
リポジトリからワーキングディレクトリにファイルを取り出すものという正しい解釈を持てば、全ての機能が一貫し、心地よく受け止められるものになるだろう。

`git checkout`の他にも間違った解釈をしてしまっているコマンドがあるかもしれないので、使っていて違和感を覚えたらリファレンスに当たるべし。

# まとめ
初心者を卒業し、Gitコマンドをちゃんと使いこなせるようになりたいなら、オブジェクトモデルを理解し、リファレンスを読むべし。