Skip to content

Instantly share code, notes, and snippets.

@timsu
Last active December 12, 2024 16:40
Show Gist options
  • Select an option

  • Save timsu/442e52ab89ee6e963d683018e27d5f43 to your computer and use it in GitHub Desktop.

Select an option

Save timsu/442e52ab89ee6e963d683018e27d5f43 to your computer and use it in GitHub Desktop.
PR Stats
import axios from "axios";
import { exec as execCb } from "child_process";
import { createObjectCsvWriter } from "csv-writer";
import { promisify } from "util";
const exec = promisify(execCb);
const GITHUB_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
const OWNER = process.env.GITHUB_REPO_OWNER;
const REPO = process.env.GITHUB_REPO_NAME;
const nameTable: Record<string, string> = {
// for those unusual usernames: github username -> name mapping
};
interface PullRequestSummary {
number: string;
title: string;
body: string;
createdAt: string;
updatedAt: string;
mergedAt?: string;
leadTime?: number;
additions?: number;
deletions?: number;
status: "open" | "closed" | "merged";
author: string;
}
interface AuthorStats {
pulls: PullRequestSummary[];
}
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
async function fetchAllPullRequests(): Promise<PullRequest[]> {
let page = 1;
let allPullRequests: PullRequest[] = [];
while (true) {
try {
const response = await axios.get<PullRequest[]>(
`https://api.github.com/repos/${OWNER}/${REPO}/pulls`,
{
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: "application/vnd.github.v3+json",
},
params: {
state: "all",
sort: "updated",
direction: "desc",
per_page: 100,
page: page,
},
},
);
const pullRequests = response.data.filter(
(pr: PullRequest) => new Date(pr.updated_at) > oneWeekAgo,
);
if (pullRequests.length === 0) {
break;
}
allPullRequests = allPullRequests.concat(pullRequests);
page++;
} catch (error: unknown) {
console.error("Error fetching pull requests:", error);
break;
}
}
return allPullRequests;
}
async function getGitStats(
baseSha: string,
headSha: string,
): Promise<{ additions?: number; deletions?: number }> {
try {
const { stdout } = await exec(`git diff --shortstat ${baseSha}..${headSha}`);
const match = stdout.match(/(\d+) insertions\(\+\), (\d+) deletions\(-\)/);
return {
additions: match ? parseInt(match[1], 10) : undefined,
deletions: match ? parseInt(match[2], 10) : undefined,
};
} catch (error) {
// this happens when the branch gets deleted
// console.error(
// "Error getting git stats:",
// error instanceof Error ? error.message : String(error),
// );
return {};
}
}
function formatDate(dateString: string | undefined | null): string {
if (!dateString) return "";
const date = new Date(dateString);
const month = (date.getMonth() + 1).toString();
const day = date.getDate().toString();
const year = date.getFullYear();
return `${month}/${day}/${year}`;
}
async function groupPullRequestsByAuthor(
pullRequests: PullRequest[],
): Promise<Record<string, AuthorStats>> {
const authorStats: Record<string, AuthorStats> = {};
for (const pr of pullRequests) {
const author = pr.user.login;
if (!authorStats[author]) {
authorStats[author] = {
pulls: [],
};
}
const leadTime =
pr.merged_at ?
Math.round(
(new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60),
)
: undefined;
const { additions, deletions } = await getGitStats(pr.base.sha, pr.head.sha);
let status: "open" | "closed" | "merged";
if (pr.merged_at) {
status = "merged";
} else if (pr.state === "closed") {
status = "closed";
} else {
status = "open";
}
authorStats[author].pulls.push({
number: `#${pr.number}`,
title: pr.title,
body: pr.body,
createdAt: formatDate(pr.created_at),
updatedAt: formatDate(pr.updated_at),
mergedAt: formatDate(pr.merged_at),
leadTime,
additions,
deletions,
status,
author: nameTable[author] || author,
});
}
return authorStats;
}
async function formatPullRequestStats(authorStats: Record<string, AuthorStats>): Promise<void> {
const allPRs: PullRequestSummary[] = Object.values(authorStats).flatMap((stats) => stats.pulls);
const csvWriter = createObjectCsvWriter({
path: "pr_table.csv",
header: [
{ id: "author", title: "Author" },
{ id: "number", title: "PR #" },
{ id: "title", title: "Title" },
{ id: "createdAt", title: "Created At" },
{ id: "updatedAt", title: "Updated At" },
{ id: "mergedAt", title: "Merged At" },
{ id: "leadTime", title: "Lead Time (hrs)" },
{ id: "additions", title: "Additions" },
{ id: "deletions", title: "Deletions" },
{ id: "status", title: "Status" },
],
});
await csvWriter.writeRecords(allPRs);
console.log("CSV file has been written successfully: pr_table.csv");
}
async function main() {
const pullRequests = await fetchAllPullRequests();
const authorStats = await groupPullRequestsByAuthor(pullRequests);
await formatPullRequestStats(authorStats);
}
void main();
type GitHubUser = {
login: string;
id: number;
avatar_url: string;
url: string;
};
type GitHubLabel = {
id: number;
name: string;
description: string;
color: string;
};
type GithubRef = {
label: string;
ref: string;
sha: string;
user: GitHubUser;
};
type PullRequest = {
url: string;
id: number;
number: number;
state: "open" | "closed" | "merged";
locked: boolean;
title: string;
user: GitHubUser;
body: string;
labels: GitHubLabel[];
created_at: string;
updated_at: string;
closed_at: string | null;
merged_at: string | null;
merge_commit_sha: string | null;
assignees: GitHubUser[];
requested_reviewers: GitHubUser[];
head: GithubRef;
base: GithubRef;
draft: boolean;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment