Gitリポジトリの歴史をlibgit2 / nodegit で可視化する

これはリブセンス Advent Calendar 16日目の記事です。
[マッハバイト](https://j-sen.jp/)でエンジニアリングマネージャーをやっている @highwide がお届けします。

## 今回やりたいこと

Gitリポジトリのコミット履歴の集計と可視化です。

### その経緯

マッハバイトは、当初「ジョブセンス」というサービス名で12年前にリブセンスの創業事業として立ち上がったビジネスなので、比較的長命なWebサービスと言えると思います。
ですので、基幹事業としてビジネスの要請に応えながらも、技術的負債を返し続ける必要があるという、エンジニアからすると難しいミッションを持った事業です。

そういった経緯の中で、比較的レガシーを引きずるPHPリポジトリと、既存ロジックをリニューアルしながらRailsに移行しているリポジトリが4年前から存在しています。
つまり、PHP側のリポジトリに関しては「コードベースが小さくなっていってほしいリポジトリ」と言うことができます。

では、マッハバイトのリポジトリのcommit履歴からそれを可視化することができないだろうか、と考えました。
個人的には、僕がアルバイト事業にJOINしたのは2017年1月なので、それ以前の歴史をより知る意味でも、有用かなと思いました。

「じゃあ `github.com/${Organization}/${Repository}/graphs/contributors` を見ましょう、ハイ終了！」とならないのが今回のポイントです。
※ リブセンスではGitHubを利用しているプロダクトもあるのですが、マッハバイトはBitBucket Serverを利用しています。graph機能こそないものの、レビューの機能などは手厚くて、これはこれでいいところもあります。

## 方針

ひとまず、Gitのコミット履歴を `git show *** --stat` で見たときの `Insertions` と `Deleteions` を集計することでやりたいことができないだろうかという方針を立てました。
<img width="1088" alt="git_show_stat.png" src="https://qiita-image-store.s3.amazonaws.com/0/51449/e9bb5733-b6ff-d475-5925-e76b929fb7b9.png">

というわけで

```bash
git log --oneline | \
cut -d' ' -f1 | \
xargs git show --stat | \
ag \(insertion\|deletion\)
```

といったシェルのワンライナーを書きかけたのですが、さすがに後から集計しにくいので、もう少しgitリポジトリをプログラマブルな方法で扱う方法がないか調べました。

### libgit2 と nodegit

>  Libgit2は、他のプログラムへの依存性のないGitの実装であり、プログラムから使いやすいAPIを提供することにフォーカスしています。 

と、 [Gitオフィシャルのドキュメント](https://git-scm.com/book/ja/v2/Appendix-B%3A-Git%E3%82%92%E3%81%82%E3%81%AA%E3%81%9F%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E7%B5%84%E3%81%BF%E8%BE%BC%E3%82%80-Libgit2%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95#r_libgit2_bindings) にもあるとおり、libgit2を使えばやりたいことができそうだと気がつきました。

公式のサイト( https://libgit2.org/ )にある通り、様々な言語のバインディングが提供されています。
この手のことをやるときはいつもRubyで書いちゃうのですが、今回はもう少し不慣れな言語でやろうとnode.jsによるバインディングのNodeGit( https://www.nodegit.org/ )を使うことにしました。ドキュメントもとても充実しています。

## こんな感じでやりました

環境変数(dotenvを使いました)で指定したリポジトリがあるローカルパスに対して、コミット履歴を遡りながら、

* 日時
* HashDigest
* Author
* Email
* コミットメッセージのサマリー
* 追加行数
* 削除行数

を取得してcsv化するスクリプトが書けました。

```javascript
const fs = require('fs');
const nodegit = require('nodegit');
const path = require('path');
const dotenv = require('dotenv');
const stringify = require('csv-stringify');

const columns = {
  Date: 'Date',
  HashDigest: 'HashDigest',
  Author: 'Author',
  Email: 'Email',
  Summary: 'Summary',
  Additions: 'Additions',
  Deletions: 'Deletions',
};

dotenv.config();

function calcAdditions(patches) {
  const additions = patches.map(patch => patch.lineStats().total_additions || 0)
    .reduce(((sum, current) => sum + current), 0);
  return additions;
}

function calcDeletions(patches) {
  const deletions = patches.map(patch => patch.lineStats().total_deletions || 0)
    .reduce(((sum, current) => sum + current), 0);
  return deletions;
}

async function traverseParent(commit) {
  if (commit.parentcount() === 0) { return null; }

  const parentCommits = await commit.getParents();
  return parentCommits[0];
}

async function generateRow(commit) {
  const author = commit.author();
  const diffs = await commit.getDiff();
  const patches = await diffs[0].patches();
  return {
    Date: commit.date().toLocaleString(),
    HashDigest: commit.sha(),
    Author: author.name(),
    Email: author.email(),
    Summary: commit.summary(),
    Additions: calcAdditions(patches),
    Deletions: calcDeletions(patches),
  };
}

async function generateData(commit, data) {
  const row = await generateRow(commit);
  data.push(row);

  const parentCommit = await traverseParent(commit);
  if (parentCommit === null) { return data; }

  return generateData(parentCommit, data);
}

async function main() {
  const pathToRepo = path.resolve(process.env.gitFileDir);
  const repo = await nodegit.Repository.open(pathToRepo);
  const commit = await repo.getMasterCommit();
  const data = await generateData(commit, []);
  stringify(data, { header: true, columns }, (_err, output) => { fs.writeFileSync('output.csv', output); });
}

main();
```

※ 一応、 `eslint-config-airbnb-base` を補助的に使いつつも、特に不慣れなasync/awaitのあたり含め、node.jsの書き方的にはこなれてないかもしれません。

## 結果
というわけで、csvファイルが生成されました。
メインリポジトリの履歴を集計したcsvは2.2MBの大きさです...。

Spreadsheetで時系列によるライン数の増減をグラフ化してみます。

<img width="787" alt="スクリーンショット 2018-12-16 12.31.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/51449/5d3b0d02-4b56-60a6-5f9a-e43e75022c88.png">

0から積み上がっていくと思ってたのに、なんか違う...／(^o^)＼
初期コミットは2009年にsvnから1,189,121行をインポートするものでした...。
100万行...マジか...。

その後、テストに利用する10万行Overのマスターデータの増減などがあり、(少なくとも行数の増減だけ見ると)激動の時代が2013年10月頃まで続いた後は、比較的安定したコミットが積まれているようです。
(あとから、対象とするディレクトリやファイルを絞ればよかったと思いました)

そして、2017年ごろからようやく、コードベースがまともなかたちで小さくなり始めています。
レガシーコードのリニューアルプロジェクトは冒頭書いたように2014年頃から始まったものですが、コードベースを小さくしようとする動きが加速し始めたのが2017年頃だというのがわかりました。

「もっと小さくしていきたい...！」と思いつつ、このグラフだとわかりにくいですが、2017年からのコードの減り幅も5万行ほどあるので、使われていたロジックをこれだけ消すことに尽力してくれた同僚への頭が上がらない感もあります。
というか、その頃のリニューアルプロジェクトの甲斐あって、普段開発するWeb画面の大半がRails側に寄せられている実感があったので、それでもこれだけのコードが残っていることが意外でもあります...。

また、今回得られたデータを使ったり、ちょっとしたスクリプトの改修を行えば、個人ごとのContribution量や、1コミットあたりの粒度を見る(ビッグバンコミットへの抑止につながる)ことなどもできるので、おもしろい活用方法を考えていきたいと思います。

## 今後
最近、同僚に声かけてもらって、O'Reillyの[進化的アーキテクチャ](https://www.oreilly.co.jp/books/9784873118567/)という本の読書会に参加しているのですが、そこに「適応度関数」という言葉が出てきます。

> 「誘導的」という言葉は、アーキテクチャとして向かうべき方向、 あるいは示される目標が存在することを示している。そのため、我々は遺伝的アルゴ リズム設計で成功の定義に使われる「適応度関数(Fitness Function)」という進化的計算の概念を拝借する。進化的計算には、各世代のソフトウェアで起きる小さな変化によって解が徐々に現れてくるようにするための仕組みが多く含まれている。解は 最終目標に近づいたか、それとも遠く離れているか、といった具合に、エンジニアは解の世代ごとに現在の状態を評価する。

Neal Ford、Rebecca Parsons、Patrick Kua著、島田浩二訳 「進化的アーキテクチャ」 P19

たとえば、ここで行ったようなリポジトリのコミット履歴の可視化を「適応度関数」と捉え、CIにhookして起動するようにすれば、「レガシーなコードベースを小さくする」という方向を「誘導的」に目指す、といったことができるのではないかと考えました。

やや赤裸々に今の状況を書いてしまって少しドキドキしているのですが、マッハバイトは前向きに技術的負債と向き合い、よりよいシステム環境を目指していきます。