octokit.rbで有名ドコロのリポジトリからコミットメッセージ一覧を引っこ抜く

GitHub初心者でコミットメッセージ英語力が貧弱なので、参考のために有名ドコロのリポジトリからコミットメッセージ一覧を引っこ抜くクローラーを書いてみた。感覚ではなくファクト大事。

書いたコードは以下に置いといた。
https://github.com/minamijoyo/commit-crawler

GitHub API仕様
https://developer.github.com/v3/

APIラッパーライブラリがあって、公式でRubyObjective-Cと.NETがあるようです。今回はRubyを使いました。
https://github.com/octokit/octokit.rb

公式ラッパーと言いつつ、あんまりドキュメントないので、ソース覗きつつ、実際に触ってみてだいたい使い方分かってきたので以下にまとめておく。

octokit.rbのバージョンは現時点の最新版3.7.0を使いました。Rubyは2.2.0です。
インストールはただのgemなので簡単。

$ gem install octokit

あと直接は関係ないけど、手探りで対話的にどんな値が取れるか調べるのにirbよりもpryのがいい感じに整形してくれて見やすくてよいです。

$ gem install pry

GitHub APIは認証なしだとデフォルトで1時間あたり60回しか使えないので、お試しならよいかもしれないけど、データ収集するなら認証した方がよいです。
認証するとAPIのリミット上限が基本は1時間あたり5000回まで使える。(ただし検索は1分あたり30回のようです。)

パスワード認証もできるけど、プログラムから操作するならアクセストークンのがよいと思う。トークンの作り方の流れはここに書いてあるけど、
https://help.github.com/articles/creating-an-access-token-for-command-line-use/

GitHubの自分のアカウント設定の画面で「Application」→「Personal access tokens」→「Generate new token」を押して、「Token description」に適当な名前、「Select scopes」のところで必要な権限にチェック入れる。
権限の説明は以下に書いてあるけど、publicな情報をAPIで読むだけならno scopeつまりすべてのチェックを外しててもよい。
https://developer.github.com/v3/oauth/#scopes

「Generate token」を押したらキーができるのでメモっておく。

適当なファイルに書いて環境変数としてexportしておく。

$ vi githubapi.conf
export GITHUBAPI_ACCESS_TOKEN=xxxxx
$ chmod 400 githubapi.conf

あと間違って公開しないように.gitignoreとかにも忘れずに足しておいた方がよい。

$ echo "githubapi.conf" >> .gitignore

ここから簡単な疎通確認。まず使う前に環境変数を読み込む。

$ source githubapi.conf

pryでoctokit読み込む。

$ pry
pry(main)> require 'octokit'
=> true

Octokit::Client.newにアクセストークンを渡す。

pry(main)> client = Octokit::Client.new(:access_token => ENV['GITHUBAPI_ACCESS_TOKEN'])
=> #<Octokit::Client:0x3fe58e0a0c04>

試しに自分のユーザのログイン名が取得できたらちゃんと使えてる。

pry(main)> client.user.login
=> "minamijoyo"

これは特にログイン処理をしているというわけではなく、loginというプロパティを参照してる。
client.userとかやるとJSONでわらわら返ってくる。

