git logコマンド 分解解説、そこから派生する私の定量化に対する思い
アジャイルにまつわるエトセトラ#13
これはウイングアーク1st Agile and DevOps Transformation Stories のAdvent Calendar 2023、2023年12月18日の投稿です。
長くなりがちな git log コマンドを分解して解説する
12/1に続いて再登板のあらかーです。
今年は業務で Git の統計情報を取得して可視化を行う機会があり、git log コマンドについてインターネットで調べる事がしばしばあったのですが、なかなか深く説明されているサイトがなかったので、実際のコマンドを例にしながら、備忘も兼ねてわかりやすく解説していきたいと思います。
どういったデータを取得したか
今回は、任意のGitリポジトリ内の特定の期間に行われた変更(追加行と削除行とその合計行)の月次統計を全件取得し、それらをAuthor(作者)ごとにリスト形式でターミナル上に標準出力するコマンドを記述しました。
期待するアウトプットの形式はこちら。
Author1 2023/11 269 260 9
Author2 2023/11 8117 7391 726
Author3 2023/11 22 14 8
Author2 2023/11 11 8 3
Author1 2023/11 1873 1069 804
Author5 2023/11 13990 12247 1743
Author1 2023/11 0 0 0
Author3 2023/11 597 454 143
・
・
・
そしてそれを実現するコマンドがこちら。長い…
git log --numstat --pretty=format:'%aN' --since=2023/11/01 --until=2023/11/30 | awk '/^[[:digit:]]/ {plus+=$1; minus+=$2; step+=$1+$2} /^[A-Za-z]/ {if (author != "" && author != $0) {print author,"2023/11",step,plus,minus; step=plus=minus=0;} author=$0} END {if (author != "") print author,"2023/11",step,plus,minus}'
お手元にgitからクローンしたリポジトリがあれば実際に試してみてください。
このまま投げてもずらっとデータが並ぶはず。
ちなみに「追加行と削除行を取っているけど、修正された行は?」と疑問に思われる方がいるかもしれませんが、gitは修正された行をこう解釈します。
- 元の行を「削除された」としてカウント
- 新しく修正された行を「追加された」としてカウント
つまり修正行は追加行と削除行で表現されるため、このリクエストでエンジニアの関わったコードのステップ数は取得できるとみなすことができます。
コマンド展開 ”分解解説”
それぞれのコマンドおよびオプションにどんな役割があるか、分解して解説していきます。
git log — numstat — pretty=format:’%aN’ — since=2023/11/01 — until=2023/11/30
- git log :Gitリポジトリのコミット履歴を表示するコマンド。全ての始まり。
- — numstat:各コミットにおける追加された行数と削除された行数を取得する時に使用します。
- — pretty=format:’%aN’:コミットの作者名を表示するカスタムフォーマットです。Author Name のaN。
- — since=2023/11/01 — until=2023/11/30:日付指定(説明不要かと)
日付の指定方法は他にも「— since=”2 weeks ago”」や「 — since=”1 month ago”」「 — since=”last Monday”」のように自然言語的な指定もできます。
「 — until=”now”」と組み合わせれば柔軟にデータを持ってこれそう。
〜ここから先はスクリプト〜
ここまでが git log コマンドの基本の型です。
以降は「|」でつないでawkスクリプトが続き、先のコマンドで取得した値を整形し、見たい情報を見やすく標準出力する命令を続けます。
awk
「awk」というのは文字列の処理に使用されるプログラミング言語です。
今回のケースでは git logで取得したデータの処理を行います。
/^[[:digit:]]/ {plus+=$1; minus+=$2; step+=$1+$2}
正規表現の前提知識が必要ですが「/^[[:digit:]]/」の箇所は「数字で始まる」を意味しています。今回のケースだと git logコマンドの「 — numstat」で取得できた追加行数と削除行数ですね。
$1の追加行数、$2の削除行数をそれぞれ、plusとminusに加算していき、stepには両者を足した値を加算している処理となります。
/^[A-Za-z]/ {if (author != “” && author != $0) {print author,”2023/11",step,plus,minus; step=plus=minus=0;} author=$0}
こちらも正規表現「/^[A-Za-z]/」に対して処理を行っています。
これは英字で始まることを指す書き方です。
次のIF文の条件は「Authorが空ではなく、かつ今処理しているAuthorと異なる文字が入っている場合」という意味です。
この条件が真の場合、今のカウントを出力して、全ての値を0にリセットして処理を実行してねという処理が書かれています。
END {if (author != “”) print author,”2023/11",step,plus,minus}
「END」はawkの処理対象が最後に達した時に実行される処理という印です。
処理すべきデータが最後に達した際に、Authorが存在していれば最終レコードとして各値を出力するというクロージング作業的な箇所となっています。
csvファイルに書き出すpythonスクリプト
このgitコマンドでは対象月を直近の11月にしていますが、一度の実行で各月のデータを取得するために少しツール化しておきたいところです。
また、出力内容もこのままでは可視化する時に不便なのでcsv形式で外部出力できたらあとあと便利でしょう。
そこでひとまず実際にデータを取り始めた今年の8月から12月までの情報を取得し、ヘッダ情報付きのcsvに出力するpythonスクリプトを作成しました(所々の直値はご容赦を。あとで直します←)。
7行目のgit_directoryにディレクトリパスを入れれば皆さんの環境でも動作するはずです。
<余談>
pythonでコマンドを実行できるsubprocessモジュールは便利ではあるのですが、今回gitコマンド行の変数化に非常に手こずった思い出があります。
機会があればおいおい分解解説できたらと思います。
import subprocess
import csv
import os
from datetime import datetime
# 移動するディレクトリのパス
git_directory = "リポジトリのあるディレクトリパス"
os.chdir(git_directory)
# スクリプトの現在の実行ディレクトリを取得
current_directory = os.path.dirname(os.path.realpath(__file__))
# 出力ファイルのパスを設定
output_file = os.path.join(current_directory, 'git_stats.csv')
# 現在の年と月を取得
current_year = datetime.now().year
current_month = datetime.now().month
# 出力をCSVファイルに書き込む
with open(output_file, 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow(['author', 'date', 'step', 'plus', 'minus'])
# 2023年8月から現在の月までのデータを取得
for year in range(2023, current_year + 1):
for month in range(1, 13):
if year == current_year and month > current_month:
break # 現在の月より先には行かない
if year == 2023 and month < 8:
continue # 2023年の8月より前はスキップ
git_command = f"""git log --numstat --pretty=format:'%aN' --since={year}/{month:02d}/01 --until={year}/{month:02d}/31 | awk '/^[[:digit:]]/ {{plus+=$1; minus+=$2; step+=$1+$2}} /^[A-Za-z]/ {{if (author != \"\" && author != $0) {{print author,\"{year}/{month:02d}\",step,plus,minus; step=plus=minus=0;}} author=$0}} END {{if (author != \"\") print author,\"{year}/{month:02d}\",step,plus,minus}}'"""
try:
output = subprocess.check_output(git_command, shell=True, text=True)
except subprocess.CalledProcessError as e:
print(f"コマンド実行中にエラー発生: {e}")
continue
# 出力をCSVに書き込む
for line in output.strip().split('\n'):
if line.strip() == "":
continue
parts = line.split(' ')
if len(parts) < 5:
continue
author = " ".join(parts[:-4])
date, step, plus, minus = parts[-4:]
writer.writerow([author, date, step, plus, minus])
print(f"CSVファイル作成完了!: {output_file}")
これからのこと
とまあここまでは、git logコマンドとそのデータ加工のお話。
ここから先はその後の展望と、私個人の今年の締めくくり的なお話が少しできればと思います。
いま私は、前章の要領で作成したcsvを、ウイングアークが誇るBIダッシュボードMotionBoardで可視化するというところに取り掛かっています。
私はウイングアークの帳票系のQA出身のため、MotionBoardのユーザー側の操作経験はあったものの、本格的にボードの設計・作成をしたのは初めてなのですが、とてもカスタマイズ性が高く面白いです。多くのお客様に支持されている理由がわかりました。
さて、gitの情報をMotionBoardを可視化するというと、こういったサービスが既に存在します。
ただ、私が今やろうとしていることは、Gitの他にもJIRAのデータなどと組み合わせて、シンプルに閲覧できるボードの開発なので、上記のサービスなどを参考にしながらひとまず自前でカスタマイズ性の高い仕組みを目指して取り組んでいこうと思ってます。
その取り組みの過程や結果については、こちらのブログで報告していきますので「乞うご期待!」ということで。
私の定量化に対する思い
私はSPQI部内でここ数年プロセス改善に関わっているのですが、プロセス改善って「うまくいってない」「なんかもやもやする」といった定性的なところから始まって、色々と手を打って「こういうところを改善しましょう」でいったん終わったとしても、その後の効果っていうところが結局「前よりうまくいってます」「あんまり変わりませんでした」というふわっとした評価で終わるという『定性のループ』から抜け出せないことにジレンマを感じていました。
例えば今回の git 系の話になぞらえると「チームの開発ペースが遅い気がする」「あるエンジニアがあまり元気がない」「調子が悪そうだ」という感覚的な課題が持ち上がった場合、git におけるソースコードの変化の推移などがすぐに見える状態にあれば、その感じたことが”事実”なのかどうか判断材料の一つとして活用できるわけです。何かしら手を打った後の効果の評価についても同様です。
今回、データの収集→集計→可視化をするといった機会と出会ったこと、そしてそれを一人称で実行できたことで、様々な課題やトピックの定量化という課題に対して以前より大胆に動いていけるという自信を獲得できたと感じています。
それではまた。