Githubの特定リポジトリからPRのレビューコメントを収集/mdファイル出力するツールを作った話 (Python)


1. はじめに

コードレビューをするようになって数ヶ月。まともにメインで使ってる言語のコードレビューをした時に、想像以上に色々拾えず、「これはまずい」となった。
そこで、レビュー観点を整理するために、過去PRでのレビュー指摘が財産となるはずなので、振り返ろうと思ったが、

GithubのWebページ上で一々PRを一つずつを開いていって、コメントを一つずつ見ていく、欲しくない情報も目に入る、それだと手間も時間もかかって面倒だと思いツールを作って収集&ファイル出力しようと思い、そのためのコードを書いた。

せっかく作ったため、その過程でGithub APIのレスポンスデータで押さえた点と、「やろうと思えばこんなことできます」という参考として作成物を簡単に紹介する(何番煎じか分からないが)。

2. Github API

Github アクセストークンが必要ですが、発行方法はググれば出てくるので割愛。Github APIについても同様に割愛。

APIで取得できるPR及びPRのレビューコメントのレスポンスデータが少々分かりにくい部分あるため、
レスポンスデータの一部項目と、それがGUI上のどの部分に対応するかを重点的に記す。

2.1. Pull Request 取得

リクエスト

GET /repos/{owner}/{repository}/pulls

以下で全PRのデータ取得可能。

curl -s -k -H "Accept: application/vnd.github.v3+json" -u :<Github access token> https://api.github.com/repos/<owner>/<repository>/pulls?state=all

レスポンス

レスポンス形式は以下参照。だが
Github Docs - Pull Request

レスポンスデータのどの項目が何のデータか特に説明がないため、(個人的に欲しかった)一部項目だけに絞って紹介する。

# 項目 説明
html_url PRのwebページのURL
review_comments_url PR上の全レビューコメントを取得するREST APIのURL (*1)
comments_url PR上の全コメントを取得するREST APIのURL (*1)
number PR番号
title PRのタイトル
user.login PRの作成者
body PRの説明


(*1) ②、③で取れる情報は以下の通り
以降、②をレビューコメント、③をコメントと記す。

2.2. PRのレビューコメント取得

レスポンス

レスポンス形式は以下参照。
Github Docs - PR review comments

こちらも同様に、レスポンスデータのうち、個人的に欲しかった項目だけに絞って紹介する

# 項目 説明
pull_request_review_id ※一度に複数コメントした場合、このidは重複する
id ユニークキー
diff_hunk レビューコメントされたコードの周辺差分()
path レビューコメントされたファイルのパス
user.login レビューコメントした人
body レビューコメント内容
_links.html.href GitHubのWebページ(当該レビューコメント)へのリンクURL

※ body に入っているデータは、レビューコメントのスレッドの先頭の内容のみのようです。

3. PR上のレビューコメントを収集/出力するために作成した物

pythonで実装

3.1. 出力

csvファイル(念の為の一覧出力) と mdファイル(各PRのレビューコメント1件を1ファイル)に出力

上記のPRから取得した結果の出力は以下の通り(レビューコメントが5つあるため、mdファイルも5つ出力)

mdファイルの中身は以下の通り

やろうと思えば、このように出力も可能というご紹介。

mdファイルへの出力は、jinja2 を利用。
簡単に紹介すると、以下のようなテンプレートファイルを用意して

# PR Review Comment Info

- PR title

{{title}}

- PR create user

{{create_user}}

  : (略)

テンプレートに当てはめるためのデータを用意し、

{
  'title': 'pr title',
  'create_user': 'user name'
    : (略)
}

以下のようにするだけで、テンプレートに基づいて出力内容作成、ファイル出力できる

from jinja2 import Environment, FileSystemLoader

TEMPLATES_DIR_PATH = './templates/'
PR_REVIEW_COMMENT_TEMPLATE_FILE_NAME = 'pr_review_comment.j2'

env = Environment(loader=FileSystemLoader(searchpath=TEMPLATES_DIR_PATH, encoding='utf8'))
template = env.get_template(PR_REVIEW_COMMENT_TEMPLATE_FILE_NAME)
md_file_data = template.render(json_data)
with open(OUTPUT_DIR_PATH + md_file_path , mode='w', encoding='CP932', errors='ignore') as f:
    f.write(md_file_data)

※jinja2の詳細はググれば分かるため割愛。

3.2. API実行周り

Github APIには、ページネーション機能があり、1回のAPI実行で最大100件までしか取得できない。
それ以上のデータがある場合は、ページ番号をカウントアップしてAPIを再実行する必要がある(PRもPR上のレビューコメントもその他色々も)。

先にページ数を取得する方法がパッと見、見当たらなかったため、申し訳程度のリトライ機構を用意して以下の通りに実装。

※今回取得したいのは、レビューコメントなので、コメントは現状取得しない
※レビューコメント以外も取得したくなったら、用意したリトライ機構に乗っけて取れば良い算段で実装

# Callback-only function used by retry_execute_github_api()
def collect_and_write_pull_requests(page_count: int, non_arg) -> int:
    api_url = build_get_pull_requests_api_url(page_count)
    response_json_pr_array = execute_github_api(api_url)
    pr_data_list = PullRequestDataList(response_json_pr_array)
    pr_data_list.write_csv(PULL_REQUEST_LIST_FILE_NAME)
    # collect review comment in PR
    for pr_data in pr_data_list.values:
        retry_execute_github_api(collect_and_write_pr_review_comments, pr_data)
    return len(response_json_pr_array)

# Callback-only function used by retry_execute_github_api()
def collect_and_write_pr_review_comments(page_count: int, pr_data) -> int:
    api_url = build_get_pr_review_comments_api_url(pr_data.review_comments_api_url, page_count)
    response_json_review_comments_array = execute_github_api(api_url)
    pr_review_comment_list = PullRequestReviewCommentList(response_json_review_comments_array, pr_data)
    pr_name = pr_data.build_pr_name()
    pr_review_comment_list.write_csv(pr_name)
    pr_review_comment_list.write_md(pr_name)
    return len(response_json_review_comments_array)

def retry_execute_github_api(api_execute_func: Callable[[int, any], int], *args):
    is_retry = True
    page_count = 1
    while is_retry:
        count = api_execute_func(page_count, args[0] if len(args) != 0 else None)
        page_count += 1
        is_retry = False if count < 100 else True

retry_execute_github_api(collect_and_write_pull_requests)

上記は、一部抜粋のため、ソース全文は以下リンク先をご参照ください。

3.3. ソースコード

※MIT License にしているため、コードはパクって頂いて問題ありません。可読性/変更容易性は高くしているつもりのため、欲しい方はご活用ください。

4. おわりに

現状は、レビューコメントのスレッドのうち先頭の内容しか取ってない。
先頭以外は回答/議論が主になると思っているため、先頭だけで十分ではないかと推測している。
足りないと思えば追加実装すればよいというスタンスで、追加実装しやすいように実装もした。
これで少しでも捗ることを期待。

以上。