Gitのコミットログから開発属人性を定量化する方法と品質向上への活用事例 - KAKEHASHI Tech Blog

KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Gitのコミットログから開発属人性を定量化する方法と品質向上への活用事例

AI在庫管理の開発チームのバックエンドエンジニアのもっち(@mottyzzz)です。今回は、AI在庫管理の開発において、Gitのコミットログから開発属人性を可視化して品質向上を実施していく箇所の優先順位をつけた事例を紹介します。

この記事は秋の技術特集 2024の 16 記事目です。

背景

AI在庫管理のプロダクトは、医薬品の在庫を効率的に管理するためのサービスです。 このシステムは3年前にリリースされ、その後も継続的に機能追加とバグ修正を重ねてきました。 その開発の過程で、次のような問題が生じてきていました。

  • システムの複雑化:プロダクトが成長しチームが拡大するにつれて、プロダクトやシステムが複雑化
  • 経験の偏り:一部の開発者だけが特定のドメイン知識や設計を理解している
  • インシデント対応の困難さ:システム全体を理解している人が少ないため、問題が発生したときの対応難易度の増加
  • 新メンバーの参画障壁:新しく加わった開発者がコミットするまでの時間の増加

このままでは、システムの長期的な運用と保守に支障をきたす可能性がありました。 そこでシステムの理解しやすさを向上し、効率的に改善や保守ができるようにしたいと考えました。

優先順位をつけて無理なくコツコツ取り組めるアプローチ

システム全体の理解容易性を向上させるという大きな目的に対して、どこから手をつけるべきか検討しました。

システムが大規模化しているため、全体を一度に改善することは現実的ではありませんでした。 もっとも重要または問題の多い部分から着手し、徐々に他の部分に拡大していくアプローチにすることで、 今後も機能開発を行いながらも改善を進められると考えました。

次に、具体的にどのようにして優先順位を決定したかを説明します。

Gitのコミットログに着目

Gitのコミットログに着目した理由を下記に説明します。

  • 客観的、かつ、アクセスが容易なデータソース: コミットログは開発活動の記録であり、主観的な印象に左右されません。また、すぐに情報を取得できます。 誰が、いつ、どのファイルを変更したかという具体的な情報を定量化して分析することで発見があると考えました。

  • 時系列で変化を確認できる: 長期間にわたる開発の傾向を追跡できたり、特定の時点の情報も取得できたりするので、 プロダクトが成長し、チームが大規模になる過程でのチームメンバーの変化を時間軸で分析できます。

  • ファイル単位での分析ができる: ファイル単位で細かく確認ができるため「暗黙の専門家」を特定し、クロスファンクショナルな開発が行われているか、あるいはサイロ化が進んでいないかを確認できます。 また、ファイル単位で確認できるため、他のメトリクスとセットで分析できます。

今回の取り組みでは、大まかな偏りを確認したかったため、開発者ごとのコミット粒度の違いや変更行数などは考慮していません。

Gitのコミットログで開発者の偏りを可視化してみました

手順の説明の前に、AI在庫管理で実際に可視化した結果はこのような形になりました。 直近一年間のコンポーネントごとの開発者のコミット数の割合をヒートマップで可視化しました。

行がコンポーネント、列が開発者で、セルの値がコンポーネントに対する開発者のコミット数の割合(0〜1)を表していて、 色が濃いほどそのコンポーネントに対してその開発者が多くの変更を行っていることを示しています。

Gitのコミットログの可視化の実例

結果の解釈と活用

  • 補完的な指標として活用: この分析結果は、改善の優先順位を決定する際のひとつの指標として使用しています。 実際にはこの結果だけで判断したわけではなく、以下の要素も考慮して優先度を決定しました。

    • 各業務領域や機能の重要度
    • システム全体における各コンポーネントの影響度
    • 現在の開発計画
    • 複雑度などの他のコードメトリクス
  • 直感の定量化、仮説の裏付け: この分析により、「このコンポーネントって◯◯さんじゃないと開発できないよね」など普段から感じていたものを数値として可視化することができました。 また、上の例では直近一年間のコミット数の割合を見ていますが、時系列変化も確認していて、上手に知識の共有や分散が進んでいるコンポーネントはどれかなども確認することができました。

  • バランスの取れた開発体制の構築: あくまでも「偏りをゼロにする」ことは考えていませんでした。 各開発者の専門性を活かしつつ、知識の適切な共有と分散による「健全な多様性」を持ちながら、バランスよく全体の理解容易性を向上させることのほうが重要だと考えていました。

