Gitを手探ってみた その2


一応これは[Gitを手探ってみた その1](http://qiita.com/vintersnow/items/1541d10a0116d04429cc)の続きです。

# gitをいじる目的
素晴らしいgitそれ自体の仕組みを知りたかったので始めたことですが、せっかくなので何かしらの機能を追加したいと思います(授業の本来の目的はこちらだろうが)。  
gitは素晴らしいのでこれ以上変更する場所はあまりないのですが、[SmallProjectsIdeas](https://git.wiki.kernel.org/index.php/SmallProjectsIdeas#Warn_before.2Fwhen_rewriting_published_history)に「amendをするときにwarningを出すようにする」というのがあったのでこれを解決出来ないか試してみたいと思います。  
git amendは直前のコミットを変更するコマンドです。とても便利なコマンドですが、すでにリモートにpushしているコミットをamendするとコンフリクトを起こしてしまいます。そのような時は、一度pullしてmergeしないといけないので素晴らしいGitの履歴が汚くなってしまいす。
なので、すでにpushしたコミットはamend出来ないようにしたほうがいいのではと考えました。

![git_amend.png](https://qiita-image-store.s3.amazonaws.com/0/34727/8e3c5fcf-b849-ea01-3dc6-9dfa69750542.png "git_amend.png")


# gitプログラムについて
では、早速gitのプログラムを見てみましょう。[git/git](https://github.com/git/git)は20万行を超えるCUIツールとしてはとても大きなソフトなので全て読み解くことは至難の業です。  
自分はこのようなことは初心者なので、基本的な攻め方はgdbで起動して`git log`や`git commit`の様子を探っていきます。
後から気づいたのですが、大まかなところを探るためにgrepで文字列を検索して大体な所を予想してbreak pointを設定すると捗りました。
個人的な意見としてはgitはライブラリなどが使用されておらず読みやすいように感じました。

## main関数
main関数は`git.c`にありました。`commit`や`log`のようにbuilt-inコマンドはここから `main->run_argv->handle_builtin->run_builtin`の順に関数を踏み、途中で収得した各コマンドごとの関数ポインタでそれぞれの処理に飛びます。
built-inコマンドは基本的にbuiltディレクトリの中に同名のファイルがあると思います。例えば`add`なら`add.c`、`commit`なら`commit.c`のがあります。
git.cから最初に呼び出されるのは`cmd_{コマンド名}`という関数のようです。`commit`の場合はcommit.cの中の`cmd_commit()`が呼び出されます。

## commit --amendを手探る。
`commit --amend`を渡してみてgdbで探ってみます。  
オプションが正しいか解決するのは`parse_and_validate_options`という関数のようです(commit.cのcmd_commit()内で呼び出されています)。
この関数の真ん中らへんに以下のようなコードがあります。

```c
if (amend && whence != FROM_COMMIT) {
    if (whence == FROM_MERGE)
        die(_("You are in the middle of a merge -- cannot amend."));
    else if (whence == FROM_CHERRY_PICK)
        die(_("You are in the middle of a cherry-pick -- cannot amend."));
}
```

`amend`の文字もあるし、もしkillするならココらへんにプログラムを入れると良さそうです。

## どのようにpush済みか判断するか
当初、push済みかどうかを判断するのに`log`のコードを使えばいいと考えていました。  
`--decorate`を使えば、下記のようにremoteとlocalのheadが分かります。なのでlocalのHEADがremoteのHEADより進んでいる場合のみamend出来るようにしたら良さそうです。

```
$ git log --decorate --oneline
d7e7b6f (HEAD -> master) Merge remote-tracking branch 'origin/master'
6e85f70 (origin/master) test
```

`log.c`の`cmd_log_walk()`を参考にこのようにしてみました。  
`decoration->name`は"origin/master"のような文字列です。`branch_get`と`branch_get_upstream`でリモート名(慣習的にorigin)を習得し現在のHEADのdecorationにリモート名が含まれていたら、dieしています。

```c
struct branch *br = branch_get(NULL);
const char *upstream_name = branch_get_upstream(br,NULL);
decoration = get_name_decoration(&commit->object);
while(decoration){
  if (strstr(decoration->name,upstream_name)){
    die(_("This commit are already pushed to remotes -- cannot amend."));
    break;
  }
  decoration = decoration->next;
}
```

これでもある場合では動くのですが、もしremoteのヘッドが先行している場合(進めていないbranchを`fetch`した場合など)は期待通りには動きません。


ではリモートのHEADとローカルのHEAD双方から全探索をするのか?
それはあまりいい方では無いように思うし、何より素晴らしいGitとしてはスマートではありません。
といことでいろいろと探してみたところ、`status`が使えるのではないかと思い当たりました。
以下の4パターンでの`git status`の出力は以下のようになります。

1. localとremoteが同じ -> amendダメ
2. localが進んでいる -> amend ok
3. remoteが進んでいる -> amend ダメ
4. localとremote両方が進んでいる。 -> amend ok

```
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
```

```
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
nothing to commit, working directory clean
```

```
On branch master
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
nothing to commit, working directory clean
```

```
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 12 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)
nothing to commit, working directory clean
```

`status`コマンドを探ってみたところ`remote.c`の中にちょうどいい関数がありました。
`stat_tracking_info(branch, &ours, &theirs, &full_base)`にbranchとint型変数のアドレス2つとconst char*型のアドレスを渡すと、oursの中にlocalの進んでいる分、theirsの中にremoteの進んでいる分を教えてくれます。
これを使った最終的なプログラムは以下のようになりました。

```c
struct branch *br = branch_get(NULL);
int num_ours,num_theirs;
const char *upstream_name;
stat_tracking_info(br,&num_ours,&num_theirs,&upstream_name);
if (amend && num_ours==0){
    die(_("This commit are already pushed to remotes -- cannot amend."));
}
```

すっきり綺麗にまとまりましたね。素晴らしい。

# Gitコミュニティにコミットしてみようとしてみた
せっかくの変更したので、gitのコミュニティにコミットしてみようと思いGit Mailing Listにメールを投稿してい見ました。
したところ、以下の様な指摘を貰いました。

1. パッチの送り方が違う([ドキュメント](https://github.com/git/git/blob/master/Documentation/SubmittingPatches)を読もう。)
2. remoteにブランチがないとバグる。
3. テストも書かないといけない。
4. 強制的に落とすのは使っている人もいると思うのであまり良くない。

送ったあと見つけたのですが、[Git にパッチを送って取り込まれた話](http://tech.nitoyon.com/ja/blog/2013/07/23/git-patch/)という記事が参考になりそうです。
2はすぐに解決することが出来ますが、dieするのではなく例えばwarningを出すことやremoteを使ったテストの書き方が分からず、まだ出せていません。
また時間があるときに挑戦してみたいと思います。

# 参考文献

+ [Git Documentation Book](https://git-scm.com/book/ja/v2)
+ [Gitの仕組み](http://koseki.hatenablog.com/entry/2014/04/22/inside-git-1)