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](https://docs.github.com/ja/rest/reference/pulls)

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

|#|項目|説明|
|:--|:--|:--|
|①|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の説明|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/89c7fdcb-d41f-7e43-608f-a3842a974a2f.png)
(*1) ②、③で取れる情報は以下の通り
以降、②をレビューコメント、③をコメントと記す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/950f0967-2099-4db4-86de-1d6271064db3.png)

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

### レスポンス

レスポンス形式は以下参照。
[Github Docs - PR review comments](https://docs.github.com/ja/rest/reference/pulls#review-comments)

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/2610a3f5-5652-d296-a895-819622404886.png)

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

pythonで実装

## 3.1. 出力

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

https://github.com/Symthy/python-simple-tools/pull/1

上記のPRから取得した結果の出力は以下の通り（レビューコメントが５つあるため、mdファイルも５つ出力）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/94444bd8-c08e-7ddd-c374-df3a9114bbfa.png)
mdファイルの中身は以下の通り
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319263/50fa7a11-0e26-c874-37d1-59c7b974516b.png)
やろうと思えば、このように出力も可能というご紹介。

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

```md
# PR Review Comment Info

- PR title

{{title}}

- PR create user

{{create_user}}

  : (略)
```

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

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

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

```python
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上のレビューコメントもその他色々も）。

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

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

```python
# 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. ソースコード

https://github.com/Symthy/python-simple-tools/tree/master/git_pr_collect

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

# 4. おわりに

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

以上。