pry(main)> client.user
=> {:login=>"minamijoyo",
 :id=>6985802,
 :avatar_url=>"https://avatars.githubusercontent.com/u/6985802?v=3",
 :gravatar_id=>"",
 :url=>"https://api.github.com/users/minamijoyo",
 :html_url=>"https://github.com/minamijoyo",
(略)

メソッド適当に呼んで応答がどんなかんじかはpryで適当に試してるうちになんとなく分かると思う。

ここから簡単な使い方の例。

まずはスターが10000個以上ついてるリポジトリを検索する。

pry(main)> repos = client.search_repos('stars:>10000')
=> {:total_count=>76,
 :incomplete_results=>false,
 :items=>
  [{:id=>2126244,
    :name=>"bootstrap",
    :full_name=>"twbs/bootstrap",
    :owner=>
     {:login=>"twbs",
(略)

レスポンスの型はoctokitが内部で依存してるSawyer::Resourceというのになってる。

pry(main)> repos.class
=> Sawyer::Resource

シンボルで必要なデータを取り出す。この場合検索結果は76件。

pry(main)> repos[:total_count]
=> 76

個々の検索結果にアクセスするには:itemsがArrayになってるので基本的にこれを使えば良い。

pry(main)> repos[:items].class
=> Array

例えば検索結果1つ目のリポジトリ名を取得するにはこんなかんじ。

pry(main)> repos[:items][0][:full_name]
=> "twbs/bootstrap"

が、検索結果が多い場合はページ分割されてる。デフォルトだと30件しか返ってこない。

  pry(main)> repos[:items].length
=> 30

オプションで:per_pageを指定するとページあたりの件数が指定できるけど、最大で100件まで。

pry(main)> repos = client.search_repos('stars:>10000', :per_page => 100)
pry(main)> repos[:items].length
=> 76

たいしたページ数でなければauto_paginate機能で自動でフェッチさせるのが簡単。

pry(main)> client.auto_paginate = true
=> true
pry(main)> repos = client.search_repos('stars:>10000')
pry(main)> repos[:items].length
=> 76
pry(main)> client.auto_paginate = false
=> false

ただ大量のページがある場合は、応答結果を一旦全部メモリ上に持ってしまうので大量にメモリを消費して使いものにならないので、自分で個別にフェッチする必要がある。後述。

リポジトリの情報を取得するにはいろいろやり方があるようだけど、ここではrepoメソッドにfull_nameを渡す。

pry(main)> repo = client.repo('twbs/bootstrap')
=> {:id=>2126244,
 :name=>"bootstrap",
 :full_name=>"twbs/bootstrap",
 :owner=>
  {:login=>"twbs",
   :id=>2918581,
(略)
 :contributors_url=>"https://api.github.com/repos/twbs/bootstrap/contributors",
 :subscribers_url=>"https://api.github.com/repos/twbs/bootstrap/subscribers",
 :subscription_url=>"https://api.github.com/repos/twbs/bootstrap/subscription",
 :commits_url=>"https://api.github.com/repos/twbs/bootstrap/commits{/sha}",
(略)

応答結果見てると、〜urlみたいなリンク情報が色々混じってるんだけど、
リンクのたどり方は例えばcommits_urlにアクセスするには

pry(main)> repo[:commits_url]
=> "https://api.github.com/repos/twbs/bootstrap/commits{/sha}"

relsというのでSawyer::Relationというのが取れるので、

pry(main)> repo.rels[:commits]
=> #<Sawyer::Relation: commits: get #<Addressable::Template:0x007fce9f948ac0>>

getを呼べばリクエストが投げられる。

pry(main)> repo.rels[:commits].get
=> #<Sawyer::Response: 200 @rels={:next=>#<Sawyer::Relation: next: get #<Addressable::Template:0x007fce9b849e70>>} @data=[{:sha=>"cb939e2efe36eb3ad7914a423d8ff08ac7160ca5",
 :commit=>
  {:author=>
    {:name=>"Bootstrap's Grunt bot",
     :email=>"twbs-grunt@users.noreply.github.com",
     :date=>2015-01-28 01:02:34 UTC},

getはSawyer::Responseを返すので、get.dataが賞味のデータになる。

pry(main)> repo.rels[:commits].get.data
=> [{:sha=>"cb939e2efe36eb3ad7914a423d8ff08ac7160ca5",
 :commit=>
  {:author=>
    {:name=>"Bootstrap's Grunt bot",
     :email=>"twbs-grunt@users.noreply.github.com",
     :date=>2015-01-28 01:02:34 UTC},

ちなみにコミットの情報を取りたいだけであれば、専用のcommitsメソッドを呼ぶほうが簡単。

pry(main)> commits = client.commits('twbs/bootstrap')
=> [{:sha=>"cb939e2efe36eb3ad7914a423d8ff08ac7160ca5",
 :commit=>
  {:author=>
    {:name=>"Bootstrap's Grunt bot",
     :email=>"twbs-grunt@users.noreply.github.com",
     :date=>2015-01-28 01:02:34 UTC},

relsでURLたどる方法はoctokit.rbで専用のメソッド定義されていない情報取りたいときとかに使える。
公式ラッパーの割にドキュメントが大してないので、どんなメソッドがあるかはソース見ないと分からんです。
あと使いどこは自前でページング処理をする場合。
前述したauto_paginateはページ数が少ない場合には問題なけど、例えばこの例のbootstrapのリポジトリとかだと10000コミット以上あって、:per_pageを100にしても100ページ以上ある。やってみるとわかるけど、数百MBオーダーでメモリをバカ食いします。どんだけ無駄遣いしてんねん。

こう言う場合は先ほどのレスポンスに含まれてるnextというリンクを辿っていけば、次のページを取得できる。

=> #<Sawyer::Response: 200 @rels={:next=>#<Sawyer::Relation: next: get #<Addressable::Template:0x007fce9b849e70>>}

auto_paginate機能を実装してるOctokit::Client::paginateメソッドもそういう実装になってる。
ちなみにpaginateメソッドは引数見るとブロック渡せるので、一見ページごとの処理を挟めるようにも見えるけど、ソース見るとブロックの呼び出しは2ページ目以降呼ばれる処理で、1ページ目では呼ばれないので、ページごとに必要なデータを抜き出したいみたいな使い方だと1ページ目の扱いが非対称になるので注意。

自前でページングのループ書くなら、client.last_reponseに前回の応答が入ってるので、

first_response = client.commits(repo, :per_page => 100)
#1ページの処理

last_response = client.last_response
while last_response && last_response.rels[:next]
  last_response = last_response.rels[:next].get
  #2ページ目以降の処理
end

みたいなかんじで書ける。

あと注意点としてAPIの呼び出し条件をチェックする方法として
rate_limitメッソドというのがあって、

pry(main)> client.rate_limit
=> #<struct Octokit::RateLimit
 limit=5000,
 remaining=4999,
 resets_at=2015-01-28 18:02:53 +0900,
 resets_in=3596>

remainingがあと何回呼び出せるか、resets_inがリセットされるまでの残り秒数を表してる。
なので、こんなかんじでループしておけば大体の場合問題ないと思う。

until client.rate_limit.remaining do
  sleep client.rate_limit.reset_in
end

例外はさっきのページング処理を自前でやった場合に、client.commitsのタイミングでは更新されるけど、次ページをgetしてもclient.rate_limitが更新されないようです。
ライブラリのバグなのかAPI側が必要なヘッダをセットして来ないのかまで切り分けてないですが、レートを明示的に取得するclient.rate_limit!だとそもそもlast_responseが変わってしまうので、実運用上とりあえずページごとに1秒ずつsleepして秒間1件以上リクエスト投げないようにすれば、上限1時間5000件に引っかかったりすることはないと思います。あんまり高速にリクエスト投げすぎるのも迷惑だしページ間は適度にsleepしといたらよいんじゃないでしょうか。

最後にcommitデータからコミットメッセージを拾うには、
これがコミットIDで、

pry(main)> commits[0][:sha]
=> "cb939e2efe36eb3ad7914a423d8ff08ac7160ca5"

コミットメッセージはここにある。

pry(main)> commits[0][:commit][:message]
=> "automatic grunt dist"

ちなみにコミッター名はこれなんだけど、たまにこの値が存在しないコミットがあるので注意。

pry(main)> commits[0][:committer]
=> {:login=>"twbs-grunt",
 :id=>9835569,

もしかするとAPI経由でコミットしたりすると空欄の場合もあるのかもしれないです。
また、別にここにも:committerがあるけど、:loginがなくてIDが特定できないのでちょっとイマイチ。
とりあえずコミッター名は今回の目的だとまぁなくてもよいか。

pry(main)> commits[0][:commit][:committer]
=> {:name=>"Bootstrap's Grunt bot",
 :email=>"twbs-grunt@users.noreply.github.com",
 :date=>2015-01-28 01:02:34 UTC}

上記を全部まとめるとこんなかんじになった。

https://github.com/minamijoyo/commit-crawler

こいつを実際にAWS上のt2.microインスタンスクローラー動かして
スター10000以上の有名ドコロリポジトリからコミットメッセージ一覧を引っこ抜いてみた。
所要時間8時間ぐらいで、リポジトリ数76個、コミット数合計878843。
ファイルサイズはshaとかも出力してるので純粋なコミットメッセージ以外も含むけど326MBで結構な量になった。

正直こんなサイズになると思わんかったので、十分すぎる量のファクトが集まったけど、逆にこんなにあったらgrepで見るの限界ある。
なんかいい感じに分析する方法考えよう。