GitHub GraphQL APIで複数リポジトリ/ブランチのコミット一覧を取得する(サンプルアプリ付き)

## 概要

- 直近の作業内容を振り返るため、リポジトリ/ブランチをまたいだGitHub上でのコミット一覧を取得したい
- GitHub Rest APIではコミットの一覧をブランチごとに別々に取得する必要がある
- GitHub GraphQL APIならクエリを工夫すると一括で取得することができる
- 以下のようなWebアプリケーションを実装して運用

![スクリーンショット 2022-08-18 17.07.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/b471b29a-8fb3-d9a2-5fa1-73db55a59d6d.png)

## 背景

直近の作業内容を振り返ったり、まとめて報告するタイミングでGitHub上で各リポジトリ/ブランチのコミット履歴を確認していました。個人としての作業内容をまとめるならリポジトリ、ブランチをまたいだコミットの一覧を見たいものです。GitHub上でそうした表示はできなさそうなので自作することにしました。

## GitHub API

GitHubのAPIにはRest APIとGraphQL APIの二種類があります。いずれにおいてもGitHub上のリポジトリやブランチ、コミットを取得することができます。

https://docs.github.com/en/rest

https://docs.github.com/en/graphql

### Rest API

Rest APIでは組織、リポジトリなどの各エンドポイントにリクエストを送ることで情報を取得することができます。今回の目的を達成するためには自分が作成したか、自分の組織に属するリポジトリの一覧を取得した上でそれぞれの最近のコミット一覧を取得する必要があります。

Rest APIのCommitsエンドポイントを見てみましょう。リポジトリとオーナーをパラメータで指定してコミットの一覧を取得することができます。一見リポジトリのコミット一覧が取得できるように見えますが、デフォルトブランチ(masterなど)のコミット一覧しか取得することができません。`sha`パラメータにブランチ名を指定することでデフォルトブランチ以外のブランチのコミットを取得することができます。しかしながらリポジトリのコミット一覧を取得するにはそのリポジトリのブランチ全てについてリクエストを送る必要があります。

https://docs.github.com/en/rest/commits/commits

GitHub APIにはのリクエスト数制限があり、1時間あたり5000回までに制限されています。リポジトリ多数あり、かつそのブランチが大量にある場合にその全てに対してコミットの一覧を取得しようとするとこの制限に容易に到達してしまいます。リポジトリ数*ブランチ数の回数リクエストが送られるからです。一度取得するだけなら可能ですが、例えばアプリケーションとして実装して取得/表示することは難しいと考えられます。

https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting

こうした点から今回の目的にはRest APIは適さないと判断しました。

### GraphQL API

GraphQL APIではエンドポイントは一つで、そこに対してGraphQLクエリを送ると対応する構造のデータが取得できる仕組みです。このAPIではユーザーの関わっているリポジトリのブランチのコミットの……というように一つの階層構造を持ったクエリによってネストしたデータを取得することができます。複数のブランチのコミット一覧を一つのクエリで取得することができるのでリクエスト回数制限に引っかかる可能性も低くなります。

問題はそのようなクエリをどのように構築していくかという点です。ドキュメントが複雑なので一つずつ読み解きながらクエリを構築していきましょう。

https://docs.github.com/en/graphql/reference

まずは`user`クエリを使ってログインユーザーの情報を取得します。

https://docs.github.com/en/graphql/reference/queries#user

取得されるデータは`User`というスキーマで定義されています。`repositoriesContributedTo`というフィールドがあり、`contributionTypes`のパラメータを指定することでそのユーザーがコミットを行ったリポジトリを取得できます。`orderBy`パラメータで順序も指定できます。

https://docs.github.com/en/graphql/reference/objects#user

以下のようなクエリで`user`クエリを利用します。`repositoriesContributedTo`フィールドの項目については後述します。

```graphql
query(
    $userName: String!
) { 
    user(login: $userName) { 
        repositoriesContributedTo(
            includeUserRepositories: true,
            contributionTypes: COMMIT,
            first: 10,
            orderBy: {
                direction: DESC,
                field: PUSHED_AT
            }
        ) {
            # 取得するフィールドを指定
        }
    }
}
```

```json
{
    "userName":"sample"
}
```

次に`repositoriesContributedTo`フィールド中身を取得するため、`RepositoryConnection`スキーマの定義を見ていきましょう。

https://docs.github.com/en/graphql/reference/objects#repositoryconnection

ややこしいですが、ページングされたリポジトリの配列のスキーマです。`nodes`フィールドで取得できる`Repository`オブジェクトには各リポジトリの情報が入っています。

