コミットハッシュから pull request ページを開いたり取得するコマンドラインツール tosa

# これは何？

https://github.com/kyoshidajp/tosa

git のコミットハッシュから pull request のページを開いたり、情報を取得するコマンドラインツールです。

![tosa1.gif](https://qiita-image-store.s3.amazonaws.com/0/36122/cf083211-7213-248c-0d3c-800d6995457e.gif)

# どんな時に使う？

Git 管理されたコードで、なぜ今の実装になったのか知りたい事があります。この時、コミットの差分を見てもその意図がよく分からないがコミットした人がすでに退職している、実は自分がコミットしたのによく覚えていない:innocent:という事はないでしょうか？

コードを GitHub で管理している場合、pull request ページが参照できれば経緯やコードレビューのログが確認できるため開発効率が向上します。また、将来のために pull request にはこのような**変更時のログをきちんと残そうという文化をチームに根付かせる動機づけ**になると思います。

# インストール

[Homebrew](https://brew.sh/index_ja.html) ユーザであれば次のコマンドから簡単にインストールできます。

```
$ brew tap kyoshidajp/tosa
$ brew install tosa
```

また、Go で実装しているため Go の環境を設定している場合は `go get` でインストールできます。

```
$ go get -u github.com/kyoshidajp/tosa
```

これ以外の場合は [リリースページ](https://github.com/kyoshidajp/tosa/releases)から動作環境に合ったファイルをダウンロード、展開し、`tosa` を実行パスの通った場所に置きます。

# 実行方法

引数にコミットハッシュを指定して実行すると、標準のブラウザで pull request ページが開きます。

```
$ tosa c97e6909
```

初回起動時に Github のログインプロンプトが表示され、ユーザ名、パスワードと2段階認証コード（設定していれば）を入力する必要があります。これは Github API でプライベートリポジトリを含めた検索に利用しているためです。CI 等で実行する場合はアクセストークンを環境変数 `GITHUB_TOKEN` に設定すればこれを優先します。

## tig から起動
[tig](https://jonas.github.io/tig/) ユーザであれば `$HOME/.tigrc` に次のキーバインド設定を行っておくと便利です。

```
bind main O @tosa %(commit)
bind blame O @tosa %(commit)
```

メインビューと blame ビューで O(Shift+o) を押したタイミングでカーソル位置のコミットハッシュから pull request の URL を開きます。

![tig_tosa.gif](https://qiita-image-store.s3.amazonaws.com/0/36122/869170c6-dd44-dde1-2a7f-a5a486ef5137.gif)

## URL の出力

`-u` または `--url` を指定すると、URL が出力されます。

```
% tosa -u c97e6909
https://github.com/kyoshidajp/tosa/pull/13
```

利用ケースとしては ssh で接続したリモートマシン上などブラウザが直接起動できない場合を想定しています。

## API URL の出力

`-a` または `--apiurl` を指定すると、API URL が出力されます。

```
$ tosa -a c97e6909
https://api.github.com/repos/kyoshidajp/tosa/issues/13
```

これを curl に渡して、[jq](https://stedolan.github.io/jq/) で取得・整形する事も可能です。次は pull request のタイトルを取得する例です。

```
$ curl -s `tosa -a c97e6909` | jq -r '.title'
Add short command option and usage
```

API から取得できる項目は [Issues \| GitHub Developer Guide](https://developer.github.com/v3/issues/#get-a-single-issue) を参照してください。

利用ケースとしては、例えば詳細なリリースノートの自動生成です。前回のリリースからの変更内容を git-log してコミットハッシュ一覧を取得し、pull request データ経由で修正概要や修正したユーザ情報を出力する際の利用を想定しています。

# どうやって pull request を取得している？

Github API の [Search issues](https://developer.github.com/v3/search/#search-issues) で検索キーワードに「**コミットハッシュ**」「**リポジトリ**」「**pull request のステータス**」を指定して検索しています。

例えば、tosa のコミットハッシュ `c97e6909` の検索条件パラメータ `q` は `q=repo:kyoshidajp/tosa pr:merged c97e6909` となります。

URLエンコードを行った最終的な検索 URL は次になります。

https://api.github.com/search/issues?q=repo%3Akyoshidajp%2Ftosa%20pr%3Amerged%20c97e6909

このレスポンスに含まれる `html_url` が pull request の URL になります。現状レスポンスの `items` は複数返ってきますが、先頭の1件が該当するはずです。

これら API のアクセス、データ取得を Go API のクライアントライブラリ [go-github](https://github.com/google/go-github) で行っています。

ちなみに API でなく通常のウェブページ検索で取得することも可能です。検索ボックスに先程の `repo:kyoshidajp/tosa pr:merged c97e6909` で検索します。

<img width="726" alt="search_github.png" src="https://qiita-image-store.s3.amazonaws.com/0/36122/87128fa0-1ef1-13e1-3739-bd471a9ffdbe.png">

検索結果のページ URL はこちらです。

https://github.com/search?utf8=%E2%9C%93&q=repo%3Akyoshidajp%2Ftosa+pr%3Amerged+c97e6909&type=

<img width="710" alt="github_issue.png" src="https://qiita-image-store.s3.amazonaws.com/0/36122/cad67c7a-e9ed-6ac4-93d7-0793b115f4c0.png">

# 参考にしたアイデア、ツール

## [GitベースのコードリーディングTips \- クックパッド開発者ブログ](http://techlife.cookpad.com/entry/2015/11/17/151426)

コミットハッシュから pull request ページを開くというアイデアはもともとこちらで紹介されていたもので、とても便利に感じて利用していました。ただ、たまに関係のない pull request ページがヒットする事があったり、リポジトリを切り替える度にURL(`$YOUR_REPO_URL`）を都度設定する必要があり、不便に感じていました。

また、Ruby が入っている事を前提としてシェルに組み込む形だった事もあり、当時在籍していたチーム（Ruby をインストールしていない Windows 環境）に浸透しなかった事を覚えています。

## [pocke/whichpr: Find a pull request form commit hash\.](https://github.com/pocke/whichpr)

検索実装部分についてはこちらのツールを参考にしました。ただ、OSS のコントリビュートでよくある fork したリポジトリでは上手く動かなかったり、API URL の出力などの機能を追加したりと色々と個人的に変更したかったため、新規に作成しました。

## [tcnksm/ghr: Upload multiple artifacts to GitHub Release in parallel](https://github.com/tcnksm/ghr)

Go による CLI の実装について特にステータスコードやデバッグ関数、Makefile の書き方が全く分からない状態だったので、かなり参考にしました。また、Release 用のアップロードで実際に利用しています。

# これからの課題

## GitLab や Bitbucket など他の git ホスティングサービスの対応

GitHub だけではなく、GitLab や Bitbucket などの git ホスティングサービスを利用しているユーザが一定数いるので利用してもらえることが見込めます。

ただ、少し調べてみると GitLab ではコミットハッシュによる検索ではコミットしかヒットしないようで、merge reuqest はヒットしませんでした。他のサービスは未調査です。

## 高速化

現状では API のリクエストに時間がかかっていて、特に fork したリポジトリのデータを取得する際には次のように最大で3回のネットワークアクセスが行われるため非常に遅くなってしまいます。

1. コミットハッシュから PR を検索
2. 1. には PR ページの URL がない（ケースが多い）ので親のリポジトリから PR を検索・取得
3. curl などによる API URL へのアクセス

これに対しては次のアイデアとさらなる課題があります。

- コミットハッシュと URL のマッピングキャッシュを導入
  - どのような形式で何をキャッシュさせるか検討が必要
    - HTML URL は必須だが、他のデータはどうする？オプションで選択できるようにする？
  - 想定している利用ケースではキャッシュヒット率が期待できない
- fork したリポジトリは先に親のリポジトリをチェックする
  - 親のリポジトリを取得する際にもネットワークのリクエストが必要
    - キャッシュさせる？
- GitHub GraphQL API v4 の対応
  - 次で説明

## GitHub GraphQL API v4 の対応

現在 tosa では [GitHub API v3](https://developer.github.com/v3/) を利用していますが、GitHub は GraphQL に対応した次世代の [GitHub GraphQL API v4](https://developer.github.com/v4/) を公開しています。

例えば、tosa の c97e6909 がマージされた pull request のタイトルと URL を取得するクエリは次のようになります。

```
query { 
  search(type: ISSUE, query: "repo:kyoshidajp/tosa pr:merged c97e6909", first: 1) {
    edges {
      node {
        ... on PullRequest {
          title
          url
        }
      }
    }
  }
}
```

実行結果は次のようになります。

```json
{
  "data": {
    "search": {
      "edges": [
        {
          "node": {
            "title": "Add short command option and usage",
            "url": "https://github.com/kyoshidajp/tosa/pull/13"
          }
        }
      ]
    }
  }
}
```

ちなみに実行は [GraphQL API Explorer](https://developer.github.com/v4/explorer/) から確認できます。

これだけだとあまりメリットが無いように感じますが、GraphQL なので、効率的にデータを取得できる事が期待できます。

例えば pull request のコミットに含まれるユーザのアバター URL を取得したいといった場合に、現状では curl によるリクエストを挟んで jq などで整形する手間が必要ですが、GitHub GraphQL API v4 ではそれらは不要で1リクエストで完結すると思います。

Go ではすでに GitHub GraphQL API v4 に対応した [githubql](https://github.com/shurcooL/githubql) ライブラリが存在します。クエリは構造体で定義するので、 reflect パッケージを使ってユーザのリクエストに合わせて動的に定義すれば良さそうです。