🗾

リポジトリへの貢献量に応じて日本を手中に収めるCLIを作った

2022/12/22に公開約10,700字

これは、LAPRAS Advent Calendar 2022の8日目の記事(代理投稿)です

https://qiita.com/advent-calendar/2022/lapras

また、この記事を書き始める前に眺めていたらたまたまいいタイミングで空いていたので、Go Advent Calendar 2022 の22日目にもエントリーしたいと思います

https://qiita.com/advent-calendar/2022/go

なにこれ

LAPRAS株式会社でSREをしていますyktakaha4と申します 🐧

このたび、Gitのリポジトリを検索し、コントリビューター毎に今稼働しているSLOCに応じて日本地図を塗り分けして可視化する Kunitori というCLIをGoを使って作りましたので、そのご紹介と学んだことを残したいと思います ✍

https://github.com/yktakaha4/kunitori


Kunitoriで生成した日本地図

作った理由

突然ですが、みなさんの一番好きなGitHubの画面はどこでしょうか
私は /graphs/contributors です

各コントリビューターがリポジトリにどの程度コミットしたかが一目でわかるグラフが並んでおり、見ていて飽きません
仕事でコミットしているプロダクトのリポジトリやほそぼそと続けてきた個人開発のプロジェクトであれば、地道に開発を継続してきたことが可視化されてモチベーションのアップに繋がりますし、
著名なOSSのものを見ていると、 別のリポジトリのコアコントリビューターの人がドキュメント修正のPRをしている…! みたいなことを発見できます


Contributors ページのイメージ

しかしながら、現時点での当機能の問題点として いまmainブランチのHEADで稼働しているコードに対して自身がどの程度コントリビュートできているかわからない というものがあります

例えば、自分がコミットしているリポジトリに以下のような修正をしている人がいたとします


どうされましたか

このグラフからは、この方が結構昔に相当行数の変更を加えていたことはわかりますが、コミットした内容が今のソースコードにどの程度影響を残しているのかはわかりません
また、修正が本当にソースコードだったのか、なにか大規模なテストデータのCSVだったのか、あるいは node_modules/ をうっかり入れてしまったのかといった、内容の詳細についても追うことはできません

あるファイルの変更内容の詳細について知るための良い方法として、 git blame コマンドがあります

https://www.atlassian.com/ja/git/tutorials/inspecting-a-repository/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を眺める機会も多いので、新規開発に挑戦してみることにしました

https://zenn.dev/yktakaha4/articles/synthetic_monitoring_with_ur_and_tf

また、CLIツールを作るのに便利な機能が標準ライブラリで色々サポートされていたり、バイナリファイルを簡単に作れて配布がしやすかったりといった点も、今回のユースケースにあっているのでないかと思いました

作ったもの

以下リポジトリで公開しています 🏯

https://github.com/yktakaha4/kunitori

使い方はひとつしかなく、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%にあたるため、リポジトリ内のコードをそのくらい書くと手にすることができます

https://ja.wikipedia.org/wiki/都道府県の面積一覧

逆に、着色されていない都道府県は 単独で特定の県を収められるほどコミットをしていなかった人の連合国 になります
仕様として北から順に色付けをしていくため、四国や九州は色付けされない場合が多いです


南には色んなコントリビューターがいる

先程のチャートは2022年12月現在の内容でしたが、同じ条件で2022年1月時点はどうだったか見てみましょう

過去のコミットの着色は最新のコミットのものと同じになるように調整しているため、2つ並べて見比べるとある人がどの都道府県まで支配を広げたか、あるいは退却したかがわかりやすいかと思います


chart.htmlのサンプル(2022年1月)

見比べてみると、当初圧倒的1位を誇っていたminojiroに迫る二大勢力が明らかになります
特にエモいのがkawamataryo で、自身の住むIbarakiから1年間でIwate / Aomoriまで勢力を伸ばしており、来年の動乱を予見させます


chart.htmlのサンプル(2022年1月)

おふたりのアドカレも貼っておきますので、よければご覧ください

https://zenn.dev/minojiro/articles/c29afc4b891876

https://zenn.dev/ryo_kawamata/articles/6e161be042f3d1

所感

GoでCLIツールを書いてみてよかったところを書いていきます
特に新規性のある内容ではないですが、Goに明るくない方にオススメポイントが伝えられればと思います

go:embed すごい

今回特有の要件として、チャートのHTMLをバイナリに抱き込んでリリースしたいというものがありました
アセットファイルの管理は色々な言語で工夫が必要なところと思いますが、Goではembedという標準パッケージで簡単に実現できます

https://pkg.go.dev/embed

今回は、 chart.html というテキストファイルを抱き込みたかったので以下のような書き方になりました
バイナリファイルであれば []byte で受け取れたり、ディレクトリツリーとして読み込むこともできるので、様々な場面で便利に使えるのでないかと思いました

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/pkg/chart.go#L9-L25

解説記事としては以下がわかりやすかったです