https://docs.github.com/en/graphql/reference/objects#repository

ここまででクエリは以下のようになります。`login`フィールドで指定したユーザーがコミットしたリポジトリの情報が取得できました。

```graphql
query(
    $userName: String!
) { 
    user(login: $userName) { 
        repositoriesContributedTo(
            includeUserRepositories: true,
            contributionTypes: COMMIT,
            first: 10,
            orderBy: {
                direction: DESC,
                field: PUSHED_AT
            }
        ) {
            nodes {
                name
                url
                owner {
                    login
                }
            }
        }
    }
}
```

ややわかりづらいですがリポジトリのブランチ一覧は、`refs`というフィールドで取得することができます。このフィールドのパラメータに`refPrefix`があり、これを`refs/heads/`と指定することでブランチの一覧が取得されます。この`refs`フィールドで取得される一覧もページングされています。このフィールドは`RefConnection`型になります。

https://docs.github.com/en/graphql/reference/objects#refconnection

この`RefConnection`の`nodes`フィールドは`Ref`型になっています。

https://docs.github.com/en/graphql/reference/objects#ref

ここまでで以下のようなクエリになります。指定したユーザーがコミットしたリポジトリの情報と、ブランチの一覧が取得できました。

```graphql
query(
    $userName: String!
) { 
    user(login: $userName) { 
        repositoriesContributedTo(
            includeUserRepositories: true,
            contributionTypes: COMMIT,
            first: 10,
            orderBy: {
                direction: DESC,
                field: PUSHED_AT
            }
        ) {
            nodes {
                name
                url
                owner {
                    login
                }
                refs(
                    refPrefix: "refs/heads/", 
                    first: 100
                ) {
                    nodes {
                        name
                    }
                }
            }
        }
    }
}
```

`Ref`型の詳細な内容は`target`フィールドに含まれています。このフィールドは`GitObject`という抽象的な型(インターフェース)になっていて、フラグメントを使って抽出します。

https://docs.github.com/en/graphql/reference/interfaces#gitobject

今回はブランチのコミットの一覧を取得したいので、`Commit`スキーマを利用します。このオブジェクト自体はブランチの最新のコミットです。`history`フィールドを利用するとそこから遡ってブランチのコミット一覧を取得できます。引数`author`でコミットしたユーザーのメールアドレスを指定することで、抽出対象のユーザーが行ったコミットに絞り込むことができます。また直近のものだけに絞り込みたいので`since`フィールドに1週間前など適当なタイムスタンプを指定しています。

https://docs.github.com/en/graphql/reference/objects#commit

`history`フィールドもページングされているので、同様に`nodes`フィールドから個々のコミットを取得します。`nodes`フィールドも`Commit`型なのであとは必要な情報のフィールドを指定するだけです。以下のようなクエリがひとまずの完成形です。

```graphql
query(
    $userName: String!,
    $email: String!,
    $since: GitTimestamp,
) { 
    user(login: $userName) { 
        repositoriesContributedTo(
            includeUserRepositories: true,
            contributionTypes: COMMIT,
            first: 10,
            orderBy: {
                direction: DESC,
                field: PUSHED_AT
            }
        ) {
            nodes {
                name
                url
                owner {
                    login
                }
                refs(
                    refPrefix: "refs/heads/", 
                    first: 100
                ) {
                    nodes {
                        name
                        target {
                            ... on Commit {
                                history(
                                    first: 100,
                                    author:{
                                        emails: [$email]
                                    },
                                    since: $since
                                ){
                                    nodes {
                                        message
                                        url
                                        committedDate
                                        oid
                                        changedFiles
                                        additions
                                        deletions
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```

```json
{
    "userName":"sample",
    "email":"sample@sample.com",
    "since":"YYYY-MM-DDTHH:mm:ss"
}
```

上のクエリでは簡単のため、条件に該当するリポジトリの最初の10件だけを取得していました。取得できる件数の上限は100件までになっています。

それ以降のリポジトリを取得するなら、`pageInfo`フィールドから`endCursor`と`hasNextPage`を取得しておきます。`hasNextPage`が`true`なら`endCursor`の値を`repositoriesContributedTo`フィールドの`after`パラメータに入れたリクエストを送ります。そうすると10件以降のリポジトリが取得できます。これを`hasNextPage`が`false`になるまで繰り返します。

https://docs.github.com/en/graphql/reference/objects#pageinfo

以下のクエリではこの方法で対象ユーザーが最近コミットしたリポジトリを全件取得しています。

