mirror of
https://github.com/LukeHagar/stats-action.git
synced 2025-12-06 04:21:26 +00:00
Compare commits
2 Commits
f70e248a3b
...
1b17782114
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b17782114 | ||
|
|
e2b4cac264 |
54
src/Types.ts
54
src/Types.ts
@@ -71,11 +71,63 @@ export type RepoDetails = {
|
||||
stars: number;
|
||||
forks: number;
|
||||
isArchived: boolean;
|
||||
isFork: boolean;
|
||||
isPrivate: boolean;
|
||||
primaryLanguage: string | null;
|
||||
topics: string[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type RepoStats = {
|
||||
totalRepos: number;
|
||||
publicRepos: number;
|
||||
privateRepos: number;
|
||||
archivedRepos: number;
|
||||
forkedRepos: number;
|
||||
originalRepos: number;
|
||||
activeRepos: number;
|
||||
reposWithStars: number;
|
||||
reposCreatedThisYear: number;
|
||||
averageStarsPerRepo: number;
|
||||
};
|
||||
|
||||
export type TopicCount = {
|
||||
name: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ComputedStats = {
|
||||
// Repo statistics
|
||||
totalRepos: number;
|
||||
publicRepos: number;
|
||||
privateRepos: number;
|
||||
archivedRepos: number;
|
||||
forkedRepos: number;
|
||||
originalRepos: number;
|
||||
activeRepos: number;
|
||||
reposWithStars: number;
|
||||
reposCreatedThisYear: number;
|
||||
averageStarsPerRepo: number;
|
||||
|
||||
// Language statistics
|
||||
languageCount: number;
|
||||
primaryLanguage: string | null;
|
||||
primaryLanguageThisYear: string | null;
|
||||
topLanguagesThisYear: Language[];
|
||||
|
||||
// Topic statistics
|
||||
totalTopics: number;
|
||||
topTopics: TopicCount[];
|
||||
allTopics: string[];
|
||||
|
||||
// Contribution statistics
|
||||
contributionsThisYear: number;
|
||||
contributionsLastYear: number;
|
||||
yearOverYearGrowth: number | null;
|
||||
mostProductiveMonth: { month: string; contributions: number } | null;
|
||||
};
|
||||
|
||||
export type UserStats = {
|
||||
name: string;
|
||||
username: string;
|
||||
@@ -110,6 +162,8 @@ export type UserStats = {
|
||||
codeByteTotal: number;
|
||||
topLanguages: Language[];
|
||||
contributionStats: ContributionStats;
|
||||
repoStats: RepoStats;
|
||||
computedStats: ComputedStats;
|
||||
contributionsCollection: ContributionsCollection;
|
||||
topRepos: RepoDetails[];
|
||||
};
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
formatNumber,
|
||||
aggregateLanguages,
|
||||
calculateContributionStats,
|
||||
processBatched,
|
||||
calculateComputedStats,
|
||||
} from "./index";
|
||||
import type { ContributionsCollection } from "./Types";
|
||||
import type { ContributionsCollection, Language, ContributionStats } from "./Types";
|
||||
|
||||
describe("formatBytes", () => {
|
||||
test("formats bytes correctly", () => {
|
||||
@@ -202,50 +202,135 @@ describe("calculateContributionStats", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("processBatched", () => {
|
||||
test("processes items in batches", async () => {
|
||||
const items = [1, 2, 3, 4, 5];
|
||||
const processedOrder: number[] = [];
|
||||
describe("calculateComputedStats", () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const lastYear = currentYear - 1;
|
||||
|
||||
const results = await processBatched(items, 2, async (item) => {
|
||||
processedOrder.push(item);
|
||||
return item * 2;
|
||||
const createRepoInfo = (overrides: Partial<{
|
||||
stars: number;
|
||||
forks: number;
|
||||
isArchived: boolean;
|
||||
isFork: boolean;
|
||||
isPrivate: boolean;
|
||||
topics: string[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
languages: { edges: Array<{ size: number; node: { name: string; color: string } }> };
|
||||
}> = {}) => ({
|
||||
stars: 0,
|
||||
forks: 0,
|
||||
isArchived: false,
|
||||
isFork: false,
|
||||
isPrivate: false,
|
||||
topics: [],
|
||||
updatedAt: `${currentYear}-06-01T00:00:00Z`,
|
||||
createdAt: `${lastYear}-01-01T00:00:00Z`,
|
||||
languages: { edges: [] },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createContributionStats = (monthlyBreakdown: Array<{ month: string; contributions: number }>): ContributionStats => ({
|
||||
longestStreak: 0,
|
||||
currentStreak: 0,
|
||||
mostActiveDay: "Monday",
|
||||
averagePerDay: 0,
|
||||
averagePerWeek: 0,
|
||||
averagePerMonth: 0,
|
||||
monthlyBreakdown,
|
||||
});
|
||||
|
||||
test("calculates repo statistics correctly", () => {
|
||||
const repos = [
|
||||
createRepoInfo({ stars: 10, isPrivate: false }),
|
||||
createRepoInfo({ stars: 5, isPrivate: true }),
|
||||
createRepoInfo({ isArchived: true }),
|
||||
createRepoInfo({ isFork: true }),
|
||||
createRepoInfo({ updatedAt: `${currentYear}-01-15T00:00:00Z` }),
|
||||
createRepoInfo({ createdAt: `${currentYear}-03-01T00:00:00Z`, updatedAt: `${currentYear}-03-01T00:00:00Z` }),
|
||||
];
|
||||
|
||||
const stats = calculateComputedStats(repos, [], createContributionStats([]));
|
||||
|
||||
expect(stats.totalRepos).toBe(6);
|
||||
expect(stats.publicRepos).toBe(5);
|
||||
expect(stats.privateRepos).toBe(1);
|
||||
expect(stats.archivedRepos).toBe(1);
|
||||
expect(stats.forkedRepos).toBe(1);
|
||||
expect(stats.originalRepos).toBe(5);
|
||||
expect(stats.reposWithStars).toBe(2);
|
||||
expect(stats.reposCreatedThisYear).toBe(1);
|
||||
expect(stats.averageStarsPerRepo).toBe(2.5); // 15 stars / 6 repos
|
||||
});
|
||||
|
||||
test("calculates language statistics correctly", () => {
|
||||
const topLanguages: Language[] = [
|
||||
{ languageName: "TypeScript", color: "#3178c6", value: 1000, percentage: 50 },
|
||||
{ languageName: "JavaScript", color: "#f1e05a", value: 500, percentage: 25 },
|
||||
{ languageName: "Python", color: "#3572A5", value: 500, percentage: 25 },
|
||||
];
|
||||
|
||||
const stats = calculateComputedStats([], topLanguages, createContributionStats([]));
|
||||
|
||||
expect(stats.languageCount).toBe(3);
|
||||
expect(stats.primaryLanguage).toBe("TypeScript");
|
||||
});
|
||||
|
||||
test("calculates year over year growth correctly", () => {
|
||||
const contributionStats = createContributionStats([
|
||||
{ month: `${lastYear}-01`, contributions: 50 },
|
||||
{ month: `${lastYear}-02`, contributions: 50 },
|
||||
{ month: `${currentYear}-01`, contributions: 75 },
|
||||
{ month: `${currentYear}-02`, contributions: 75 },
|
||||
]);
|
||||
|
||||
const stats = calculateComputedStats([], [], contributionStats);
|
||||
|
||||
expect(stats.contributionsLastYear).toBe(100);
|
||||
expect(stats.contributionsThisYear).toBe(150);
|
||||
expect(stats.yearOverYearGrowth).toBe(50); // 50% growth
|
||||
});
|
||||
|
||||
test("identifies most productive month", () => {
|
||||
const contributionStats = createContributionStats([
|
||||
{ month: `${currentYear}-01`, contributions: 50 },
|
||||
{ month: `${currentYear}-02`, contributions: 150 },
|
||||
{ month: `${currentYear}-03`, contributions: 75 },
|
||||
]);
|
||||
|
||||
const stats = calculateComputedStats([], [], contributionStats);
|
||||
|
||||
expect(stats.mostProductiveMonth).toEqual({
|
||||
month: `${currentYear}-02`,
|
||||
contributions: 150,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(5);
|
||||
expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(5);
|
||||
|
||||
const values = results
|
||||
.filter((r): r is PromiseFulfilledResult<number> => r.status === "fulfilled")
|
||||
.map((r) => r.value);
|
||||
expect(values).toEqual([2, 4, 6, 8, 10]);
|
||||
});
|
||||
|
||||
test("handles errors gracefully with Promise.allSettled", async () => {
|
||||
const items = [1, 2, 3];
|
||||
test("handles empty data gracefully", () => {
|
||||
const stats = calculateComputedStats([], [], createContributionStats([]));
|
||||
|
||||
const results = await processBatched(items, 2, async (item) => {
|
||||
if (item === 2) throw new Error("Test error");
|
||||
return item;
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results[0].status).toBe("fulfilled");
|
||||
expect(results[1].status).toBe("rejected");
|
||||
expect(results[2].status).toBe("fulfilled");
|
||||
expect(stats.totalRepos).toBe(0);
|
||||
expect(stats.languageCount).toBe(0);
|
||||
expect(stats.primaryLanguage).toBe(null);
|
||||
expect(stats.yearOverYearGrowth).toBe(null);
|
||||
expect(stats.mostProductiveMonth).toBe(null);
|
||||
expect(stats.totalTopics).toBe(0);
|
||||
expect(stats.topTopics).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("processes single batch correctly", async () => {
|
||||
const items = [1, 2, 3];
|
||||
test("aggregates topics correctly", () => {
|
||||
const repos = [
|
||||
createRepoInfo({ topics: ["typescript", "github-action", "automation"] }),
|
||||
createRepoInfo({ topics: ["typescript", "cli"] }),
|
||||
createRepoInfo({ topics: ["python", "automation"] }),
|
||||
];
|
||||
|
||||
const results = await processBatched(items, 10, async (item) => item);
|
||||
const stats = calculateComputedStats(repos, [], createContributionStats([]));
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("handles empty array", async () => {
|
||||
const results = await processBatched([], 5, async (item: number) => item);
|
||||
expect(results).toHaveLength(0);
|
||||
expect(stats.totalTopics).toBe(5);
|
||||
expect(stats.allTopics).toEqual(["automation", "cli", "github-action", "python", "typescript"]);
|
||||
|
||||
// Top topics should be sorted by count
|
||||
expect(stats.topTopics[0]).toEqual({ name: "typescript", count: 2 });
|
||||
expect(stats.topTopics[1]).toEqual({ name: "automation", count: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
259
src/index.ts
259
src/index.ts
@@ -9,15 +9,17 @@ import {
|
||||
ContributionStats,
|
||||
MonthlyContribution,
|
||||
RepoDetails,
|
||||
RepoStats,
|
||||
ComputedStats,
|
||||
TopicCount,
|
||||
} from "./Types";
|
||||
import type { GraphQlQueryResponseData } from "@octokit/graphql";
|
||||
|
||||
const ThrottledOctokit = Octokit.plugin(throttling);
|
||||
|
||||
// Constants
|
||||
const MAX_RETRY_COUNT = 5;
|
||||
const BATCH_SIZE = 10;
|
||||
const RETRY_DELAY_MS = 2000;
|
||||
const MAX_RETRY_COUNT = 10; // With exponential backoff: 1+2+4+8+16+32+64+128+256+512 = ~17 minutes max
|
||||
const INITIAL_RETRY_DELAY_MS = 1000;
|
||||
|
||||
/**
|
||||
* Log rate limit information from GraphQL response
|
||||
@@ -30,25 +32,6 @@ function logRateLimit(rateLimit: RateLimitInfo | undefined, context: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process promises in batches to avoid overwhelming the API
|
||||
*/
|
||||
export async function processBatched<T, R>(
|
||||
items: T[],
|
||||
batchSize: number,
|
||||
processor: (item: T) => Promise<R>
|
||||
): Promise<PromiseSettledResult<R>[]> {
|
||||
const results: PromiseSettledResult<R>[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.allSettled(batch.map(processor));
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile data with extended fields
|
||||
*/
|
||||
@@ -131,6 +114,8 @@ export async function getRepoData(
|
||||
name
|
||||
description
|
||||
isArchived
|
||||
isFork
|
||||
isPrivate
|
||||
createdAt
|
||||
updatedAt
|
||||
stargazers {
|
||||
@@ -141,6 +126,13 @@ export async function getRepoData(
|
||||
name
|
||||
color
|
||||
}
|
||||
repositoryTopics(first: 20) {
|
||||
nodes {
|
||||
topic {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
|
||||
edges {
|
||||
size
|
||||
@@ -300,8 +292,6 @@ export async function getUsersStars(octokit: Octokit, username: string) {
|
||||
username,
|
||||
per_page: 1,
|
||||
});
|
||||
// The total count is in the Link header or we can make a request with per_page=1
|
||||
// and check the last page. For simplicity, we'll count from headers if available.
|
||||
const linkHeader = response.headers.link;
|
||||
if (linkHeader) {
|
||||
const lastPageMatch = linkHeader.match(/page=(\d+)>; rel="last"/);
|
||||
@@ -313,7 +303,7 @@ export async function getUsersStars(octokit: Octokit, username: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contributor stats for a repo with retry limit
|
||||
* Get contributor stats for a repo with exponential backoff for 202 responses
|
||||
*/
|
||||
export async function getReposContributorsStats(
|
||||
octokit: Octokit,
|
||||
@@ -329,13 +319,22 @@ export async function getReposContributorsStats(
|
||||
|
||||
if (response.status === 202) {
|
||||
if (retryCount >= MAX_RETRY_COUNT) {
|
||||
console.warn(
|
||||
`Max retries (${MAX_RETRY_COUNT}) reached for ${owner}/${repo}, skipping`
|
||||
);
|
||||
console.warn(`Max retries (${MAX_RETRY_COUNT}) reached for ${owner}/${repo} after exponential backoff, skipping`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
||||
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s
|
||||
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
||||
const maxDelay = 60000; // Cap at 60 seconds per retry
|
||||
const actualDelay = Math.min(delay, maxDelay);
|
||||
|
||||
if (retryCount === 0) {
|
||||
console.log(`[202] ${owner}/${repo}: GitHub computing stats, waiting ${actualDelay}ms...`);
|
||||
} else {
|
||||
console.log(`[202] ${owner}/${repo}: Retry ${retryCount + 1}/${MAX_RETRY_COUNT}, waiting ${actualDelay}ms...`);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, actualDelay));
|
||||
return getReposContributorsStats(octokit, owner, repo, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -422,7 +421,6 @@ export function calculateContributionStats(
|
||||
if (allDays[i].count > 0) {
|
||||
currentStreak++;
|
||||
} else if (allDays[i].date !== today) {
|
||||
// Allow today to have 0 if we're still on today
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -503,6 +501,123 @@ export function aggregateLanguages(
|
||||
return { languages, codeByteTotal };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate computed stats from existing data (no API calls)
|
||||
*/
|
||||
export function calculateComputedStats(
|
||||
repoInfoList: Array<{
|
||||
stars: number;
|
||||
forks: number;
|
||||
isArchived: boolean;
|
||||
isFork: boolean;
|
||||
isPrivate: boolean;
|
||||
topics: string[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
languages: { edges: Array<{ size: number; node: { name: string; color: string } }> };
|
||||
}>,
|
||||
topLanguages: Language[],
|
||||
contributionStats: ContributionStats
|
||||
): ComputedStats {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentYearStr = `${currentYear}`;
|
||||
const lastYearStr = `${currentYear - 1}`;
|
||||
|
||||
// Repo statistics
|
||||
const totalRepos = repoInfoList.length;
|
||||
const publicRepos = repoInfoList.filter((r) => !r.isPrivate).length;
|
||||
const privateRepos = repoInfoList.filter((r) => r.isPrivate).length;
|
||||
const archivedRepos = repoInfoList.filter((r) => r.isArchived).length;
|
||||
const forkedRepos = repoInfoList.filter((r) => r.isFork).length;
|
||||
const originalRepos = totalRepos - forkedRepos;
|
||||
const activeRepos = repoInfoList.filter((r) => r.updatedAt.startsWith(currentYearStr)).length;
|
||||
const reposWithStars = repoInfoList.filter((r) => r.stars > 0).length;
|
||||
const reposCreatedThisYear = repoInfoList.filter((r) => r.createdAt.startsWith(currentYearStr)).length;
|
||||
|
||||
// Star statistics
|
||||
const totalStars = repoInfoList.reduce((sum, r) => sum + r.stars, 0);
|
||||
const averageStarsPerRepo = totalRepos > 0 ? Math.round((totalStars / totalRepos) * 100) / 100 : 0;
|
||||
|
||||
// Language statistics
|
||||
const languageCount = topLanguages.length;
|
||||
const primaryLanguage = topLanguages[0]?.languageName || null;
|
||||
|
||||
// Filter repos active this year for language calculation
|
||||
const reposThisYear = repoInfoList.filter((r) => r.updatedAt.startsWith(currentYearStr));
|
||||
const { languages: languagesThisYear } = aggregateLanguages(reposThisYear);
|
||||
const topLanguagesThisYear = languagesThisYear.slice(0, 10);
|
||||
const primaryLanguageThisYear = topLanguagesThisYear[0]?.languageName || null;
|
||||
|
||||
// Contribution statistics from monthly breakdown
|
||||
const contributionsThisYear = contributionStats.monthlyBreakdown
|
||||
.filter((m) => m.month.startsWith(currentYearStr))
|
||||
.reduce((sum, m) => sum + m.contributions, 0);
|
||||
|
||||
const contributionsLastYear = contributionStats.monthlyBreakdown
|
||||
.filter((m) => m.month.startsWith(lastYearStr))
|
||||
.reduce((sum, m) => sum + m.contributions, 0);
|
||||
|
||||
// Year over year growth
|
||||
const yearOverYearGrowth = contributionsLastYear > 0
|
||||
? Math.round(((contributionsThisYear - contributionsLastYear) / contributionsLastYear) * 10000) / 100
|
||||
: null;
|
||||
|
||||
// Most productive month
|
||||
let mostProductiveMonth: { month: string; contributions: number } | null = null;
|
||||
for (const m of contributionStats.monthlyBreakdown) {
|
||||
if (!mostProductiveMonth || m.contributions > mostProductiveMonth.contributions) {
|
||||
mostProductiveMonth = m;
|
||||
}
|
||||
}
|
||||
|
||||
// Topic statistics
|
||||
const topicCountMap = new Map<string, number>();
|
||||
const allTopicsSet = new Set<string>();
|
||||
for (const repo of repoInfoList) {
|
||||
for (const topic of repo.topics) {
|
||||
topicCountMap.set(topic, (topicCountMap.get(topic) || 0) + 1);
|
||||
allTopicsSet.add(topic);
|
||||
}
|
||||
}
|
||||
const topTopics: TopicCount[] = Array.from(topicCountMap.entries())
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 20);
|
||||
const allTopics = Array.from(allTopicsSet).sort();
|
||||
const totalTopics = allTopics.length;
|
||||
|
||||
return {
|
||||
// Repo stats
|
||||
totalRepos,
|
||||
publicRepos,
|
||||
privateRepos,
|
||||
archivedRepos,
|
||||
forkedRepos,
|
||||
originalRepos,
|
||||
activeRepos,
|
||||
reposWithStars,
|
||||
reposCreatedThisYear,
|
||||
averageStarsPerRepo,
|
||||
|
||||
// Language stats
|
||||
languageCount,
|
||||
primaryLanguage,
|
||||
primaryLanguageThisYear,
|
||||
topLanguagesThisYear,
|
||||
|
||||
// Topic stats
|
||||
totalTopics,
|
||||
topTopics,
|
||||
allTopics,
|
||||
|
||||
// Contribution stats
|
||||
contributionsThisYear,
|
||||
contributionsLastYear,
|
||||
yearOverYearGrowth,
|
||||
mostProductiveMonth,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable string
|
||||
*/
|
||||
@@ -536,16 +651,12 @@ async function main() {
|
||||
const octokit = new ThrottledOctokit({
|
||||
auth: token,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
||||
onRateLimit: (retryAfter, options, octokit) => {
|
||||
octokit.log.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`
|
||||
);
|
||||
|
||||
if (retryCount < 1) {
|
||||
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true; // Always retry on rate limit
|
||||
},
|
||||
onSecondaryRateLimit: (retryAfter, options, octokit) => {
|
||||
octokit.log.warn(
|
||||
@@ -591,9 +702,13 @@ async function main() {
|
||||
forks: number;
|
||||
description: string | null;
|
||||
isArchived: boolean;
|
||||
isFork: boolean;
|
||||
isPrivate: boolean;
|
||||
primaryLanguage: string | null;
|
||||
topics: string[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
languages: { edges: Array<{ size: number; node: { name: string; color: string } }> };
|
||||
}
|
||||
|
||||
const repoInfoList: RepoInfo[] = repos.map((repo: {
|
||||
@@ -603,9 +718,13 @@ async function main() {
|
||||
forkCount: number;
|
||||
description: string | null;
|
||||
isArchived: boolean;
|
||||
isFork: boolean;
|
||||
isPrivate: boolean;
|
||||
primaryLanguage: { name: string } | null;
|
||||
repositoryTopics: { nodes: Array<{ topic: { name: string } }> };
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
languages: { edges: Array<{ size: number; node: { name: string; color: string } }> };
|
||||
}) => {
|
||||
let repoOwner: string;
|
||||
let repoName: string;
|
||||
@@ -631,32 +750,34 @@ async function main() {
|
||||
forks: repo.forkCount,
|
||||
description: repo.description,
|
||||
isArchived: repo.isArchived,
|
||||
isFork: repo.isFork,
|
||||
isPrivate: repo.isPrivate,
|
||||
primaryLanguage: repo.primaryLanguage?.name || null,
|
||||
topics: repo.repositoryTopics.nodes.map((n) => n.topic.name),
|
||||
updatedAt: repo.updatedAt,
|
||||
createdAt: repo.createdAt,
|
||||
languages: repo.languages,
|
||||
};
|
||||
});
|
||||
|
||||
// Fetch contributor stats in batches
|
||||
// Fire ALL requests in parallel - let throttle plugin handle rate limiting
|
||||
const contribStats1 = performance.now();
|
||||
const contribStatsResults = await processBatched(
|
||||
repoInfoList,
|
||||
BATCH_SIZE,
|
||||
(repo) => getReposContributorsStats(octokit, repo.owner, repo.name)
|
||||
const contribStatsPromises = repoInfoList.map((repo) =>
|
||||
getReposContributorsStats(octokit, repo.owner, repo.name)
|
||||
);
|
||||
const contribStats2 = performance.now();
|
||||
console.log(`Contributor stats fetch time: ${(contribStats2 - contribStats1).toFixed(2)}ms`);
|
||||
|
||||
// Fetch view counts in batches (only for owned repos)
|
||||
const viewCount1 = performance.now();
|
||||
const ownedRepos = repoInfoList.filter((r) => r.isOwner);
|
||||
const viewCountResults = await processBatched(
|
||||
ownedRepos,
|
||||
BATCH_SIZE,
|
||||
(repo) => getReposViewCount(octokit, repo.owner, repo.name)
|
||||
const viewCountPromises = ownedRepos.map((repo) =>
|
||||
getReposViewCount(octokit, repo.owner, repo.name)
|
||||
);
|
||||
const viewCount2 = performance.now();
|
||||
console.log(`View count fetch time: ${(viewCount2 - viewCount1).toFixed(2)}ms`);
|
||||
|
||||
// Wait for all in parallel
|
||||
const [contribStatsResults, viewCountResults] = await Promise.all([
|
||||
Promise.allSettled(contribStatsPromises),
|
||||
Promise.allSettled(viewCountPromises),
|
||||
]);
|
||||
|
||||
const contribStats2 = performance.now();
|
||||
console.log(`All repo stats fetch time: ${(contribStats2 - contribStats1).toFixed(2)}ms`);
|
||||
|
||||
// Process contributor stats
|
||||
const parseStats1 = performance.now();
|
||||
@@ -709,6 +830,9 @@ async function main() {
|
||||
const calcStats2 = performance.now();
|
||||
console.log(`Calculate contribution stats time: ${(calcStats2 - calcStats1).toFixed(2)}ms`);
|
||||
|
||||
// Calculate computed stats (no API calls)
|
||||
const computedStats = calculateComputedStats(repoInfoList, topLanguages, contributionStats);
|
||||
|
||||
// Build top repos list (top 10 by stars)
|
||||
const topRepos: RepoDetails[] = repoInfoList
|
||||
.filter((r) => r.isOwner && !r.isArchived)
|
||||
@@ -720,15 +844,34 @@ async function main() {
|
||||
stars: r.stars,
|
||||
forks: r.forks,
|
||||
isArchived: r.isArchived,
|
||||
isFork: r.isFork,
|
||||
isPrivate: r.isPrivate,
|
||||
primaryLanguage: r.primaryLanguage,
|
||||
topics: r.topics,
|
||||
updatedAt: r.updatedAt,
|
||||
createdAt: r.createdAt,
|
||||
}));
|
||||
|
||||
// Build repo stats
|
||||
const repoStats: RepoStats = {
|
||||
totalRepos: computedStats.totalRepos,
|
||||
publicRepos: computedStats.publicRepos,
|
||||
privateRepos: computedStats.privateRepos,
|
||||
archivedRepos: computedStats.archivedRepos,
|
||||
forkedRepos: computedStats.forkedRepos,
|
||||
originalRepos: computedStats.originalRepos,
|
||||
activeRepos: computedStats.activeRepos,
|
||||
reposWithStars: computedStats.reposWithStars,
|
||||
reposCreatedThisYear: computedStats.reposCreatedThisYear,
|
||||
averageStarsPerRepo: computedStats.averageStarsPerRepo,
|
||||
};
|
||||
|
||||
// Build output
|
||||
const tableData = [
|
||||
["Name", userDetails.data.name || ""],
|
||||
["Username", username],
|
||||
["Total Repos", computedStats.totalRepos],
|
||||
["Active Repos (this year)", computedStats.activeRepos],
|
||||
["Repository Views", formatNumber(repoViews)],
|
||||
["Lines of Code Changed", formatNumber(linesOfCodeChanged)],
|
||||
["Lines Added", formatNumber(linesAdded)],
|
||||
@@ -738,15 +881,21 @@ async function main() {
|
||||
["Total Pull Requests", userData.user.pullRequests.totalCount],
|
||||
["Total PR Reviews", contributionsCollection.totalPullRequestReviewContributions],
|
||||
["Code Bytes Total", formatBytes(codeByteTotal)],
|
||||
["Top Languages", topLanguages.slice(0, 5).map((lang) => lang.languageName).join(", ")],
|
||||
["Languages Used", computedStats.languageCount],
|
||||
["Primary Language", computedStats.primaryLanguage || "N/A"],
|
||||
["Primary Language (this year)", computedStats.primaryLanguageThisYear || "N/A"],
|
||||
["Fork Count", forkCount],
|
||||
["Star Count", starCount],
|
||||
["Avg Stars/Repo", computedStats.averageStarsPerRepo],
|
||||
["Stars Given", starsGiven],
|
||||
["Followers", userData.user.followers.totalCount],
|
||||
["Following", userData.user.following.totalCount],
|
||||
["Current Streak", `${contributionStats.currentStreak} days`],
|
||||
["Longest Streak", `${contributionStats.longestStreak} days`],
|
||||
["Most Active Day", contributionStats.mostActiveDay],
|
||||
["Contributions (this year)", computedStats.contributionsThisYear],
|
||||
["Contributions (last year)", computedStats.contributionsLastYear],
|
||||
["YoY Growth", computedStats.yearOverYearGrowth !== null ? `${computedStats.yearOverYearGrowth}%` : "N/A"],
|
||||
["Total Contributions", contributionsCollection.contributionCalendar.totalContributions],
|
||||
["Closed Issues", userData.user.closedIssues.totalCount],
|
||||
["Open Issues", userData.user.openIssues.totalCount],
|
||||
@@ -792,6 +941,8 @@ async function main() {
|
||||
discussionsAnswered: userData.user.repositoryDiscussionComments.totalCount,
|
||||
totalContributions: contributionsCollection.contributionCalendar.totalContributions,
|
||||
contributionStats,
|
||||
repoStats,
|
||||
computedStats,
|
||||
contributionsCollection,
|
||||
topRepos,
|
||||
closedIssues: userData.user.closedIssues.totalCount,
|
||||
|
||||
Reference in New Issue
Block a user