Gitリポジトリのファイル変更履歴を可視化してみる


はじめに

TIS Advent Calendar 2019 16 日目の記事です!

プログラムを書いているとき、あのファイルってどのコミットで変えたんだっけ……みたいのを git でたどると思いますが、ファイル名を変えたり場所を変えたりしていて前のコミットだとどこの何だったか??? ってなったりしませんか? 私はなります。というか git log のオプション覚えられなくないですか……? あと、このプロジェクト「全体」がどう育ってきたのか見てみたいなーとか思ったりするわけですよ。

せっかく最近 D3.js を使って図を作る話なんかをやっていたので、ここはひとつ D3.js 修行もかねてそういうのを可視化するツールを作ってみようと思ったわけです。ネタの方向性としては (1) 作ってみたもの (2) その中での D3.js の使い方や工夫のポイント、というのがあるんですが、今日は前者・作ってみたものの話をします。

何を作ったのか?

gh-file-grass というツール をつくってみました。Git repository の情報を可視化するツールなので、このツール自身のロポジトリ情報を可視化してみましょう。こんな感じになります。

可視化されている要素

横軸

横軸は時系列 (commit 単位) です。マウスオーバーで各 commit の基本情報が出ます。ちなみに commit hash をクリックすると GitHub のそのコミットのページを開きます。

Commit hash の下にある棒グラフは各コミットの変更量 (変更した行数の合計) をグラフ化したものです。後でまた出てきますが、変更行数の幅が大きいので、対数グラフになっています。 (スペース的な都合で) 目盛りを入れていないので、オーダーの違いを色の違いとして表現しています。10行・100行・1000行……と桁が変わると棒グラフの色が変わるってことですね。コミットごとにどのくらいの変更が加わっているのかの目安・傾向を見るものです。

縦軸

縦軸はファイルです。ファイル名の横に表示されている棒グラフは commit 数を表しています。より頻繁に変更されているファイルは何かを表示しているものです。ファイル名をクリックすると GitHub のそのファイルのページを開きます。1

表本体: Commit/File ごとの変更情報

問題はこの表の中身ですね。各 commit/file ごとに変更情報 (insertion, deletion) とファイル変更のトラッキングをしています。

このとき緑色の四角(■; ここでは "stat" と呼びます) はそのコミットで変更があったファイルとその情報を表しています。グレーのものはファイルとしては存在しているけど変更がない状態です。"stat" がないところではそのファイルは存在していません。これもクリックすると、GitHub でそのコミット時点のファイルを開きます。

あるコミットに着目して縦にみると、そのコミットの時点で存在していたファイル・変更したファイルがわかります。また、あるファイルに着目して横に見ると、そのファイルがいつ作成され、どのコミットでどれくらい変更されたのかという変更履歴を追いかけることができます。

また、"stat" をつなぐ線がありますが、これは diff hash の値をもとに変更前後の "stat" をトラッキングしてつないでいるものです。これによってファイル名の変更なども追跡できています。例を見てみましょう。

上の図では プロジェクトディレクトリ直下にある log_corrector.rbbin/ 下に移動 (git mv) されています。単純な移動だけだとファイル内容の変更がないので、diff hash によるトラッキング上はその先にあるファイル内容の変更が発生した "stat" につながっています。(そしてこのファイル、"correct" と "collect" を間違えていてさらにファイル名を変えるというちょっと恥ずかしい履歴が可視化されてます。)

"芝生" の色を付ける

"stat" によって表される「いつ・どこで・何を・どれくらい」変更したのかを GitHub 本家の "芝生" (日次のコミット数で色分け) にならって 4 段階の緑に色分けして可視化しています。問題は、ファイルの変更量 (変更行数) はばらつきが大きいので、単純に変更行数の範囲指定するだけでは "stat" 全体をうまく色分けできないことです。ファイルによって、1 行の増減から 10000 行オーダーの増減まであるので、何かしらの分類方法を考えないといけない。

(A) ランキング方式

手っ取り早いのは、単純にすべての "stat" を変更量の合計でソートして、上位から 1/4 ごとにグルーピングするという方法ですね。

この方法では、どんなリポジトリに対しても、ある色の "stat" の割合は常に一定になります。ファイルの変更量のオーダーによらず一定のルールで色分けできるというメリットはあるのですが、デメリットもあります。というのは、変更量の多い少ないと色分けが感覚的に合わないことがあるんですよね。似たような変更量のものも違う色で分類されてしまうとか、かなり変更量が違うのに同じ色で分類されてしまうといったものが出ます。

