リポジトリへの貢献量に応じて日本を手中に収めるCLIを作った
これは、LAPRAS Advent Calendar 2022の8日目の記事(代理投稿)です
また、この記事を書き始める前に眺めていたらたまたまいいタイミングで空いていたので、Go Advent Calendar 2022 の22日目にもエントリーしたいと思います
なにこれ
LAPRAS株式会社でSREをしていますyktakaha4と申します 🐧
このたび、Gitのリポジトリを検索し、コントリビューター毎に今稼働しているSLOCに応じて日本地図を塗り分けして可視化する Kunitori
というCLIをGoを使って作りましたので、そのご紹介と学んだことを残したいと思います ✍
Kunitoriで生成した日本地図
作った理由
突然ですが、みなさんの一番好きなGitHubの画面はどこでしょうか
私は /graphs/contributors
です
各コントリビューターがリポジトリにどの程度コミットしたかが一目でわかるグラフが並んでおり、見ていて飽きません
仕事でコミットしているプロダクトのリポジトリやほそぼそと続けてきた個人開発のプロジェクトであれば、地道に開発を継続してきたことが可視化されてモチベーションのアップに繋がりますし、
著名なOSSのものを見ていると、 別のリポジトリのコアコントリビューターの人がドキュメント修正のPRをしている…!
みたいなことを発見できます
Contributors ページのイメージ
しかしながら、現時点での当機能の問題点として いまmainブランチのHEADで稼働しているコードに対して自身がどの程度コントリビュートできているかわからない
というものがあります
例えば、自分がコミットしているリポジトリに以下のような修正をしている人がいたとします
どうされましたか
このグラフからは、この方が結構昔に相当行数の変更を加えていたことはわかりますが、コミットした内容が今のソースコードにどの程度影響を残しているのかはわかりません
また、修正が本当にソースコードだったのか、なにか大規模なテストデータのCSVだったのか、あるいは node_modules/
をうっかり入れてしまったのかといった、内容の詳細についても追うことはできません
あるファイルの変更内容の詳細について知るための良い方法として、 git blame コマンドがあります
このコマンドは、ファイル内の各行毎のリビジョン、変更者、変更日を見ることができ、既存実装の経緯を知る際に助けられている方も多いのでないかと思います
$ git blame -el articles/how_to_make_ansi_art.md | head -3
fa45c6c5f7253df42890648628487cc50bc76018 (<20282867+yktakaha4@users.noreply.github.com> 2022-11-27 22:17:29 +0900 1) ---
1dfa136ac3863c6a7236a16aa8f4f18fe152d74f (<20282867+yktakaha4@users.noreply.github.com> 2022-11-28 01:17:16 +0900 2) title: "ANSI artをつくろう"
fa45c6c5f7253df42890648628487cc50bc76018 (<20282867+yktakaha4@users.noreply.github.com> 2022-11-27 22:17:29 +0900 3) emoji: "👾"
ここから、 あるリビジョンの全てのソースコードのファイルでgit blameしてユーザーごとの行数を集計すれば、自分がどの程度プロダクトに貢献できているのかわかるのでは
という着想を得ました
実際にはSLOCとそのコードが生み出す価値には直接的な関係はありませんが、それでも傾向を手早く理解するという意味ではSLOCは一見の価値があると個人的には考えています
ということで、自身の貢献量を可視化して悦に入るために開発をはじめました🦆
どのように作るか
今回、開発言語としてはGoを選択しました
業務では基本的にPythonかTypeScriptを書いていることが多いのですが、直近でTerraform Providerを触ったり、SREをしているのでOSSのコードリーディングでGoを眺める機会も多いので、新規開発に挑戦してみることにしました
また、CLIツールを作るのに便利な機能が標準ライブラリで色々サポートされていたり、バイナリファイルを簡単に作れて配布がしやすかったりといった点も、今回のユースケースにあっているのでないかと思いました
作ったもの
以下リポジトリで公開しています 🏯
使い方はひとつしかなく、Releaseページからバイナリを落としてきて、 kunitori generate
コマンドを実行するだけです
オプションは以下のような感じです
引数としては、リポジトリのローカルパスを指定する -path
か、Gitリポジトリの -url
が必須になります
$ ./kunitori generate -h
Usage of generate:
-authors value
target file author regex (multiple specified, format: author=regex)
-filters value
target file filter regex (multiple specified)
-interval duration
commit pick interval (default 720h0m0s)
-json
export as json format
-limit int
commit pick limit (default 12)
-out string
out directory path (default ".")
-path string
repository path
-region string
chart region (default "JP")
-since string
filter commit since date (format: 2006-01-02T15:04:05Z07:00)
-until string
filter commit until date (format: 2006-01-02T15:04:05Z07:00)
-url string
repository url
例として、 your-org/your-repo
リポジトリのプロダクトコードとテストコードを、言語別(PythonとVue.js & TypeScript)に集計したい場合は以下のようになります
引数を何も指定しないと、今日から過去1年間のコミットを1か月おきに選択し、集計処理をおこないます
$ ./kunitori generate -path /path-to/your-org/your-repo -filters '.+\.py$' -filters 'test_.+\.py$' -filters '\.(vue|ts)$' -filters '\.(spec|test)\.(vue|ts)$'
open repository: path=/path-to/your-org/your-repo
location: remote=git@github.com:your-org/your-repo.git
search commit: since=1970-01-01 00:00:00 +0000 UTC, until=2022-12-22 12:27:53.771890417 +0000 UTC, interval=720h0m0s, limit=12
matched commits: count=12
count group: filters=4, authors=0
count lines: progress=1/12, hash=xxxxxxxxxxxx, when=2022-12-21 03:41:36 +0000 UTC
略
output: /path-to-your-curent/chart.html
一例として、弊社のとあるリポジトリのフロントエンドの支配状況について可視化したいと思います
generate
サブコマンドで出力されたチャートは以下のようになりました
chart.htmlのサンプル(2022年12月)
右下のウィンドウで集計対象としたコミットとコードを選択すると、右上に git blame
の結果がコントリビューター毎に集計して表示され、行数が多かった人順で日本地図を着色します
地図の描画については、Google Chartsを使っています
都道府県を選択すると、そこを領土としているコントリビューターが着色されます
北海道は約83,424平方キロメートルで、日本国土全体の22%にあたるため、リポジトリ内のコードをそのくらい書くと手にすることができます
逆に、着色されていない都道府県は 単独で特定の県を収められるほどコミットをしていなかった人の連合国
になります
仕様として北から順に色付けをしていくため、四国や九州は色付けされない場合が多いです
南には色んなコントリビューターがいる
先程のチャートは2022年12月現在の内容でしたが、同じ条件で2022年1月時点はどうだったか見てみましょう
過去のコミットの着色は最新のコミットのものと同じになるように調整しているため、2つ並べて見比べるとある人がどの都道府県まで支配を広げたか、あるいは退却したかがわかりやすいかと思います
chart.htmlのサンプル(2022年1月)
見比べてみると、当初圧倒的1位を誇っていたminojiroに迫る二大勢力が明らかになります
特にエモいのがkawamataryo で、自身の住むIbarakiから1年間でIwate / Aomoriまで勢力を伸ばしており、来年の動乱を予見させます
chart.htmlのサンプル(2022年1月)
おふたりのアドカレも貼っておきますので、よければご覧ください
所感
GoでCLIツールを書いてみてよかったところを書いていきます
特に新規性のある内容ではないですが、Goに明るくない方にオススメポイントが伝えられればと思います
go:embed すごい
今回特有の要件として、チャートのHTMLをバイナリに抱き込んでリリースしたいというものがありました
アセットファイルの管理は色々な言語で工夫が必要なところと思いますが、Goではembedという標準パッケージで簡単に実現できます
今回は、 chart.html
というテキストファイルを抱き込みたかったので以下のような書き方になりました
バイナリファイルであれば []byte
で受け取れたり、ディレクトリツリーとして読み込むこともできるので、様々な場面で便利に使えるのでないかと思いました
解説記事としては以下がわかりやすかったです
標準機能が豊富
embed以外に便利だなーと思ったのは、テンプレートエンジンのtext/templateとCLIのフラグパーサであるflagでした
あまり利用経験のない言語で個人開発をガンガンしていきたい時に、いろんなライブラリが出ていると目移りしたり選定に困ってしまうのですが、その点Goは大抵の機能が標準搭載されており、選ぶストレスを感じる場面が少なかったような気がします
記事としては、それぞれ以下が参考になりました
使ったOSS
個人的にPythonのunittest的なアサーションの書き方に慣れていたため、テストについてはstretchr/testifyを利用しました
特に、assertの機能を使っています
以下のように、 assert.Hogehoge(t, xxx)
のような馴染みのある書き方でアサーションが書けるので、直感的にテストを書いていけました
あと、バイナリのビルドとReleaseへのアップロードはgoreleaserがオススメです
以下のクイックスタートを見つつカスタマイズをしていくだけですぐに動かせてよかったです
また、私はまだ使いこなせていませんが、CHANGELOGをいい感じにしてくれたり、homebrewへの登録やGPGでの署名もyamlを書くだけで出来たりと非常に高機能な印象を受けました
学んだこと
細かい学びを書いておきます
テストデータ管理にSubmodulesを使う
今回、テストコードでGitリポジトリを使いたかったので、Git submoduleを使って実現しました
今回は、大規模リポジトリとしてDjangoと、小さなリポジトリとして個人で作った適当なやつをSubmoduleとして設定しておき、CI上のテストなど必要なところでのみ使うようにしました
GitHub Actionsで以下のように submodules
を取得するようにした上で fetch-depth
に0を指定しておくと、親側のリポジトリだけでなくサブモジュール側も全てのリビジョンを取ってきてくれるという仕様のようでした
バイナリバージョンの埋め込み
今回、versionコマンドを実行時にバイナリのバージョンとリビジョンを出力するようにしたのですが、これはldflagsで簡単に実現できました
$ ./kunitori version
Kunitori (国盗り)
Version: 0.0.2
Commit: 95a266e
ビルドのタイミングで以下のようにgoreleaserから指定すると、
コード側では以下に埋め込まれるので自由に使えます
記事は以下がためになりました
git blameが遅くて困った
当初、一番肝心の git blame
がメチャ遅で挫折を覚悟したのですが、これはgit操作に利用している go-git がラッパーでなくpure goで書かれており、そのblameコマンドが最適化できていないことによるものでした
解消方法としては、環境変数の設定状態に応じてgitコマンドを exec.Commandで実行できるようにして、gitコマンドがある環境ではそちらを利用可能にすることで高速化しました
ライブラリを使う側からすると実行環境に依存しないOSSは魅力的ですが、今回のような比較的ピーキーな使い方をしたいケースではラッパー形式のライブラリのほうが不確実性を低減できる場合もあると体で理解できたのはよかったです
Goの正規表現の仕様
今回、正規表現を使ってコードの集計対象を指定できる機能を作ったのですが、利用していて否定先読みを使うことができず困ることがありました
理由としては以下記事で詳しく説明されていますが、Goで実装している正規表現のアルゴリズムが、PerlやPythonで使われているPCRE系のものと異なっていて、それにより利用ができないということのようでした
個人的にPCREに慣れていたこともあり、最終的に標準ライブラリのregexpでなく dlclark/regexp2を利用することとしました
開発終盤にリファクタしたため性能に影響するかなとも思ったのですが、今回のユースケースではそこまで気にならなかったのでヨシとします
おわりに
Goでそこそこのサイズのものを書くいい機会となってよかったです🐤
今後も色んなリポジトリを可視化して笑顔になりたいと思います
明日(公開時点でもう今日ですが、)のアドベントカレンダーもお楽しみに
Discussion