clocとシェルスクリプトでgit管理ソースの改修ステップ数を集計

## やりたいこと

世の中にはソースコードの行数を知りたがる人がいます。

Gitリポジトリから詳細なステップ数を取得するツールとして[cloc](https://github.com/AlDanial/cloc)があり、これだけで十分便利なのですが、一部のファイルはカウントから除外するとか、全体のステップ数と改修分のステップ数を出すとか、コメントを含めないのと含めたのとそれぞれ合算するとか、細かい集計を毎回手作業でやるのは地味に面倒です。

というわけで、シェルでJSONをいじる練習も兼ねて自動化してみました。

### アウトプットイメージ

最終的にこんなものを作ります。

```txt
■ロジックのみ
・追加：1000
・修正：1000
・流用：1000
---
・計　：3000

■コメント・空行を含む
・追加：2000
・修正：2000
・流用：2000
---
・計　：6000
```

行単位で、追加されたものを「追加」、変更されたものを「修正」、変更のないものを「流用」としてカウントします。

## 実行環境

* Windows 10 PC
    * 基本的にはLinuxでもできることしかやらないので、CI環境にもっていくこともできると思います
* git
* git bash（シェルの実行環境）
* cloc
    * clocコマンドが使えるようにPATHを通しておきます
    * Windows用のexeもありますが、npmからグローバルインストールすることも可能
* [jq](https://stedolan.github.io/jq/)
    * BashでJSONを扱うため
    * Windowsでは[scoop](https://github.com/lukesampson/scoop)などからインストールできます
   * PATHを通しておきます

## 概要

以下のようなことをやります。

* Gitリポジトリからクローン
* 全ステップ数、差分ステップ数をclocで取得し、json形式で出力
* jsonをjqに渡し、必要な情報だけ取り出して集計
* テキストファイルとして保存

## 事前準備

### ベースコミットのハッシュ値確認

改修のBeforeとなるgitコミットのハッシュ値を調べておきます。  

このコミットと最新コミット（HEAD）までの差分ステップ数から、追加／修正／流用を判定します。

### 言語識別キーの確認

対象のソースが配置されているルートディレクトリで clocコマンドを打ってみます。

例として[Prism](https://github.com/PrismLibrary/Prism)のソースであれば、以下のような結果が出力されます。

```
$ cloc ./
     895 text files.
     883 unique files.
     278 files ignored.

github.com/AlDanial/cloc v 1.78  T=3.01 s (278.4 files/s, 25393.2 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C#                             743          13034          11411          47593
MSBuild script                  39            143            106           2043
JSON                             4              0              0            623
XAML                            27             39              4            459
Markdown                         6            130              0            333
YAML                             8             40              0            182
XML                             10             49            128             64
PowerShell                       1             11              2             29
-------------------------------------------------------------------------------
SUM:                           838          13446          11651          51326
-------------------------------------------------------------------------------
```

左端の「Language」の中で、カウントの対象とする言語の表記を確認しておきます。
ここでは「XML」「C#」「XAML」を対象とすることにします。

カウントしたい言語が結果に出てこない場合、言語定義を自分で作成する必要があります。
詳細は[Create Custom Language Definitions](https://github.com/AlDanial/cloc#user-content-custom_lang)を参照。

### 無視リスト作成

一部のファイル／フォルダをカウント対象から除外したい場合、無視リストを作っておいて指定することができます。

ここでは stepignore.txt という名前で以下のようなファイルを作りました。

```
Sandbox/Windows10/SampleData
Sandbox/Xamarin
```

その他いろいろオプションがあるので詳しくは[README](https://github.com/AlDanial/cloc)を。

## シェルスクリプト

はじめに完成品を載せます。

```bash
#!/bin/env bash

# Gitリポジトリ
readonly GIT_REPO=https://github.com/PrismLibrary/Prism.git
# 差分比較元のコミットハッシュ
readonly BASE_COMMIT=2a89100f
# 収集対象言語
readonly LANGS='["XAML","C#","XML"]'

# リポジトリから一時フォルダへclone
git clone $GIT_REPO ./_work
cd _work

# clocでステップ数を取得
step_total=`cloc --vcs=git --json --exclude-list-file=../stepignore.txt`
step_diff=`cloc --json --exclude-list-file=../stepignore.txt --diff $BASE_COMMIT HEAD`

# 一時フォルダの削除
cd ..
rm -rf ./_work

# 全体のステップ数（ロジック、コメント）
logic_total=`echo $step_total | jq '[ .'$LANGS'.code ] | add'`
comment_total=`echo $step_total | jq '[ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`

# 追加・修正分のステップ数（ロジック、コメント）
logic_added=`echo $step_diff | jq '.added | [ .'$LANGS'.code ] | add'`
logic_modified=`echo $step_diff | jq '.modified | [ .'$LANGS'.code ] | add'`
comment_added=`echo $step_diff | jq '.added | [ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`
comment_modified=`echo $step_diff | jq '.modified | [ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`

# 流用分を算出
logic_same=$((logic_total-logic_added-logic_modified))
comment_same=$((comment_total-comment_added-comment_modified))

# 出力
{
  echo "■ロジックのみ"
  echo "・追加："$logic_added
  echo "・修正："$logic_modified
  echo "・流用："$logic_same
  echo "---"
  echo "・計　："$logic_total
  echo ""
  echo "■コメント・空行を含む"
  echo "・追加："$((logic_added+comment_added))
  echo "・修正："$((logic_modified+comment_modified))
  echo "・流用："$((logic_same+comment_same))
  echo "---"
  echo "・計　："$((logic_total+comment_total))
}>./step-result.txt
```

以下で解説します。

### 定数

以下は環境に合わせて書き換えます。
コミットハッシュと対象言語は事前準備で調べたものを使用してください。
ハッシュは頭4ケタもあれば普通は十分らしいです。念のため8ケタ書きました。

```bash
# Gitリポジトリ
readonly GIT_REPO=https://github.com/PrismLibrary/Prism.git
# 差分比較元のコミットハッシュ
readonly BASE_COMMIT=2a89100f
# 収集対象言語
readonly LANGS='["XAML","C#","XML"]'
```

LANGSはカンマ区切りの部分にスペースを入れるとダメっぽいので注意。

### clocでステップ数を取得

全体のステップ数と、ベースコミットからの差分とを順に取得しています。

```bash
step_total=`cloc --vcs=git --json --exclude-list-file=../stepignore.txt`
step_diff=`cloc --json --exclude-list-file=../stepignore.txt --diff $BASE_COMMIT HEAD`
```

先ほど作った無視リストを `--exclude-list-file` オプションで指定しています。

totalのほうで `--vcs=git` を指定する代わりに `cloc ./` という感じでディレクトリを指定することもできるのですが、その場合は `.gitignore` を考慮してくれないので注意が必要です。

それと、 `--diff` で実行すれば same, modified, added(, removed) が出力されるので、これらを合算すればトータルステップ数になりそうなものなんですが、この[sameの判定がちょっと怪しく](https://github.com/AlDanial/cloc/issues/349)て、合算しても行数が一致しません。

なので、トータルステップ数は別で取得し、そこからadded,modifedを引いたものを流用分とカウントすることにします。

### 全体のステップ数を取り出す

全体ステップ数を取得するclocコマンドの戻り値は以下のような形になります。

```json
{
  "header": {
    "cloc_url": "github.com/AlDanial/cloc",
    "cloc_version": "1.78",
    "elapsed_seconds": 2.93183207511902,
    "n_files": 838,
    "n_lines": 76423,
    "files_per_second": 285.828102882046,
    "lines_per_second": 26066.6361653396
  },
  "C#": {
    "nFiles": 743,
    "blank": 13034,
    "comment": 11411,
    "code": 47593
  },
  "MSBuild script": {
    "nFiles": 39,
    "blank": 143,
    "comment": 106,
    "code": 2043
  },
  "JSON": {
    "nFiles": 4,
    "blank": 0,
    "comment": 0,
    "code": 623
  },
  "XAML": {
    "nFiles": 27,
    "blank": 39,
    "comment": 4,
    "code": 459
  },
  "Markdown": {
    "nFiles": 6,
    "blank": 130,
    "comment": 0,
    "code": 333
  },
  "YAML": {
    "nFiles": 8,
    "blank": 40,
    "comment": 0,
    "code": 182
  },
  "XML": {
    "nFiles": 10,
    "blank": 49,
    "comment": 128,
    "code": 64
  },
  "PowerShell": {
    "nFiles": 1,
    "blank": 11,
    "comment": 2,
    "code": 29
  },
  "SUM": {
    "blank": 13446,
    "comment": 11651,
    "code": 51326,
    "nFiles": 838
  }
}
```

これをjqコマンドに渡して、対象言語のステップ数を集計します。

```bash
logic_total=`echo $step_total | jq '[ .'$LANGS'.code ] | add'`
comment_total=`echo $step_total | jq '[ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`
```

jqの構文はちょっとクセがありますが、 `logic_total` は C#,XML,XAMLの `code` の値を取ってきて合算したもの、 `comment_total`は `comment` と`blank` の値を取ってきて合算したものになっています。

### 追加・修正のステップ数を取り出す

差分のclocコマンドの戻り値は以下のような形になります。

```json
{
  "header": {
    "cloc_url": "github.com/AlDanial/cloc",
    "cloc_version": "1.78",
    "elapsed_seconds": 5.76067495346069,
    "n_files": 322,
    "n_lines": 31889,
    "files_per_second": 55.8962278901989,
    "lines_per_second": 5535.63605959799
  },
  "added": {
    "XAML": {
      "blank": 6,
      "code": 95,
      "nFiles": 2,
      "comment": 0
    },
    "JSON": {
      "blank": 0,
      "code": 498,
      "nFiles": 1,
      "comment": 0
    },
    "XML": {
      "blank": 23,
      "code": 15,
      "nFiles": 3,
      "comment": 59
    },
    "Markdown": {
      "comment": 0,
      "nFiles": 1,
      "blank": 24,
      "code": 118
    },
    "YAML": {
      "comment": 0,
      "nFiles": 0,
      "blank": 4,
      "code": 14
    },
    "MSBuild script": {
      "comment": 19,
      "nFiles": 2,
      "blank": 11,
      "code": 324
    },
    "C#": {
      "comment": 455,
      "code": 3365,
      "blank": 657,
      "nFiles": 67
    }
  },
  "same": {
    "XAML": {
      "blank": 0,
      "code": 43,
      "nFiles": 0,
      "comment": 0
    },
    "JSON": {
      "blank": 0,
      "code": 0,
      "nFiles": 0,
      "comment": 0
    },
    "XML": {
      "blank": 0,
      "code": 9,
      "nFiles": 0,
      "comment": 40
    },
    "Markdown": {
      "comment": 0,
      "nFiles": 0,
      "blank": 0,
      "code": 130
    },
    "YAML": {
      "comment": 0,
      "nFiles": 0,
      "blank": 0,
      "code": 106
    },
    "MSBuild script": {
      "comment": 60,
      "nFiles": 0,
      "blank": 0,
      "code": 1138
    },
    "C#": {
      "comment": 1177,
      "code": 13850,
      "blank": 0,
      "nFiles": 0
    }
  },
  "modified": {
    (略)
  },
  "removed": {
    (略)
  },
  "SUM": {
    "added": {
      "nFiles": 76,
      "code": 4429,
      "blank": 725,
      "comment": 533
    },
    "same": {
      "nFiles": 0,
      "code": 15276,
      "blank": 0,
      "comment": 1277
    },
    "modified": {
      "nFiles": 178,
      "code": 2822,
      "blank": 0,
      "comment": 17
    },
    "removed": {
      "nFiles": 68,
      "code": 4184,
      "blank": 832,
      "comment": 1794
    }
  }
}
```

これをjqに渡し、addedとmodifiedをそれぞれ取り出します。

```bash
logic_added=`echo $step_diff | jq '.added | [ .'$LANGS'.code ] | add'`
logic_modified=`echo $step_diff | jq '.modified | [ .'$LANGS'.code ] | add'`
comment_added=`echo $step_diff | jq '.added | [ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`
comment_modified=`echo $step_diff | jq '.modified | [ .'$LANGS'.comment,.'$LANGS'.blank ] | add'`
```

### 出力

書式はお好みで。

```bash
{
  echo "■ロジックのみ"
  echo "・追加："$logic_added
  echo "・修正："$logic_modified
  echo "・流用："$logic_same
  echo "---"
  echo "・計　："$logic_total
  echo ""
  echo "■コメント・空行を含む"
  echo "・追加："$((logic_added+comment_added))
  echo "・修正："$((logic_modified+comment_modified))
  echo "・流用："$((logic_same+comment_same))
  echo "---"
  echo "・計　："$((logic_total+comment_total))
}>./step-result.txt
```

以上です。