```graphql
query(
    $userName: String!,
    $nextCursor: String
) { 
    user(login: $userName) { 
        repositoriesContributedTo(
            includeUserRepositories: true,
            contributionTypes: COMMIT,
            first: 10,
            orderBy: {
                direction: DESC,
                field: PUSHED_AT
            },
            after: $nextCursor
        ) {
            totalCount
            pageInfo {
                endCursor
                hasNextPage
            }
            nodes {
                name
                url
                owner {
                    login
                }
            }
        }
    }
}
```

```json
{
    "userName":"sample",
    "nextCursor":"xxxx"
}
```

この場合、取得されたリポジトリそれぞれについてコミットの一覧を取得する必要があります。以下のクエリをそれぞれのリポジトリに対して実行します。リポジトリ名とオーナー名を指定して`repository`クエリで任意のリポジトリの情報を取得できます。

https://docs.github.com/en/graphql/reference/queries#repository

```graphql
query(
    $name: String!,
    $owner: String!,
    $email: String!,
    $since: GitTimestamp,
    $nextCursor: String
) {
    repository(
        name: $name, 
        owner: $owner
    ) {
        name
        url
        owner {
            login
        }
        refs(
            refPrefix: "refs/heads/", 
            first: 100,
            after: $nextCursor
        ) {
            totalCount
            pageInfo {
                endCursor
                hasNextPage
            }
            nodes {
                name
                target{
                    ... on Commit {
                        history(
                            first:100,
                            author:{
                                emails: [$email]
                            },
                            since: $since
                        ){
                            nodes {
                                message
                                url
                                committedDate
                                oid
                                changedFiles
                                additions
                                deletions
                            }
                        }
                    }
                }
            }
        }
    }
}
```

```json
{
    "name":"sample",
    "owner":"sample",
    "email":"sample@sample.com",
    "since":"YYYY-MM-DDTHH:mm:ss",
    "nextCursor":"xxxx"
}
```

さらに対象のリポジトリのブランチも一度に100件までしか取得できないので、同様に`endCursor`を使ってそれ以降を取得していきます。各ブランチのコミット一覧も同様にページネーションされていますが、ひとまず直近のコミットが取得できればいいので100件で十分と判断しました。

リポジトリごとにリクエストを送ることになりますが、ブランチ100件ごとにコミット一覧を取得できるのでRest APIに比べて必要なリクエストの数は少なくなっています。

## 運用

以下のようなWebアプリケーションにして運用しています。Next.js/Tailwind CSSを利用しています。

OAuth認証した上で上記のクエリを使ってコミットの一覧を取得、フロントエンドで日毎や週ごとにまとめて時系列で表示することができます。14日前や1ヶ月前などどの期間のコミット一覧を取得するか選択したり、コミットした週やリポジトリ単位でまとめて表示する機能があります。

![スクリーンショット 2022-08-18 17.07.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/b471b29a-8fb3-d9a2-5fa1-73db55a59d6d.png)

プライバシーポリシーの関係で弊社で利用しているアプリケーションをそのまま公開できないため、ソースコードだけ置いておきます。クローンしてローカルで起動したり適当にデプロイしていただければ利用いだけます。以下に利用手順を書いておきます。

https://github.com/oshima-valuesccg/commit-timeline

```bash
git clone https://github.com/oshima-valuesccg/commit-timeline.git
# or
git clone git@github.com:oshima-valuesccg/commit-timeline.git
```

### OAuthクライアント作成

以下の手順でGitHub APIのOAuthクライアントを作成します。

https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app

ローカル環境で動作させる場合は`Authorization callback URL`を`http://localhost:3000/api/auth/authorize`に設定してください。

### 環境変数設定

また`Client ID`と`Client secret`をコピーしてアプリケーションの環境変数に設定する必要があります。`.env.development`ファイルを作成して以下のように設定します。最後の`COOKIE_PASSWORD`は認証用のCookieを暗号化するパスワードです。32文字以上で適当にランダムな文字列を指定してください。

```
GITHUB_CLIENT_ID="xxxxxxxxxxxxxxxxxxxx"
GITHUB_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
GITHUB_REDIRECT_URL="http://localhost:3000/api/auth/authorize"
COOKIE_PASSWORD="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

環境変数を設定した上で以下のコマンドで起動させます。

```bash
npm run dev
```

`localhost:3000`にアクセスしてGitHubのOAuth認証を行えばコミットの一覧を表示できます。

## おわりに

GitHub GraphQL API、575で美しいですね。いい下の句が思い付いた方はコメントお願いいたします。

## 参考文献

https://stackoverflow.com/questions/48285888/github-graphql-getting-a-repositorys-list-of-commits