LoginSignup
3
3

More than 1 year has passed since last update.

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

Posted at

概要

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

スクリーンショット 2022-08-18 17.07.20.png

背景

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

GitHub API

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

Rest API

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

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

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

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

GraphQL API

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

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

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

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

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

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

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

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

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

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型になります。

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

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

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という抽象的な型(インターフェース)になっていて、フラグメントを使って抽出します。

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

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

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
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
{
    "userName":"sample",
    "email":"sample@sample.com",
    "since":"YYYY-MM-DDTHH:mm:ss"
}

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

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

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

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
                }
            }
        }
    }
}
{
    "userName":"sample",
    "nextCursor":"xxxx"
}

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

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
                            }
                        }
                    }
                }
            }
        }
    }
}
{
    "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

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

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クライアントを作成します。

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

環境変数設定

またClient IDClient 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"

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

npm run dev

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

おわりに

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

参考文献

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3