Last active
December 12, 2024 16:40
-
-
Save timsu/442e52ab89ee6e963d683018e27d5f43 to your computer and use it in GitHub Desktop.
PR Stats
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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