https://future-architect.github.io/articles/20210208/

標準機能が豊富

embed以外に便利だなーと思ったのは、テンプレートエンジンのtext/templateとCLIのフラグパーサであるflagでした
あまり利用経験のない言語で個人開発をガンガンしていきたい時に、いろんなライブラリが出ていると目移りしたり選定に困ってしまうのですが、その点Goは大抵の機能が標準搭載されており、選ぶストレスを感じる場面が少なかったような気がします

記事としては、それぞれ以下が参考になりました

https://kazuhira-r.hatenablog.com/entry/2021/01/09/220048

https://www.spinute.org/go-by-example/command-line-flags.html

使ったOSS

個人的にPythonのunittest的なアサーションの書き方に慣れていたため、テストについてはstretchr/testifyを利用しました
特に、assertの機能を使っています

https://github.com/stretchr/testify

以下のように、 assert.Hogehoge(t, xxx) のような馴染みのある書き方でアサーションが書けるので、直感的にテストを書いていけました

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/pkg/git_test.go#L45-L68

あと、バイナリのビルドとReleaseへのアップロードはgoreleaserがオススメです

https://goreleaser.com/

以下のクイックスタートを見つつカスタマイズをしていくだけですぐに動かせてよかったです
また、私はまだ使いこなせていませんが、CHANGELOGをいい感じにしてくれたり、homebrewへの登録やGPGでの署名もyamlを書くだけで出来たりと非常に高機能な印象を受けました

https://goreleaser.com/quick-start/

学んだこと

細かい学びを書いておきます

テストデータ管理にSubmodulesを使う

今回、テストコードでGitリポジトリを使いたかったので、Git submoduleを使って実現しました

https://git-scm.com/book/ja/v2/Git-のさまざまなツール-サブモジュール

今回は、大規模リポジトリとしてDjangoと、小さなリポジトリとして個人で作った適当なやつをSubmoduleとして設定しておき、CI上のテストなど必要なところでのみ使うようにしました

https://github.com/yktakaha4/kunitori/tree/95a266e5539ab68841d02bb8875e465c4d34f055/test/testdata

GitHub Actionsで以下のように submodules を取得するようにした上で fetch-depth に0を指定しておくと、親側のリポジトリだけでなくサブモジュール側も全てのリビジョンを取ってきてくれるという仕様のようでした

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/.github/workflows/check.yml#L42-L55

バイナリバージョンの埋め込み

今回、versionコマンドを実行時にバイナリのバージョンとリビジョンを出力するようにしたのですが、これはldflagsで簡単に実現できました

$ ./kunitori version
Kunitori (国盗り)

Version: 0.0.2
Commit: 95a266e

ビルドのタイミングで以下のようにgoreleaserから指定すると、

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/.goreleaser.yml#L10-L13

コード側では以下に埋め込まれるので自由に使えます

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/cmd/kunitori/main.go#L19-L22

記事は以下がためになりました

https://kazuhira-r.hatenablog.com/entry/2021/03/08/003752

git blameが遅くて困った

当初、一番肝心の git blame がメチャ遅で挫折を覚悟したのですが、これはgit操作に利用している go-git がラッパーでなくpure goで書かれており、そのblameコマンドが最適化できていないことによるものでした

https://github.com/go-git/go-git/issues/14

解消方法としては、環境変数の設定状態に応じてgitコマンドを exec.Commandで実行できるようにして、gitコマンドがある環境ではそちらを利用可能にすることで高速化しました

https://github.com/yktakaha4/kunitori/blob/95a266e5539ab68841d02bb8875e465c4d34f055/pkg/git.go#L310-L313

ライブラリを使う側からすると実行環境に依存しないOSSは魅力的ですが、今回のような比較的ピーキーな使い方をしたいケースではラッパー形式のライブラリのほうが不確実性を低減できる場合もあると体で理解できたのはよかったです

Goの正規表現の仕様

今回、正規表現を使ってコードの集計対象を指定できる機能を作ったのですが、利用していて否定先読みを使うことができず困ることがありました

理由としては以下記事で詳しく説明されていますが、Goで実装している正規表現のアルゴリズムが、PerlやPythonで使われているPCRE系のものと異なっていて、それにより利用ができないということのようでした

https://medium.com/eureka-engineering/984b6cbeeb2b

個人的にPCREに慣れていたこともあり、最終的に標準ライブラリのregexpでなく dlclark/regexp2を利用することとしました
開発終盤にリファクタしたため性能に影響するかなとも思ったのですが、今回のユースケースではそこまで気にならなかったのでヨシとします

https://github.com/dlclark/regexp2

おわりに

Goでそこそこのサイズのものを書くいい機会となってよかったです🐤
今後も色んなリポジトリを可視化して笑顔になりたいと思います

明日(公開時点でもう今日ですが、)のアドベントカレンダーもお楽しみに

https://qiita.com/advent-calendar/2022/lapras

https://qiita.com/advent-calendar/2022/go

Discussion

ログインするとコメントできます