@stoked-ui/github 0.0.0-a.0 → 0.1.0-alpha.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GithubBranch/GithubBranch.d.ts +12 -0
- package/GithubBranch/GithubBranch.js +177 -0
- package/GithubBranch/index.d.ts +2 -0
- package/GithubBranch/index.js +2 -0
- package/GithubBranch/package.json +6 -0
- package/GithubCalendar/GithubCalendar.d.ts +6 -2
- package/GithubCalendar/GithubCalendar.js +70 -28
- package/GithubCommit/GithubCommit.d.ts +11 -0
- package/GithubCommit/GithubCommit.js +170 -0
- package/GithubCommit/index.d.ts +2 -0
- package/GithubCommit/index.js +2 -0
- package/GithubCommit/package.json +6 -0
- package/GithubEvents/GithubEvents.d.ts +4 -24
- package/GithubEvents/GithubEvents.js +141 -220
- package/apiHandlers/createGithubBranchHandler.d.ts +12 -0
- package/apiHandlers/createGithubBranchHandler.js +41 -0
- package/apiHandlers/createGithubCommitHandler.d.ts +12 -0
- package/apiHandlers/createGithubCommitHandler.js +39 -0
- package/apiHandlers/createGithubContributionsHandler.d.ts +15 -0
- package/apiHandlers/createGithubContributionsHandler.js +41 -0
- package/apiHandlers/createGithubEventsHandler.d.ts +15 -0
- package/apiHandlers/createGithubEventsHandler.js +50 -0
- package/apiHandlers/getBranchCompareDetails.d.ts +9 -0
- package/apiHandlers/getBranchCompareDetails.js +29 -0
- package/apiHandlers/getCommitDetails.d.ts +8 -0
- package/apiHandlers/getCommitDetails.js +34 -0
- package/apiHandlers/getGithubContributions.d.ts +8 -0
- package/apiHandlers/getGithubContributions.js +126 -0
- package/apiHandlers/getGithubEvents.d.ts +25 -0
- package/apiHandlers/getGithubEvents.js +142 -0
- package/apiHandlers/getPullRequestDetails.js +2 -2
- package/apiHandlers/githubApi.d.ts +17 -0
- package/apiHandlers/githubApi.js +155 -0
- package/apiHandlers/index.d.ts +9 -0
- package/apiHandlers/index.js +9 -1
- package/components/GithubContributorsList.d.ts +8 -0
- package/components/GithubContributorsList.js +72 -0
- package/components/fetchGithubViewData.d.ts +1 -0
- package/components/fetchGithubViewData.js +10 -0
- package/index.d.ts +6 -1
- package/index.js +5 -2
- package/modern/GithubBranch/GithubBranch.js +177 -0
- package/modern/GithubBranch/index.js +2 -0
- package/modern/GithubCalendar/GithubCalendar.js +70 -28
- package/modern/GithubCommit/GithubCommit.js +170 -0
- package/modern/GithubCommit/index.js +2 -0
- package/modern/GithubEvents/GithubEvents.js +141 -220
- package/modern/apiHandlers/createGithubBranchHandler.js +41 -0
- package/modern/apiHandlers/createGithubCommitHandler.js +39 -0
- package/modern/apiHandlers/createGithubContributionsHandler.js +41 -0
- package/modern/apiHandlers/createGithubEventsHandler.js +50 -0
- package/modern/apiHandlers/getBranchCompareDetails.js +29 -0
- package/modern/apiHandlers/getCommitDetails.js +34 -0
- package/modern/apiHandlers/getGithubContributions.js +126 -0
- package/modern/apiHandlers/getGithubEvents.js +142 -0
- package/modern/apiHandlers/getPullRequestDetails.js +2 -2
- package/modern/apiHandlers/githubApi.js +155 -0
- package/modern/apiHandlers/index.js +9 -1
- package/modern/components/GithubContributorsList.js +72 -0
- package/modern/components/fetchGithubViewData.js +10 -0
- package/modern/index.js +5 -2
- package/node/GithubBranch/GithubBranch.js +185 -0
- package/node/GithubBranch/index.js +9 -0
- package/node/GithubCalendar/GithubCalendar.js +70 -28
- package/node/GithubCommit/GithubCommit.js +178 -0
- package/node/GithubCommit/index.js +9 -0
- package/node/GithubEvents/GithubEvents.js +148 -223
- package/node/apiHandlers/createGithubBranchHandler.js +48 -0
- package/node/apiHandlers/createGithubCommitHandler.js +46 -0
- package/node/apiHandlers/createGithubContributionsHandler.js +48 -0
- package/node/apiHandlers/createGithubEventsHandler.js +57 -0
- package/node/apiHandlers/getBranchCompareDetails.js +35 -0
- package/node/apiHandlers/getCommitDetails.js +40 -0
- package/node/apiHandlers/getGithubContributions.js +132 -0
- package/node/apiHandlers/getGithubEvents.js +149 -0
- package/node/apiHandlers/getPullRequestDetails.js +2 -2
- package/node/apiHandlers/githubApi.js +168 -0
- package/node/apiHandlers/index.js +64 -1
- package/node/components/GithubContributorsList.js +80 -0
- package/node/components/fetchGithubViewData.js +16 -0
- package/node/index.js +77 -2
- package/package.json +6 -6
- package/types/github.d.ts +75 -11
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import githubEventsQuery from './getGithubEvents';
|
|
2
|
+
function getQueryValue(value) {
|
|
3
|
+
return Array.isArray(value) ? value[0] : value;
|
|
4
|
+
}
|
|
5
|
+
function parseNumber(value, fallback) {
|
|
6
|
+
const parsed = Number(getQueryValue(value));
|
|
7
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
8
|
+
}
|
|
9
|
+
export default function createGithubEventsHandler(config = {}) {
|
|
10
|
+
return async function handler(req, res) {
|
|
11
|
+
if (req.method !== 'GET') {
|
|
12
|
+
var _res$setHeader;
|
|
13
|
+
(_res$setHeader = res.setHeader) == null || _res$setHeader.call(res, 'Allow', 'GET');
|
|
14
|
+
return res.status(405).json({
|
|
15
|
+
message: 'Method not allowed'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const githubUser = getQueryValue(req.query.username);
|
|
19
|
+
if (!githubUser) {
|
|
20
|
+
return res.status(400).json({
|
|
21
|
+
message: 'username is required'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const query = {
|
|
25
|
+
page: parseNumber(req.query.page, 1),
|
|
26
|
+
per_page: parseNumber(req.query.per_page, 40),
|
|
27
|
+
repo: getQueryValue(req.query.repo) || undefined,
|
|
28
|
+
action: getQueryValue(req.query.action) || undefined,
|
|
29
|
+
date: getQueryValue(req.query.date) || undefined,
|
|
30
|
+
description: getQueryValue(req.query.description) || undefined
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
var _config$getGithubToke, _res$setHeader2;
|
|
34
|
+
const githubToken = ((_config$getGithubToke = config.getGithubToken) == null ? void 0 : _config$getGithubToke.call(config, req)) || process.env.GITHUB_TOKEN;
|
|
35
|
+
const data = await githubEventsQuery({
|
|
36
|
+
query,
|
|
37
|
+
githubUser,
|
|
38
|
+
githubToken
|
|
39
|
+
});
|
|
40
|
+
(_res$setHeader2 = res.setHeader) == null || _res$setHeader2.call(res, 'Cache-Control', 's-maxage=300, stale-while-revalidate=3600');
|
|
41
|
+
return res.status(200).json(data);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
const status = message.includes('username is required') ? 400 : 502;
|
|
45
|
+
return res.status(status).json({
|
|
46
|
+
message
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GithubBranchData } from '../types/github';
|
|
2
|
+
type GetBranchCompareDetailsParams = {
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
base: string;
|
|
6
|
+
head: string;
|
|
7
|
+
};
|
|
8
|
+
export default function getBranchCompareDetails(params: GetBranchCompareDetailsParams): Promise<GithubBranchData>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildGithubContributors, fetchGithubResource, normalizeGithubCommit, normalizeGithubFile, summarizeGithubStats } from './githubApi';
|
|
2
|
+
export default async function getBranchCompareDetails(params) {
|
|
3
|
+
const {
|
|
4
|
+
owner,
|
|
5
|
+
repo,
|
|
6
|
+
base,
|
|
7
|
+
head
|
|
8
|
+
} = params;
|
|
9
|
+
if (!owner || !repo || !base || !head) {
|
|
10
|
+
throw new Error('Missing required parameters: owner, repo, base, head');
|
|
11
|
+
}
|
|
12
|
+
const compare = await fetchGithubResource(`/repos/${owner}/${repo}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`);
|
|
13
|
+
const commits = (compare.commits || []).map(commit => normalizeGithubCommit(commit));
|
|
14
|
+
const files = (compare.files || []).map(file => normalizeGithubFile(file));
|
|
15
|
+
return {
|
|
16
|
+
repo: `${owner}/${repo}`,
|
|
17
|
+
baseRef: base,
|
|
18
|
+
headRef: head,
|
|
19
|
+
status: compare.status || 'unknown',
|
|
20
|
+
aheadBy: compare.ahead_by || 0,
|
|
21
|
+
behindBy: compare.behind_by || 0,
|
|
22
|
+
totalCommits: compare.total_commits || commits.length,
|
|
23
|
+
url: compare.html_url || `https://github.com/${owner}/${repo}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`,
|
|
24
|
+
contributors: buildGithubContributors(compare.commits || []),
|
|
25
|
+
commits,
|
|
26
|
+
files,
|
|
27
|
+
stats: summarizeGithubStats(files)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildGithubContributors, fetchGithubResource, getGithubMessageSummary, getGithubShortRef, normalizeGithubCommit, normalizeGithubFile, summarizeGithubStats } from './githubApi';
|
|
2
|
+
export default async function getCommitDetails(params) {
|
|
3
|
+
var _commit$commit, _commit$commit2, _commit$commit3, _commit$commit4;
|
|
4
|
+
const {
|
|
5
|
+
owner,
|
|
6
|
+
repo,
|
|
7
|
+
ref
|
|
8
|
+
} = params;
|
|
9
|
+
if (!owner || !repo || !ref) {
|
|
10
|
+
throw new Error('Missing required parameters: owner, repo, ref');
|
|
11
|
+
}
|
|
12
|
+
const commit = await fetchGithubResource(`/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`);
|
|
13
|
+
const files = (commit.files || []).map(file => normalizeGithubFile(file));
|
|
14
|
+
const commits = [normalizeGithubCommit(commit)];
|
|
15
|
+
const contributor = buildGithubContributors([commit])[0] || {
|
|
16
|
+
login: commits[0].author.login,
|
|
17
|
+
name: commits[0].author.name,
|
|
18
|
+
avatarUrl: commits[0].author.avatar,
|
|
19
|
+
contributions: 1
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
repo: `${owner}/${repo}`,
|
|
23
|
+
ref: commit.sha || ref,
|
|
24
|
+
shortRef: getGithubShortRef(commit.sha || ref),
|
|
25
|
+
url: commit.html_url || `https://github.com/${owner}/${repo}/commit/${encodeURIComponent(commit.sha || ref)}`,
|
|
26
|
+
summary: getGithubMessageSummary(((_commit$commit = commit.commit) == null ? void 0 : _commit$commit.message) || ''),
|
|
27
|
+
message: ((_commit$commit2 = commit.commit) == null ? void 0 : _commit$commit2.message) || '',
|
|
28
|
+
committedAt: ((_commit$commit3 = commit.commit) == null || (_commit$commit3 = _commit$commit3.author) == null ? void 0 : _commit$commit3.date) || ((_commit$commit4 = commit.commit) == null || (_commit$commit4 = _commit$commit4.committer) == null ? void 0 : _commit$commit4.date) || '',
|
|
29
|
+
contributor,
|
|
30
|
+
commits,
|
|
31
|
+
files,
|
|
32
|
+
stats: summarizeGithubStats(files)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GithubContributionsResponse } from '../types/github';
|
|
2
|
+
export interface GetGithubContributionsParams {
|
|
3
|
+
githubUser: string;
|
|
4
|
+
githubToken?: string;
|
|
5
|
+
from?: string;
|
|
6
|
+
to?: string;
|
|
7
|
+
}
|
|
8
|
+
export default function getGithubContributions({ githubUser, githubToken, from, to, }: GetGithubContributionsParams): Promise<GithubContributionsResponse>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const CONTRIBUTION_LEVEL_MAP = {
|
|
2
|
+
NONE: 0,
|
|
3
|
+
FIRST_QUARTILE: 1,
|
|
4
|
+
SECOND_QUARTILE: 2,
|
|
5
|
+
THIRD_QUARTILE: 3,
|
|
6
|
+
FOURTH_QUARTILE: 4
|
|
7
|
+
};
|
|
8
|
+
const GITHUB_CONTRIBUTIONS_QUERY = `
|
|
9
|
+
query GithubContributions($login: String!, $from: DateTime!, $to: DateTime!) {
|
|
10
|
+
user(login: $login) {
|
|
11
|
+
contributionsCollection(from: $from, to: $to) {
|
|
12
|
+
contributionCalendar {
|
|
13
|
+
totalContributions
|
|
14
|
+
weeks {
|
|
15
|
+
contributionDays {
|
|
16
|
+
date
|
|
17
|
+
contributionCount
|
|
18
|
+
contributionLevel
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
function isValidDateInput(value) {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return !Number.isNaN(new Date(value).getTime());
|
|
31
|
+
}
|
|
32
|
+
function toDateRange(from, to) {
|
|
33
|
+
const rangeEnd = isValidDateInput(to) ? new Date(to) : new Date();
|
|
34
|
+
const rangeStart = isValidDateInput(from) ? new Date(from) : new Date(rangeEnd);
|
|
35
|
+
if (!isValidDateInput(from)) {
|
|
36
|
+
rangeStart.setFullYear(rangeEnd.getFullYear() - 1);
|
|
37
|
+
}
|
|
38
|
+
if (rangeStart > rangeEnd) {
|
|
39
|
+
throw new Error('Invalid contribution range: "from" must be before "to".');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
from: rangeStart,
|
|
43
|
+
to: rangeEnd
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function buildContributionTotals(contributions) {
|
|
47
|
+
return contributions.reduce((acc, contribution) => {
|
|
48
|
+
const year = contribution.date.slice(0, 4);
|
|
49
|
+
acc[year] = (acc[year] || 0) + contribution.count;
|
|
50
|
+
return acc;
|
|
51
|
+
}, {});
|
|
52
|
+
}
|
|
53
|
+
function buildCountLabel(total, from, to) {
|
|
54
|
+
const fromYear = from.getFullYear();
|
|
55
|
+
const toYear = to.getFullYear();
|
|
56
|
+
if (fromYear === toYear) {
|
|
57
|
+
return `${total} contributions in ${fromYear}`;
|
|
58
|
+
}
|
|
59
|
+
return `${total} contributions from ${fromYear} to ${toYear}`;
|
|
60
|
+
}
|
|
61
|
+
export default async function getGithubContributions({
|
|
62
|
+
githubUser,
|
|
63
|
+
githubToken = process.env.GITHUB_TOKEN,
|
|
64
|
+
from,
|
|
65
|
+
to
|
|
66
|
+
}) {
|
|
67
|
+
var _payload$errors2, _payload$data;
|
|
68
|
+
if (!githubUser) {
|
|
69
|
+
throw new Error('Missing required parameter: githubUser');
|
|
70
|
+
}
|
|
71
|
+
if (!githubToken) {
|
|
72
|
+
throw new Error('GitHub token not configured');
|
|
73
|
+
}
|
|
74
|
+
const range = toDateRange(from, to);
|
|
75
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'User-Agent': 'stoked-ui-github-calendar',
|
|
80
|
+
Authorization: `Bearer ${githubToken}`
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
query: GITHUB_CONTRIBUTIONS_QUERY,
|
|
84
|
+
variables: {
|
|
85
|
+
login: githubUser,
|
|
86
|
+
from: range.from.toISOString(),
|
|
87
|
+
to: range.to.toISOString()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
const payload = await response.json();
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
var _payload$errors;
|
|
94
|
+
const message = (payload == null ? void 0 : payload.message) || (payload == null || (_payload$errors = payload.errors) == null || (_payload$errors = _payload$errors[0]) == null ? void 0 : _payload$errors.message) || `GitHub GraphQL error: ${response.status}`;
|
|
95
|
+
throw new Error(message);
|
|
96
|
+
}
|
|
97
|
+
if (payload != null && (_payload$errors2 = payload.errors) != null && _payload$errors2.length) {
|
|
98
|
+
throw new Error(payload.errors.map(error => error.message || 'Unknown GitHub GraphQL error').join('; '));
|
|
99
|
+
}
|
|
100
|
+
const calendar = payload == null || (_payload$data = payload.data) == null || (_payload$data = _payload$data.user) == null || (_payload$data = _payload$data.contributionsCollection) == null ? void 0 : _payload$data.contributionCalendar;
|
|
101
|
+
if (!calendar) {
|
|
102
|
+
throw new Error(`No contribution calendar returned for "${githubUser}".`);
|
|
103
|
+
}
|
|
104
|
+
const contributions = (calendar.weeks || []).flatMap(week => week.contributionDays || []).map(day => {
|
|
105
|
+
var _CONTRIBUTION_LEVEL_M;
|
|
106
|
+
return {
|
|
107
|
+
date: day.date,
|
|
108
|
+
count: day.contributionCount,
|
|
109
|
+
level: (_CONTRIBUTION_LEVEL_M = CONTRIBUTION_LEVEL_MAP[day.contributionLevel]) != null ? _CONTRIBUTION_LEVEL_M : 0
|
|
110
|
+
};
|
|
111
|
+
}).sort((a, b) => {
|
|
112
|
+
if (a.date < b.date) {
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
if (a.date > b.date) {
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
});
|
|
120
|
+
const total = buildContributionTotals(contributions);
|
|
121
|
+
return {
|
|
122
|
+
total,
|
|
123
|
+
contributions,
|
|
124
|
+
countLabel: buildCountLabel(calendar.totalContributions || 0, range.from, range.to)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GitHubEvent } from '../types/github';
|
|
2
|
+
export type EventsQuery = {
|
|
3
|
+
page?: number;
|
|
4
|
+
per_page?: number;
|
|
5
|
+
repo?: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
date?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function githubEventsQuery({ query, githubUser, githubToken }: {
|
|
11
|
+
query: EventsQuery;
|
|
12
|
+
githubUser: string;
|
|
13
|
+
githubToken?: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
events: GitHubEvent[];
|
|
16
|
+
total: number;
|
|
17
|
+
repositories: string[];
|
|
18
|
+
actionTypes: string[];
|
|
19
|
+
page: number;
|
|
20
|
+
per_page: number;
|
|
21
|
+
total_pages: number;
|
|
22
|
+
total_fetched_events: number;
|
|
23
|
+
max_pages_fetched: number;
|
|
24
|
+
}>;
|
|
25
|
+
export default githubEventsQuery;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
function parseLinkHeader(header) {
|
|
2
|
+
if (!header) return {};
|
|
3
|
+
return header.split(',').reduce((links, part) => {
|
|
4
|
+
const match = part.match(/<(.+)>;\s*rel="([\w]+)"/);
|
|
5
|
+
if (match) {
|
|
6
|
+
const [, url, rel] = match;
|
|
7
|
+
if (rel === 'next' || rel === 'last') {
|
|
8
|
+
links[rel] = url;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return links;
|
|
12
|
+
}, {});
|
|
13
|
+
}
|
|
14
|
+
export async function githubEventsQuery({
|
|
15
|
+
query,
|
|
16
|
+
githubUser,
|
|
17
|
+
githubToken
|
|
18
|
+
}) {
|
|
19
|
+
try {
|
|
20
|
+
const {
|
|
21
|
+
page = query.page || 1,
|
|
22
|
+
per_page = query.per_page || 100,
|
|
23
|
+
repo,
|
|
24
|
+
action,
|
|
25
|
+
date,
|
|
26
|
+
description
|
|
27
|
+
} = query;
|
|
28
|
+
const pageNum = Number(page);
|
|
29
|
+
const perPage = Number(per_page);
|
|
30
|
+
console.log(`Fetching events for user: ${githubUser}, page: ${pageNum}, per_page: ${perPage}`);
|
|
31
|
+
let allEvents = [];
|
|
32
|
+
let hasMore = true;
|
|
33
|
+
let githubPage = 1;
|
|
34
|
+
const maxPages = 30;
|
|
35
|
+
const fetchPerPage = 100;
|
|
36
|
+
while (hasMore && githubPage <= maxPages) {
|
|
37
|
+
console.log(`Fetching page ${githubPage}...`);
|
|
38
|
+
const fetchOptions = {
|
|
39
|
+
headers: {
|
|
40
|
+
'User-Agent': 'brianstoker.com-website'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
if (githubToken) {
|
|
44
|
+
fetchOptions.headers.Authorization = `token ${githubToken}`;
|
|
45
|
+
}
|
|
46
|
+
const queryParams = new URLSearchParams({
|
|
47
|
+
page: String(githubPage),
|
|
48
|
+
per_page: String(fetchPerPage)
|
|
49
|
+
});
|
|
50
|
+
const response = await fetch(`https://api.github.com/users/${githubUser}/events?${queryParams}`, fetchOptions);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errorText = await response.text();
|
|
53
|
+
console.error(`GitHub API error: ${response.status}`, errorText);
|
|
54
|
+
throw new Error(`GitHub API error: ${response.status} - ${errorText}`);
|
|
55
|
+
}
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
console.log(`Page ${githubPage}: Received ${data.length} events`);
|
|
58
|
+
if (data.length === 0) {
|
|
59
|
+
hasMore = false;
|
|
60
|
+
} else {
|
|
61
|
+
allEvents = [...allEvents, ...data];
|
|
62
|
+
console.log(`Total events so far: ${allEvents.length}`);
|
|
63
|
+
const linkHeader = response.headers.get('Link');
|
|
64
|
+
const links = parseLinkHeader(linkHeader);
|
|
65
|
+
hasMore = !!links.next;
|
|
66
|
+
githubPage++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
allEvents = Array.from(new Map(allEvents.map(event => [event.id, event])).values());
|
|
70
|
+
console.log(`Final total events: ${allEvents.length}`);
|
|
71
|
+
let filteredEvents = allEvents;
|
|
72
|
+
if (repo) {
|
|
73
|
+
filteredEvents = filteredEvents.filter(event => event.repo.name === repo);
|
|
74
|
+
}
|
|
75
|
+
if (action) {
|
|
76
|
+
filteredEvents = filteredEvents.filter(event => event.type.replace('Event', '') === action);
|
|
77
|
+
}
|
|
78
|
+
if (date) {
|
|
79
|
+
const now = new Date();
|
|
80
|
+
let cutoffDate;
|
|
81
|
+
switch (date) {
|
|
82
|
+
case 'today':
|
|
83
|
+
cutoffDate = new Date(now.setHours(0, 0, 0, 0));
|
|
84
|
+
break;
|
|
85
|
+
case 'yesterday':
|
|
86
|
+
cutoffDate = new Date(now);
|
|
87
|
+
cutoffDate.setDate(cutoffDate.getDate() - 1);
|
|
88
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
89
|
+
break;
|
|
90
|
+
case 'week':
|
|
91
|
+
cutoffDate = new Date(now);
|
|
92
|
+
cutoffDate.setDate(cutoffDate.getDate() - 7);
|
|
93
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
94
|
+
break;
|
|
95
|
+
case 'month':
|
|
96
|
+
cutoffDate = new Date(now);
|
|
97
|
+
cutoffDate.setMonth(cutoffDate.getMonth() - 1);
|
|
98
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
cutoffDate = new Date(0);
|
|
102
|
+
}
|
|
103
|
+
filteredEvents = filteredEvents.filter(event => new Date(event.created_at) >= cutoffDate);
|
|
104
|
+
}
|
|
105
|
+
if (description) {
|
|
106
|
+
filteredEvents = filteredEvents.filter(event => {
|
|
107
|
+
var _event$payload$commit, _event$payload$pull_r, _event$payload$issue, _event$payload$issue2;
|
|
108
|
+
let eventDescription = '';
|
|
109
|
+
if (event.type === 'PushEvent' && (_event$payload$commit = event.payload.commits) != null && _event$payload$commit.length) {
|
|
110
|
+
eventDescription = `Pushed ${event.payload.commits.length} commits`;
|
|
111
|
+
} else if (event.type === 'PullRequestEvent' && (_event$payload$pull_r = event.payload.pull_request) != null && _event$payload$pull_r.title) {
|
|
112
|
+
eventDescription = event.payload.pull_request.title;
|
|
113
|
+
} else if (event.type === 'IssuesEvent' && (_event$payload$issue = event.payload.issue) != null && _event$payload$issue.title) {
|
|
114
|
+
eventDescription = event.payload.issue.title;
|
|
115
|
+
} else if (event.type === 'IssueCommentEvent' && (_event$payload$issue2 = event.payload.issue) != null && _event$payload$issue2.title) {
|
|
116
|
+
eventDescription = `Commented on issue: ${event.payload.issue.title}`;
|
|
117
|
+
}
|
|
118
|
+
return eventDescription.toLowerCase().includes(description.toLowerCase());
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const startIndex = (pageNum - 1) * perPage;
|
|
122
|
+
const endIndex = startIndex + perPage;
|
|
123
|
+
const paginatedEvents = filteredEvents.slice(startIndex, endIndex);
|
|
124
|
+
const repositories = Array.from(new Set(allEvents.map(event => event.repo.name))).sort();
|
|
125
|
+
const actionTypes = Array.from(new Set(allEvents.map(event => event.type.replace('Event', '')))).sort();
|
|
126
|
+
return {
|
|
127
|
+
events: paginatedEvents,
|
|
128
|
+
total: filteredEvents.length,
|
|
129
|
+
repositories,
|
|
130
|
+
actionTypes,
|
|
131
|
+
page: pageNum,
|
|
132
|
+
per_page: perPage,
|
|
133
|
+
total_pages: Math.ceil(filteredEvents.length / perPage),
|
|
134
|
+
total_fetched_events: allEvents.length,
|
|
135
|
+
max_pages_fetched: githubPage - 1
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error fetching GitHub events:', error);
|
|
139
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export default githubEventsQuery;
|
|
@@ -37,7 +37,7 @@ export default async function getPullRequestDetails(params) {
|
|
|
37
37
|
if (githubToken) {
|
|
38
38
|
headers.Authorization = `token ${githubToken}`;
|
|
39
39
|
}
|
|
40
|
-
async
|
|
40
|
+
const fetchWithRateLimit = async url => {
|
|
41
41
|
const response = await fetch(url, {
|
|
42
42
|
headers
|
|
43
43
|
});
|
|
@@ -59,7 +59,7 @@ export default async function getPullRequestDetails(params) {
|
|
|
59
59
|
data: await response.json(),
|
|
60
60
|
rateLimit
|
|
61
61
|
};
|
|
62
|
-
}
|
|
62
|
+
};
|
|
63
63
|
|
|
64
64
|
// Fetch basic PR information
|
|
65
65
|
const {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GithubChangedFile, GithubCommitListItem, GithubCommitStats, GithubContributor, GithubDiffLine } from '../types/github';
|
|
2
|
+
export declare function fetchGithubResource<T>(path: string): Promise<T>;
|
|
3
|
+
export declare function parseGithubDiff(patch: string | undefined, maxLines?: number): GithubDiffLine[];
|
|
4
|
+
export declare function normalizeGithubFile(file: {
|
|
5
|
+
filename?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
additions?: number;
|
|
9
|
+
deletions?: number;
|
|
10
|
+
patch?: string;
|
|
11
|
+
diff?: GithubDiffLine[];
|
|
12
|
+
}, maxDiffLines?: number): GithubChangedFile;
|
|
13
|
+
export declare function normalizeGithubCommit(commit: any): GithubCommitListItem;
|
|
14
|
+
export declare function buildGithubContributors(commits: any[]): GithubContributor[];
|
|
15
|
+
export declare function summarizeGithubStats(files: GithubChangedFile[]): GithubCommitStats;
|
|
16
|
+
export declare function getGithubMessageSummary(message: string): string;
|
|
17
|
+
export declare function getGithubShortRef(ref: string, length?: number): string;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
|
2
|
+
const DEFAULT_DIFF_LINE_LIMIT = 24;
|
|
3
|
+
function getGithubHeaders() {
|
|
4
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
5
|
+
const headers = {
|
|
6
|
+
'User-Agent': 'stoked-ui-github-components',
|
|
7
|
+
Accept: 'application/vnd.github+json'
|
|
8
|
+
};
|
|
9
|
+
if (githubToken) {
|
|
10
|
+
headers.Authorization = `token ${githubToken}`;
|
|
11
|
+
}
|
|
12
|
+
return headers;
|
|
13
|
+
}
|
|
14
|
+
export async function fetchGithubResource(path) {
|
|
15
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}${path}`, {
|
|
16
|
+
headers: getGithubHeaders()
|
|
17
|
+
});
|
|
18
|
+
const rateLimit = {
|
|
19
|
+
remaining: response.headers.get('x-ratelimit-remaining'),
|
|
20
|
+
reset: response.headers.get('x-ratelimit-reset')
|
|
21
|
+
};
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
if (response.status === 403 && rateLimit.remaining === '0') {
|
|
25
|
+
const resetDate = rateLimit.reset ? new Date(Number(rateLimit.reset) * 1000).toLocaleString() : 'later';
|
|
26
|
+
throw new Error(`GitHub rate limit exceeded. Resets at ${resetDate}.`);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(body || `GitHub API error: ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
export function parseGithubDiff(patch, maxLines = DEFAULT_DIFF_LINE_LIMIT) {
|
|
33
|
+
if (!patch) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const patchLines = patch.split('\n');
|
|
37
|
+
const visibleLines = patchLines.slice(0, maxLines);
|
|
38
|
+
const diffLines = visibleLines.map((line, index) => {
|
|
39
|
+
let type = 'context';
|
|
40
|
+
if (line.startsWith('+')) {
|
|
41
|
+
type = 'addition';
|
|
42
|
+
} else if (line.startsWith('-')) {
|
|
43
|
+
type = 'deletion';
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type,
|
|
47
|
+
content: line,
|
|
48
|
+
lineNumber: index + 1
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
if (patchLines.length > maxLines) {
|
|
52
|
+
diffLines.push({
|
|
53
|
+
type: 'context',
|
|
54
|
+
content: `... ${patchLines.length - maxLines} more diff lines`,
|
|
55
|
+
lineNumber: diffLines.length + 1
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return diffLines;
|
|
59
|
+
}
|
|
60
|
+
function toFileChangeType(status) {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case 'added':
|
|
63
|
+
return 'added';
|
|
64
|
+
case 'removed':
|
|
65
|
+
return 'deleted';
|
|
66
|
+
default:
|
|
67
|
+
return 'modified';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function normalizeGithubFile(file, maxDiffLines = DEFAULT_DIFF_LINE_LIMIT) {
|
|
71
|
+
return {
|
|
72
|
+
path: file.filename || file.path || 'unknown',
|
|
73
|
+
type: toFileChangeType(file.status),
|
|
74
|
+
additions: file.additions || 0,
|
|
75
|
+
deletions: file.deletions || 0,
|
|
76
|
+
diff: file.diff || parseGithubDiff(file.patch, maxDiffLines)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function getGithubIdentity(commit) {
|
|
80
|
+
var _commit$commit, _commit$commit2;
|
|
81
|
+
const apiAuthor = commit.author || commit.committer || {};
|
|
82
|
+
const commitAuthor = ((_commit$commit = commit.commit) == null ? void 0 : _commit$commit.author) || ((_commit$commit2 = commit.commit) == null ? void 0 : _commit$commit2.committer) || {};
|
|
83
|
+
return {
|
|
84
|
+
login: apiAuthor.login || commitAuthor.name || commitAuthor.email || commit.sha || 'unknown',
|
|
85
|
+
name: commitAuthor.name || apiAuthor.login || commitAuthor.email || 'Unknown author',
|
|
86
|
+
avatarUrl: apiAuthor.avatar_url || ''
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function normalizeGithubCommit(commit) {
|
|
90
|
+
var _commit$commit3, _commit$commit4, _commit$commit5;
|
|
91
|
+
const identity = getGithubIdentity(commit);
|
|
92
|
+
return {
|
|
93
|
+
id: commit.sha || commit.node_id || identity.login,
|
|
94
|
+
message: ((_commit$commit3 = commit.commit) == null ? void 0 : _commit$commit3.message) || '',
|
|
95
|
+
author: {
|
|
96
|
+
name: identity.name,
|
|
97
|
+
login: identity.login,
|
|
98
|
+
avatar: identity.avatarUrl
|
|
99
|
+
},
|
|
100
|
+
date: ((_commit$commit4 = commit.commit) == null || (_commit$commit4 = _commit$commit4.author) == null ? void 0 : _commit$commit4.date) || ((_commit$commit5 = commit.commit) == null || (_commit$commit5 = _commit$commit5.committer) == null ? void 0 : _commit$commit5.date) || '',
|
|
101
|
+
hash: commit.sha || '',
|
|
102
|
+
url: commit.html_url || ''
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function buildGithubContributors(commits) {
|
|
106
|
+
const contributors = new Map();
|
|
107
|
+
commits.forEach(commit => {
|
|
108
|
+
const identity = getGithubIdentity(commit);
|
|
109
|
+
const key = identity.login || identity.name;
|
|
110
|
+
const current = contributors.get(key);
|
|
111
|
+
if (current) {
|
|
112
|
+
current.contributions += 1;
|
|
113
|
+
if (!current.avatarUrl && identity.avatarUrl) {
|
|
114
|
+
current.avatarUrl = identity.avatarUrl;
|
|
115
|
+
}
|
|
116
|
+
if (current.name === 'Unknown author' && identity.name) {
|
|
117
|
+
current.name = identity.name;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
contributors.set(key, {
|
|
122
|
+
login: identity.login,
|
|
123
|
+
name: identity.name,
|
|
124
|
+
avatarUrl: identity.avatarUrl,
|
|
125
|
+
contributions: 1
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return Array.from(contributors.values()).sort((a, b) => {
|
|
129
|
+
if (b.contributions !== a.contributions) {
|
|
130
|
+
return b.contributions - a.contributions;
|
|
131
|
+
}
|
|
132
|
+
return a.login.localeCompare(b.login);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function summarizeGithubStats(files) {
|
|
136
|
+
return files.reduce((stats, file) => ({
|
|
137
|
+
additions: stats.additions + file.additions,
|
|
138
|
+
deletions: stats.deletions + file.deletions,
|
|
139
|
+
changedFiles: stats.changedFiles + 1
|
|
140
|
+
}), {
|
|
141
|
+
additions: 0,
|
|
142
|
+
deletions: 0,
|
|
143
|
+
changedFiles: 0
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function getGithubMessageSummary(message) {
|
|
147
|
+
var _message$split$;
|
|
148
|
+
return ((_message$split$ = message.split('\n')[0]) == null ? void 0 : _message$split$.trim()) || 'Untitled commit';
|
|
149
|
+
}
|
|
150
|
+
export function getGithubShortRef(ref, length = 7) {
|
|
151
|
+
if (!ref) {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
return ref.length > length ? ref.slice(0, length) : ref;
|
|
155
|
+
}
|
package/apiHandlers/index.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
export { default as createGithubBranchHandler } from './createGithubBranchHandler';
|
|
2
|
+
export { default as createGithubCommitHandler } from './createGithubCommitHandler';
|
|
3
|
+
export { default as getBranchCompareDetails } from './getBranchCompareDetails';
|
|
4
|
+
export { default as getCommitDetails } from './getCommitDetails';
|
|
1
5
|
export { default as getPullRequestDetails } from './getPullRequestDetails';
|
|
6
|
+
export { default as getGithubContributions } from './getGithubContributions';
|
|
7
|
+
export { default as createGithubContributionsHandler } from './createGithubContributionsHandler';
|
|
8
|
+
export { default as getGithubEvents, githubEventsQuery } from './getGithubEvents';
|
|
9
|
+
export type { EventsQuery } from './getGithubEvents';
|
|
10
|
+
export { default as createGithubEventsHandler } from './createGithubEventsHandler';
|
package/apiHandlers/index.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export { default as
|
|
1
|
+
export { default as createGithubBranchHandler } from './createGithubBranchHandler';
|
|
2
|
+
export { default as createGithubCommitHandler } from './createGithubCommitHandler';
|
|
3
|
+
export { default as getBranchCompareDetails } from './getBranchCompareDetails';
|
|
4
|
+
export { default as getCommitDetails } from './getCommitDetails';
|
|
5
|
+
export { default as getPullRequestDetails } from './getPullRequestDetails';
|
|
6
|
+
export { default as getGithubContributions } from './getGithubContributions';
|
|
7
|
+
export { default as createGithubContributionsHandler } from './createGithubContributionsHandler';
|
|
8
|
+
export { default as getGithubEvents, githubEventsQuery } from './getGithubEvents';
|
|
9
|
+
export { default as createGithubEventsHandler } from './createGithubEventsHandler';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { GithubContributor } from '../types/github';
|
|
3
|
+
interface GithubContributorsListProps {
|
|
4
|
+
contributors: GithubContributor[];
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export default function GithubContributorsList({ contributors, title, }: GithubContributorsListProps): React.JSX.Element | null;
|
|
8
|
+
export {};
|