Gitのコミットログを取得し可視化するまでの流れ

それでは実際にGitコミットログ取得から、集計して可視化していく流れを下記の流れで説明します。 以降の手順の出力結果は、本ブログ用に作成したサンプルの結果となっています。

  • Gitのコミットログの取得
  • コミットログのクレンジングと分析対象の決定
  • コミットログの集計
  • 結果の可視化

前提条件

この手順で使用したツールとライブラリ、およびそのバージョンを下記に示します。 Jupyter Notebookを使うことで、スムーズに分析の試行錯誤を実施できます。

  • Gitクライアント: 2.45.1
  • Python: 3.12.1
  • pandas: 2.2.2
  • seaborn: 0.13.2
  • tqdm: 4.66.5
  • jupyter: 1.0.0

必要なPythonライブラリをインポートします。

import pandas as pd
import subprocess
from pathlib import Path
import datetime as dt
import seaborn as sns
import pytz
import csv
import io
from tqdm import tqdm

Gitのコミットログの取得

まず、Gitコマンドを実行するためのヘルパー関数を定義します。

def _run_command(
    cmd: str, current_dir: Path, encording: str = "utf-8"
) -> list[str]:
    p = subprocess.Popen(
        cmd,
        cwd=current_dir.as_posix(),
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    out, err = p.communicate()
    if p.returncode != 0:
        encoded_stderr = err.decode(encording)
        raise ValueError(f"Error running command: {cmd}, cause: {encoded_stderr}: ")

    encoded_stdout = out.decode(encording)
    return encoded_stdout.split("\n")

次に、この関数を使用してGitコミットログを取得します。

# Gitコミットログを取得する対象のGitリポジトリのディレクトリを指定
target_git_repo_dir = Path("/path/to/your/git/repo/directory/")
target_file_paths = [Path(f) for f in _run_command("git ls-files", target_git_repo_dir)]

def _parse_csv_line(csv_line: str): 
    # CSV形式の文字列をカンマ区切りでリストに変換する
    return list(csv.reader(io.StringIO(csv_line)))[0]

commitlogs = []

# git ls-filesでリポジトリ内のファイルごとにgit logを取得する
for git_file_path in tqdm(target_file_paths):

    # git logコマンドにprettyオプションをつけることで、ひとつのコミットに対して1行のコミットログを取得
    cmd = f"git log --pretty=format:'\"%h\",\"%aN\",\"%ad\",\"%s\"' --date=iso -- {git_file_path}"
    gitlogs = _run_command(cmd, target_git_repo_dir)
    for gitlog in gitlogs:
        if gitlog.strip() == "":
            continue

        # コミットログをパースして、辞書形式でcommitlogsに追加
        commit_hash, author, commit_datetime, *message = _parse_csv_line(gitlog)
        message = "".join(message)
        commit_datetime = dt.datetime.strptime(commit_datetime, "%Y-%m-%d %H:%M:%S %z")
        commit_datetime =  commit_datetime.astimezone(pytz.timezone('Asia/Tokyo'))
        commitlogs.append(
            {
                "filepath": git_file_path.as_posix(),
                "file_ext": git_file_path.suffix,
                "commit_hash": commit_hash,
                "author": author,
                "commit_datetime": commit_datetime,
                "message": message,
            }
        )

コミットログのクレンジングと分析対象の決定

取得したデータをpandasのDataFrameに変換し、必要な前処理を行います。 プロジェクトの状況やリポジトリ内ののディレクトリ構造に合わせて下記はカスタマイズしてください。

この例では、下記を対象とするようにフィルタリングしています。

  • 拡張子が.py, .ts, .jsのファイル
  • ファイルパスにtestsを含まないプロダクトコード
  • コミットのauthorがbotでない開発者
  • 直近1年間のコミット
commitlog_df = pd.DataFrame(commitlogs)

# 拡張子を分析対象に限定
commitlog_df = commitlog_df[commitlog_df["file_ext"].isin([".py", ".ts", ".js"])]

# filepathにtestsが含まれるレコードを除去
commitlog_df = commitlog_df[~commitlog_df["filepath"].str.contains("tests")]

# commit_datetimeからcommit_dateを抽出
commitlog_df["commit_date"] = commitlog_df["commit_datetime"].dt.date

# authorがrenovate、および、github-actionsのレコードを除去
commitlog_df = commitlog_df[~commitlog_df["author"].isin(["renovate[bot]", "github-actions"])]

# commit_dateは直近1年間のレコードのみを対象
commitlog_df = commitlog_df[commitlog_df["commit_date"] >= dt.date.today() - dt.timedelta(days=365)]

# 集計に必要なカラムのみを抽出
commitlog_df = commitlog_df[['filepath', 'file_ext', 'author', 'commit_date']]

commitlog_df

分析対象の決定後のDataFrameのイメージはこのようになります。

分析対象の決定後のDataFrameのイメージ

実際のAI在庫管理での分析では、ファイルパスなどから機能やシステムのコンポーネント単位にグルーピングして集計しています。

コミットログの集計

ファイルごと、開発者ごとのコミット数を集計します。

# ファイルごとにautorのcommit数をカウント
author_commit_count = commitlog_df.groupby(["filepath", "author"]).size().reset_index()
author_commit_count.columns = ["filepath", "author", "commit_count"]

# pivot_tableを使って、ファイルごとにauthorのcommit数をカウント
author_commit_count_pivot = author_commit_count.pivot_table(index="filepath", columns="author", values="commit_count", fill_value=0)
author_commit_count_pivot = author_commit_count_pivot.astype(int)
author_commit_count_pivot

コミット回数だと偏りとしての大小を判断しづらいため、変更数の割合に正規化します。

# ファイルごとに割合に変換
author_commit_count_pivot_ratio = author_commit_count_pivot.div(author_commit_count_pivot.sum(axis=1), axis=0)
author_commit_count_pivot_ratio = author_commit_count_pivot_ratio.fillna(0)
author_commit_count_pivot_ratio = author_commit_count_pivot_ratio.round(2)
author_commit_count_pivot_ratio

ファイルごとに割合に変換後のDataFrameのイメージはこのようになります。

ファイルごとに割合に変換後のDataFrameのイメージ

結果の可視化

結果を分かりやすく表示するために、seabornライブラリを使用してヒートマップを作成します。

# heatmapを表示
sns.heatmap(author_commit_count_pivot_ratio, cmap="Blues")

ヒートマップの表示はこのようになります。

ヒートマップの表示

結果の解釈における注意点

結果の解釈には、プロジェクトの特性や開発プラクティスを考慮する必要があります。 実際に、AI在庫管理のプロダクトでは、さらにこれを機能やシステムのコンポーネントの単位にグルーピングして集計して、他のメトリクスを合わせて分析しています。

この結果だけで判断すると危険なシーンもあります。 たとえば、特定の開発者1人に偏っていた結果でも、ここ1年で1度しか変更していないファイルなども存在するためです。 変更回数にも着目したり、分析の期間などを変えながら、さまざまな角度からプロダクトやプロジェクトの特性と照らし合わせながら確認をするようにしてください。

このあと実施したアクション

AI在庫管理のシステムの理解容易性を高め、開発者が変更に対して自信を持って作業できるようにしていくために、 これらの結果をもとに、ひとつの施策として自動テストの理解容易性を向上させる取り組みを実施しました。

自動テストの理解容易性を向上させることで、下記の効果が期待されます。

  • 開発者のシステムの理解促進: 将来の自分や新規参画者にとって、テストの目的が明確になる。 システムの期待される動作が分かりやすくなることで、プロダクトコードの理解もしやすくなる。

  • 変更の影響把握の容易化: プロダクトコード変更時に、影響を受ける振る舞いが明確になる。 どの機能が正常に動作し、どの機能が破壊されたかが一目でわかるようになる。

  • ドメイン知識の可視化: テストコードはドメイン知識の塊であり、テストを分かりやすくすることで、関連するコード、機能、仕様の理解も向上する。

実際にテストコードの理解容易性の改善としては、Behavior-Driven Development(BDD)という開発手法で使われているGherkin記法を使用しました。 その詳細は別の記事で紹介できればと思います。

まとめ

Gitのコミットログを分析することで、客観的な指標をもとに改善の優先度をつけることができました。 それにより、プロジェクト全体の理解容易性を向上させる施策をコツコツを無理なく進められる形で始めることができました。

それだけではなく、このGitのコミットログを分析することは、まるでチームの歴史書を読むような感覚でした。 誰がどこを頑張って開発してきたか、どの部分に愛情を注いできたのかも見え、よりプロダクトへの愛着も湧くプロセスとなりました。 ぜひ、あなたのプロジェクトでも試してみてください。