Skip to content

Commit

Permalink
use GitHub's GraphQL API to minimise data returned from GitHub (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
awwsmm committed Apr 7, 2023
1 parent ea8ec00 commit f6260bb
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 58 deletions.
119 changes: 67 additions & 52 deletions lib/utils/ProjectUtils.ts
@@ -1,6 +1,7 @@
import { graphql, GraphqlResponseError } from '@octokit/graphql';
import Commit from '../model/project/Commit';
import { Endpoints } from '@octokit/types';
import fs from 'fs';
import type { GraphQlQueryResponseData } from '@octokit/graphql';
import LogEntry from '../model/project/LogEntry';
import matter from 'gray-matter';
import { parseISO } from 'date-fns';
Expand Down Expand Up @@ -32,59 +33,73 @@ export default abstract class ProjectUtils {
* Commits are returned in reverse chronological order (newest first).
*/
static async getCommits(name: string): Promise<Commit[]> {
if (ProjectUtils.isValidRepositoryName(name)) {
type Success = Endpoints['GET /repos/{owner}/{repo}/commits']['response']['data'];
type Failure = { message: string; documentation_url: string };
type Response = Success | Failure;

function mapCommits(commits: Success, project: string): Commit[] {
return commits.map((each) => {
const maybeDate = each.commit.committer?.date ?? each.commit.author?.date;
const link = `https://github.com/awwsmm/${project}/commit/${each.sha}`;

if (maybeDate) {
return new Commit(name, maybeDate, each.commit.message, each.sha, link);
} else {
// The GitHub API can theoretically return commits with no date.
// If that ever happens, figure out how to handle it.
// see: https://docs.github.com/en/rest/reference/commits#list-commits

throw new Error(`GitHub commit missing date: ${link}`);
}
});
}

// sometimes, we get rate limiting errors from GitHub
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
// (60 requests/hour unauthenticated, 1000 requests/hour authenticated)

const input = `https://api.github.com/repos/awwsmm/${name}/commits?per_page=100`;

const init = {
headers: {
Authorization: `Bearer ${process.env.GITHUB_PAT}`,
},
};

const preamble = Promise.resolve(
console.log(`Querying GitHub for commits for "${name}"`) // eslint-disable-line no-console
);

const commits: Promise<Response> = fetch(input, init).then((response) => response.json());

return preamble
.then((_) => commits) // eslint-disable-line @typescript-eslint/no-unused-vars
.then((response) => {
if ('message' in response && 'documentation_url' in response) {
const failure = response as Failure;
throw new Error(`Querying GitHub failed due to: ${failure.message}. ${failure.documentation_url}`);
} else {
return mapCommits(response as Success, name);
}
});
} else {
// to pull the GraphQL schema from the REST API, use curl
// curl -H "Authorization: bearer $GITHUB_PAT" https://api.github.com/graphql > graphql.json

// to look at a particular node of the schema, use jq
// cat graphql.json | jq '.data.__schema.types[961]' > repository.json

// to get e.g. only the field names, do
// cat graphql.json | jq '.data.__schema.types[961].fields[] | {name: .name}' > repository-field-names.json

if (!ProjectUtils.isValidRepositoryName(name)) {
return Promise.reject(`Repository name is invalid: ${name}`);
}

type Success = GraphQlQueryResponseData; // { message: string, committedDate: string, oid: string }[];
type Failure = GraphqlResponseError<GraphQlQueryResponseData>;
type Response = Success | Failure;

function mapCommits(response: Success, project: string): Commit[] {
const commits: { node: { message: string; committedDate: string; oid: string } }[] =
response.repository.defaultBranchRef.target.history.edges;

return commits.map((each) => {
const link = `https://github.com/awwsmm/${project}/commit/${each.node.oid}`;
return new Commit(name, each.node.committedDate, each.node.message, each.node.oid, link);
});
}

// sometimes, we get rate limiting errors from GitHub
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
// (60 requests/hour unauthenticated, 1000 requests/hour authenticated)

const input = `{
repository(owner: "awwsmm", name: "${name}") {
defaultBranchRef {
target {
... on Commit {
history(first:100) {
edges {
node {
... on Commit {
message
committedDate
oid
} } } } } } } } }`;

const init = {
headers: {
authorization: `token ${process.env.GITHUB_PAT}`,
},
};

const preamble = Promise.resolve(
console.log(`Querying GitHub GraphQL API for commits for "${name}"`) // eslint-disable-line no-console
);

const commits: Promise<Response> = graphql.defaults(init)(input);

return preamble
.then((_) => commits) // eslint-disable-line @typescript-eslint/no-unused-vars
.then((response) => {
if ('message' in response && 'documentation_url' in response) {
const failure = response as Failure;
throw new Error(`Querying GitHub GraphQL API failed due to: ${failure.message}. ${failure.message}`);
} else {
return mapCommits(response as Success, name);
}
});
}

/**
Expand Down
119 changes: 113 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -14,6 +14,7 @@
"unhook": "find .git/hooks -type l -exec unlink {} \\;"
},
"dependencies": {
"@octokit/graphql": "^5.0.5",
"@octokit/types": "^9.0.0",
"date-fns": "^2.29.3",
"gray-matter": "^4.0.3",
Expand Down

1 comment on commit f6260bb

@vercel
Copy link

@vercel vercel bot commented on f6260bb Apr 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.