(B) 変更量分布に応じた分類

(A) のデメリットを解消しようと思うと変更量に応じた分類をしなければいけない。オーダーの違う数をそろえるといえば対数です。"stat" ごとの変更量を対数にとって、同じような変更量のものをまとめてから 1/4 ずつ分けていく方法を実装しています。言葉で説明するのが難しいので図で。感覚的なとらえかたをすれば、変更量が 1 桁のもの・2 桁のもの・3 桁のもの……と、オーダーごとにまとめる形です。

分布については適当ですが、ファイルサイズが 100 行オーダーのものが多いので 50 行くらいの変更が多いかなーくらいの図です。package-lock.json とか機械生成のファイルは 1000 行オーダーとかにすぐなるんですよね。分布をとってから分布範囲を分割することで変更量に応じた色分けができるだろうってことですね。

やってみよう

(A)(B) 両方やってみました。左が (A), 右が (B) です。……まあこれだけ見てもわかりにくいな……。

ちなみに実際の (B) 分布はこんな感じでした 2 。1 桁行の変更と 40 (10^1.6) 行前後の変更が多いですね。

log(lines)=0 : *******************
log(lines)=3 : ***************
log(lines)=4 : ********
log(lines)=6 : **************
log(lines)=7 : ********
log(lines)=8 : ****
log(lines)=9 : *******

log(lines)=10 : *******            <= 10行
log(lines)=11 : *******
log(lines)=12 : *********
log(lines)=13 : ***********
log(lines)=14 : ************
log(lines)=15 : ********

log(lines)=16 : *************
log(lines)=17 : *************
log(lines)=18 : ***
log(lines)=19 : ********
log(lines)=20 : ****               <= 100行
log(lines)=21 : *****

log(lines)=22 : **
log(lines)=23 : **
log(lines)=24 : *
log(lines)=29 : *                  <= 1000行あたり
log(lines)=31 : *
log(lines)=36 : *
log(lines)=40 : *                  <= 10000行

過去の操作履歴をたどる

全体の変更履歴・傾向の変化

図で「一覧する」と全体の動きがある程度見えてきます。このツール自身のリポジトリでは、全体に左上→右下に変更対象が移っていっている様子が見えます。まあファイル構成とかにもよるので一概にはいえないですが、たいてい機能の近いものは同じディレクトリ・似たようなファイル名にっていると思うので、関連したもののところに変更が寄ってくる……というのが出てくるんじゃないかと。

大きな変更の発生

例えば、このコミットでは一度にいくつもファイルが追加されています。

コミットログを見るとわかりますが、この時点で Vue.js アプリのファイルがまとめて追加されています。最初はデータ収集用スクリプト (ruby) から作って、後から WebUI 追加している様子がわかります。ほかにも、Gemfile とか package[-lock].json の変化をたどることで、使っているモジュールやライブラリなどの変化タイミングなどを追いかけられたりもしますね。

移動や分割の履歴

ファイルの移動やリネームなんかについては上でふれました。ほかには、例えばこれなんですが、

最初のころはひとつのファイルに全部突っ込んでいたんだけど、行数が増えてメンテしにくくなったので、クラスごとに別のファイルに分割したものですね。ひとつのファイルでずっと書いていて、ところどころ分割しているというのが現れています。その場その場で作っていることがうかがえます。

しばらく更新していないドキュメント

いくつか機能変更してるのに README.md ぜんぜんドキュメントを更新してないなーなんてことも見えるわけです。

おわりに

Git リポジトリのファイル変更履歴を可視化するツールを作ろう (その中で D3.js 修行をしよう) というのをやってみました。D3.js (じゃなくてもいいけど何かしらの可視化のライブラリ) が使えるようになるとこういうトライアルをいろいろできるのがいいですよね。こうして可視化してみると、リポジトリに対して発生した「イベント」が何かと見えるようになります。「見てわかるようになる」というの重要だなと。まあこのツール、大量のファイル・履歴があるプロジェクトだと描画オブジェクト多くなりすぎてえらい重くなるんですけどね……。

こういうのを作る上で D3.js でどんなことをやっているのか……という要素技術のネタもあるので、そのあたりもそのうち出していけたらと思います。

このツール使ってみたい場合は README.md にセットアップ方法とか書いてあるので見てください。


  1. 時点 (commit) 指定は入れていなくて HEAD 前提で開くので、deleted なファイルは 404 になっちゃうけど。 

  2. 実装上は 10 * log10(lines) してから小数点以下切り落として 2 桁の整数で分布をとっています。