@dytsou/github-readme-stats 1.0.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/LICENSE +21 -0
- package/api/gist.js +98 -0
- package/api/index.js +146 -0
- package/api/pin.js +114 -0
- package/api/status/pat-info.js +193 -0
- package/api/status/up.js +129 -0
- package/api/top-langs.js +163 -0
- package/api/wakatime.js +129 -0
- package/package.json +102 -0
- package/readme.md +412 -0
- package/src/calculateRank.js +87 -0
- package/src/cards/gist.js +151 -0
- package/src/cards/index.js +4 -0
- package/src/cards/repo.js +199 -0
- package/src/cards/stats.js +607 -0
- package/src/cards/top-languages.js +968 -0
- package/src/cards/types.d.ts +67 -0
- package/src/cards/wakatime.js +482 -0
- package/src/common/Card.js +294 -0
- package/src/common/I18n.js +41 -0
- package/src/common/access.js +69 -0
- package/src/common/api-utils.js +221 -0
- package/src/common/blacklist.js +10 -0
- package/src/common/cache.js +153 -0
- package/src/common/color.js +194 -0
- package/src/common/envs.js +15 -0
- package/src/common/error.js +84 -0
- package/src/common/fmt.js +90 -0
- package/src/common/html.js +43 -0
- package/src/common/http.js +24 -0
- package/src/common/icons.js +63 -0
- package/src/common/index.js +13 -0
- package/src/common/languageColors.json +651 -0
- package/src/common/log.js +14 -0
- package/src/common/ops.js +124 -0
- package/src/common/render.js +261 -0
- package/src/common/retryer.js +97 -0
- package/src/common/worker-adapter.js +148 -0
- package/src/common/worker-env.js +48 -0
- package/src/fetchers/gist.js +114 -0
- package/src/fetchers/repo.js +118 -0
- package/src/fetchers/stats.js +350 -0
- package/src/fetchers/top-languages.js +192 -0
- package/src/fetchers/types.d.ts +118 -0
- package/src/fetchers/wakatime.js +109 -0
- package/src/index.js +2 -0
- package/src/translations.js +1105 -0
- package/src/worker.ts +116 -0
- package/themes/README.md +229 -0
- package/themes/index.js +467 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { retryer } from "../common/retryer.js";
|
|
4
|
+
import { MissingParamError } from "../common/error.js";
|
|
5
|
+
import { request } from "../common/http.js";
|
|
6
|
+
|
|
7
|
+
const QUERY = `
|
|
8
|
+
query gistInfo($gistName: String!) {
|
|
9
|
+
viewer {
|
|
10
|
+
gist(name: $gistName) {
|
|
11
|
+
description
|
|
12
|
+
owner {
|
|
13
|
+
login
|
|
14
|
+
}
|
|
15
|
+
stargazerCount
|
|
16
|
+
forks {
|
|
17
|
+
totalCount
|
|
18
|
+
}
|
|
19
|
+
files {
|
|
20
|
+
name
|
|
21
|
+
language {
|
|
22
|
+
name
|
|
23
|
+
}
|
|
24
|
+
size
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gist data fetcher.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} variables Fetcher variables.
|
|
35
|
+
* @param {string} token GitHub token.
|
|
36
|
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
|
37
|
+
*/
|
|
38
|
+
const fetcher = async (variables, token) => {
|
|
39
|
+
return await request(
|
|
40
|
+
{ query: QUERY, variables },
|
|
41
|
+
{ Authorization: `token ${token}` },
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {{ name: string; language: { name: string; }, size: number }} GistFile Gist file.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function calculates the primary language of a gist by files size.
|
|
51
|
+
*
|
|
52
|
+
* @param {GistFile[]} files Files.
|
|
53
|
+
* @returns {string} Primary language.
|
|
54
|
+
*/
|
|
55
|
+
const calculatePrimaryLanguage = (files) => {
|
|
56
|
+
/** @type {Record<string, number>} */
|
|
57
|
+
const languages = {};
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (file.language) {
|
|
61
|
+
if (languages[file.language.name]) {
|
|
62
|
+
languages[file.language.name] += file.size;
|
|
63
|
+
} else {
|
|
64
|
+
languages[file.language.name] = file.size;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let primaryLanguage = Object.keys(languages)[0];
|
|
70
|
+
for (const language in languages) {
|
|
71
|
+
if (languages[language] > languages[primaryLanguage]) {
|
|
72
|
+
primaryLanguage = language;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return primaryLanguage;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {import('./types').GistData} GistData Gist data.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fetch GitHub gist information by given username and ID.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} id GitHub gist ID.
|
|
87
|
+
* @returns {Promise<GistData>} Gist data.
|
|
88
|
+
*/
|
|
89
|
+
const fetchGist = async (id) => {
|
|
90
|
+
if (!id) {
|
|
91
|
+
throw new MissingParamError(["id"], "/api/gist?id=GIST_ID");
|
|
92
|
+
}
|
|
93
|
+
const res = await retryer(fetcher, { gistName: id });
|
|
94
|
+
if (res.data.errors) {
|
|
95
|
+
throw new Error(res.data.errors[0].message);
|
|
96
|
+
}
|
|
97
|
+
if (!res.data.data.viewer.gist) {
|
|
98
|
+
throw new Error("Gist not found");
|
|
99
|
+
}
|
|
100
|
+
const data = res.data.data.viewer.gist;
|
|
101
|
+
return {
|
|
102
|
+
name: data.files[Object.keys(data.files)[0]].name,
|
|
103
|
+
nameWithOwner: `${data.owner.login}/${
|
|
104
|
+
data.files[Object.keys(data.files)[0]].name
|
|
105
|
+
}`,
|
|
106
|
+
description: data.description,
|
|
107
|
+
language: calculatePrimaryLanguage(data.files),
|
|
108
|
+
starsCount: data.stargazerCount,
|
|
109
|
+
forksCount: data.forks.totalCount,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export { fetchGist };
|
|
114
|
+
export default fetchGist;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { MissingParamError } from "../common/error.js";
|
|
4
|
+
import { request } from "../common/http.js";
|
|
5
|
+
import { retryer } from "../common/retryer.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Repo data fetcher.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} variables Fetcher variables.
|
|
11
|
+
* @param {string} token GitHub token.
|
|
12
|
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
|
13
|
+
*/
|
|
14
|
+
const fetcher = (variables, token) => {
|
|
15
|
+
return request(
|
|
16
|
+
{
|
|
17
|
+
query: `
|
|
18
|
+
fragment RepoInfo on Repository {
|
|
19
|
+
name
|
|
20
|
+
nameWithOwner
|
|
21
|
+
isPrivate
|
|
22
|
+
isArchived
|
|
23
|
+
isTemplate
|
|
24
|
+
stargazers {
|
|
25
|
+
totalCount
|
|
26
|
+
}
|
|
27
|
+
description
|
|
28
|
+
primaryLanguage {
|
|
29
|
+
color
|
|
30
|
+
id
|
|
31
|
+
name
|
|
32
|
+
}
|
|
33
|
+
forkCount
|
|
34
|
+
}
|
|
35
|
+
query getRepo($login: String!, $repo: String!) {
|
|
36
|
+
user(login: $login) {
|
|
37
|
+
repository(name: $repo) {
|
|
38
|
+
...RepoInfo
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
organization(login: $login) {
|
|
42
|
+
repository(name: $repo) {
|
|
43
|
+
...RepoInfo
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
variables,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
Authorization: `token ${token}`,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {import("./types").RepositoryData} RepositoryData Repository data.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetch repository data.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} username GitHub username.
|
|
66
|
+
* @param {string} reponame GitHub repository name.
|
|
67
|
+
* @returns {Promise<RepositoryData>} Repository data.
|
|
68
|
+
*/
|
|
69
|
+
const fetchRepo = async (username, reponame) => {
|
|
70
|
+
if (!username && !reponame) {
|
|
71
|
+
throw new MissingParamError(["username", "repo"], urlExample);
|
|
72
|
+
}
|
|
73
|
+
if (!username) {
|
|
74
|
+
throw new MissingParamError(["username"], urlExample);
|
|
75
|
+
}
|
|
76
|
+
if (!reponame) {
|
|
77
|
+
throw new MissingParamError(["repo"], urlExample);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let res = await retryer(fetcher, { login: username, repo: reponame });
|
|
81
|
+
|
|
82
|
+
const data = res.data.data;
|
|
83
|
+
|
|
84
|
+
if (!data.user && !data.organization) {
|
|
85
|
+
throw new Error("Not found");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isUser = data.organization === null && data.user;
|
|
89
|
+
const isOrg = data.user === null && data.organization;
|
|
90
|
+
|
|
91
|
+
if (isUser) {
|
|
92
|
+
if (!data.user.repository || data.user.repository.isPrivate) {
|
|
93
|
+
throw new Error("User Repository Not found");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...data.user.repository,
|
|
97
|
+
starCount: data.user.repository.stargazers.totalCount,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isOrg) {
|
|
102
|
+
if (
|
|
103
|
+
!data.organization.repository ||
|
|
104
|
+
data.organization.repository.isPrivate
|
|
105
|
+
) {
|
|
106
|
+
throw new Error("Organization Repository Not found");
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...data.organization.repository,
|
|
110
|
+
starCount: data.organization.repository.stargazers.totalCount,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error("Unexpected behavior");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { fetchRepo };
|
|
118
|
+
export default fetchRepo;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import githubUsernameRegex from "github-username-regex";
|
|
6
|
+
import { calculateRank } from "../calculateRank.js";
|
|
7
|
+
import { retryer } from "../common/retryer.js";
|
|
8
|
+
import { logger } from "../common/log.js";
|
|
9
|
+
import { excludeRepositories } from "../common/envs.js";
|
|
10
|
+
import { CustomError, MissingParamError } from "../common/error.js";
|
|
11
|
+
import { wrapTextMultiline } from "../common/fmt.js";
|
|
12
|
+
import { request } from "../common/http.js";
|
|
13
|
+
import { isCloudflareWorkers } from "../common/worker-env.js";
|
|
14
|
+
|
|
15
|
+
// Only load dotenv if not in Cloudflare Workers (where env vars come from wrangler.toml)
|
|
16
|
+
// Wrap in try-catch to handle cases where file system is not available (e.g., Cloudflare Workers)
|
|
17
|
+
try {
|
|
18
|
+
if (!isCloudflareWorkers()) {
|
|
19
|
+
dotenv.config();
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Silently fail in environments without file system (Cloudflare Workers)
|
|
23
|
+
// Environment variables will be provided via Cloudflare Workers env object
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// GraphQL queries.
|
|
27
|
+
const GRAPHQL_REPOS_FIELD = `
|
|
28
|
+
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
|
29
|
+
totalCount
|
|
30
|
+
nodes {
|
|
31
|
+
name
|
|
32
|
+
stargazers {
|
|
33
|
+
totalCount
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
pageInfo {
|
|
37
|
+
hasNextPage
|
|
38
|
+
endCursor
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const GRAPHQL_REPOS_QUERY = `
|
|
44
|
+
query userInfo($login: String!, $after: String) {
|
|
45
|
+
user(login: $login) {
|
|
46
|
+
${GRAPHQL_REPOS_FIELD}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const GRAPHQL_STATS_QUERY = `
|
|
52
|
+
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null) {
|
|
53
|
+
user(login: $login) {
|
|
54
|
+
name
|
|
55
|
+
login
|
|
56
|
+
commits: contributionsCollection (from: $startTime) {
|
|
57
|
+
totalCommitContributions,
|
|
58
|
+
}
|
|
59
|
+
reviews: contributionsCollection {
|
|
60
|
+
totalPullRequestReviewContributions
|
|
61
|
+
}
|
|
62
|
+
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
|
63
|
+
totalCount
|
|
64
|
+
}
|
|
65
|
+
pullRequests(first: 1) {
|
|
66
|
+
totalCount
|
|
67
|
+
}
|
|
68
|
+
mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
|
|
69
|
+
totalCount
|
|
70
|
+
}
|
|
71
|
+
openIssues: issues(states: OPEN) {
|
|
72
|
+
totalCount
|
|
73
|
+
}
|
|
74
|
+
closedIssues: issues(states: CLOSED) {
|
|
75
|
+
totalCount
|
|
76
|
+
}
|
|
77
|
+
followers {
|
|
78
|
+
totalCount
|
|
79
|
+
}
|
|
80
|
+
repositoryDiscussions @include(if: $includeDiscussions) {
|
|
81
|
+
totalCount
|
|
82
|
+
}
|
|
83
|
+
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
|
|
84
|
+
totalCount
|
|
85
|
+
}
|
|
86
|
+
${GRAPHQL_REPOS_FIELD}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stats fetcher object.
|
|
93
|
+
*
|
|
94
|
+
* @param {object & { after: string | null }} variables Fetcher variables.
|
|
95
|
+
* @param {string} token GitHub token.
|
|
96
|
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
|
97
|
+
*/
|
|
98
|
+
const fetcher = (variables, token) => {
|
|
99
|
+
const query = variables.after ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY;
|
|
100
|
+
return request(
|
|
101
|
+
{
|
|
102
|
+
query,
|
|
103
|
+
variables,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
Authorization: `bearer ${token}`,
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch stats information for a given username.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} variables Fetcher variables.
|
|
115
|
+
* @param {string} variables.username GitHub username.
|
|
116
|
+
* @param {boolean} variables.includeMergedPullRequests Include merged pull requests.
|
|
117
|
+
* @param {boolean} variables.includeDiscussions Include discussions.
|
|
118
|
+
* @param {boolean} variables.includeDiscussionsAnswers Include discussions answers.
|
|
119
|
+
* @param {string|undefined} variables.startTime Time to start the count of total commits.
|
|
120
|
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
|
121
|
+
*
|
|
122
|
+
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
|
|
123
|
+
*/
|
|
124
|
+
const statsFetcher = async ({
|
|
125
|
+
username,
|
|
126
|
+
includeMergedPullRequests,
|
|
127
|
+
includeDiscussions,
|
|
128
|
+
includeDiscussionsAnswers,
|
|
129
|
+
startTime,
|
|
130
|
+
}) => {
|
|
131
|
+
let stats;
|
|
132
|
+
let hasNextPage = true;
|
|
133
|
+
let endCursor = null;
|
|
134
|
+
while (hasNextPage) {
|
|
135
|
+
const variables = {
|
|
136
|
+
login: username,
|
|
137
|
+
first: 100,
|
|
138
|
+
after: endCursor,
|
|
139
|
+
includeMergedPullRequests,
|
|
140
|
+
includeDiscussions,
|
|
141
|
+
includeDiscussionsAnswers,
|
|
142
|
+
startTime,
|
|
143
|
+
};
|
|
144
|
+
let res = await retryer(fetcher, variables);
|
|
145
|
+
if (res.data.errors) {
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Store stats data.
|
|
150
|
+
const repoNodes = res.data.data.user.repositories.nodes;
|
|
151
|
+
if (stats) {
|
|
152
|
+
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
|
153
|
+
} else {
|
|
154
|
+
stats = res;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Disable multi page fetching on public instances due to rate limits.
|
|
158
|
+
const repoNodesWithStars = repoNodes.filter(
|
|
159
|
+
(node) => node.stargazers.totalCount !== 0,
|
|
160
|
+
);
|
|
161
|
+
hasNextPage =
|
|
162
|
+
process.env.FETCH_MULTI_PAGE_STARS === "true" &&
|
|
163
|
+
repoNodes.length === repoNodesWithStars.length &&
|
|
164
|
+
res.data.data.user.repositories.pageInfo.hasNextPage;
|
|
165
|
+
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return stats;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fetch total commits using the REST API.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} variables Fetcher variables.
|
|
175
|
+
* @param {string} token GitHub token.
|
|
176
|
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
|
177
|
+
*
|
|
178
|
+
* @see https://developer.github.com/v3/search/#search-commits
|
|
179
|
+
*/
|
|
180
|
+
const fetchTotalCommits = (variables, token) => {
|
|
181
|
+
return axios({
|
|
182
|
+
method: "get",
|
|
183
|
+
url: `https://api.github.com/search/commits?q=author:${variables.login}`,
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
Accept: "application/vnd.github.cloak-preview",
|
|
187
|
+
Authorization: `token ${token}`,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fetch all the commits for all the repositories of a given username.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} username GitHub username.
|
|
196
|
+
* @returns {Promise<number>} Total commits.
|
|
197
|
+
*
|
|
198
|
+
* @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
|
|
199
|
+
* #92#issuecomment-661026467 and #211 for more information.
|
|
200
|
+
*/
|
|
201
|
+
const totalCommitsFetcher = async (username) => {
|
|
202
|
+
if (!githubUsernameRegex.test(username)) {
|
|
203
|
+
logger.log("Invalid username provided.");
|
|
204
|
+
throw new Error("Invalid username provided.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let res;
|
|
208
|
+
try {
|
|
209
|
+
res = await retryer(fetchTotalCommits, { login: username });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger.log(err);
|
|
212
|
+
throw new Error(err);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const totalCount = res.data.total_count;
|
|
216
|
+
if (!totalCount || isNaN(totalCount)) {
|
|
217
|
+
throw new CustomError(
|
|
218
|
+
"Could not fetch total commits.",
|
|
219
|
+
CustomError.GITHUB_REST_API_ERROR,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return totalCount;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Fetch stats for a given username.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} username GitHub username.
|
|
229
|
+
* @param {boolean} include_all_commits Include all commits.
|
|
230
|
+
* @param {string[]} exclude_repo Repositories to exclude.
|
|
231
|
+
* @param {boolean} include_merged_pull_requests Include merged pull requests.
|
|
232
|
+
* @param {boolean} include_discussions Include discussions.
|
|
233
|
+
* @param {boolean} include_discussions_answers Include discussions answers.
|
|
234
|
+
* @param {number|undefined} commits_year Year to count total commits
|
|
235
|
+
* @returns {Promise<import("./types").StatsData>} Stats data.
|
|
236
|
+
*/
|
|
237
|
+
const fetchStats = async (
|
|
238
|
+
username,
|
|
239
|
+
include_all_commits = false,
|
|
240
|
+
exclude_repo = [],
|
|
241
|
+
include_merged_pull_requests = false,
|
|
242
|
+
include_discussions = false,
|
|
243
|
+
include_discussions_answers = false,
|
|
244
|
+
commits_year,
|
|
245
|
+
) => {
|
|
246
|
+
if (!username) {
|
|
247
|
+
throw new MissingParamError(["username"]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stats = {
|
|
251
|
+
name: "",
|
|
252
|
+
totalPRs: 0,
|
|
253
|
+
totalPRsMerged: 0,
|
|
254
|
+
mergedPRsPercentage: 0,
|
|
255
|
+
totalReviews: 0,
|
|
256
|
+
totalCommits: 0,
|
|
257
|
+
totalIssues: 0,
|
|
258
|
+
totalStars: 0,
|
|
259
|
+
totalDiscussionsStarted: 0,
|
|
260
|
+
totalDiscussionsAnswered: 0,
|
|
261
|
+
contributedTo: 0,
|
|
262
|
+
rank: { level: "C", percentile: 100 },
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
let res = await statsFetcher({
|
|
266
|
+
username,
|
|
267
|
+
includeMergedPullRequests: include_merged_pull_requests,
|
|
268
|
+
includeDiscussions: include_discussions,
|
|
269
|
+
includeDiscussionsAnswers: include_discussions_answers,
|
|
270
|
+
startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Catch GraphQL errors.
|
|
274
|
+
if (res.data.errors) {
|
|
275
|
+
logger.error(res.data.errors);
|
|
276
|
+
if (res.data.errors[0].type === "NOT_FOUND") {
|
|
277
|
+
throw new CustomError(
|
|
278
|
+
res.data.errors[0].message || "Could not fetch user.",
|
|
279
|
+
CustomError.USER_NOT_FOUND,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (res.data.errors[0].message) {
|
|
283
|
+
throw new CustomError(
|
|
284
|
+
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
|
|
285
|
+
res.statusText,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
throw new CustomError(
|
|
289
|
+
"Something went wrong while trying to retrieve the stats data using the GraphQL API.",
|
|
290
|
+
CustomError.GRAPHQL_ERROR,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const user = res.data.data.user;
|
|
295
|
+
|
|
296
|
+
stats.name = user.name || user.login;
|
|
297
|
+
|
|
298
|
+
// if include_all_commits, fetch all commits using the REST API.
|
|
299
|
+
if (include_all_commits) {
|
|
300
|
+
stats.totalCommits = await totalCommitsFetcher(username);
|
|
301
|
+
} else {
|
|
302
|
+
stats.totalCommits = user.commits.totalCommitContributions;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
stats.totalPRs = user.pullRequests.totalCount;
|
|
306
|
+
if (include_merged_pull_requests) {
|
|
307
|
+
stats.totalPRsMerged = user.mergedPullRequests.totalCount;
|
|
308
|
+
stats.mergedPRsPercentage =
|
|
309
|
+
(user.mergedPullRequests.totalCount / user.pullRequests.totalCount) *
|
|
310
|
+
100 || 0;
|
|
311
|
+
}
|
|
312
|
+
stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
|
|
313
|
+
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
|
|
314
|
+
if (include_discussions) {
|
|
315
|
+
stats.totalDiscussionsStarted = user.repositoryDiscussions.totalCount;
|
|
316
|
+
}
|
|
317
|
+
if (include_discussions_answers) {
|
|
318
|
+
stats.totalDiscussionsAnswered =
|
|
319
|
+
user.repositoryDiscussionComments.totalCount;
|
|
320
|
+
}
|
|
321
|
+
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
|
322
|
+
|
|
323
|
+
// Retrieve stars while filtering out repositories to be hidden.
|
|
324
|
+
const allExcludedRepos = [...exclude_repo, ...excludeRepositories];
|
|
325
|
+
let repoToHide = new Set(allExcludedRepos);
|
|
326
|
+
|
|
327
|
+
stats.totalStars = user.repositories.nodes
|
|
328
|
+
.filter((data) => {
|
|
329
|
+
return !repoToHide.has(data.name);
|
|
330
|
+
})
|
|
331
|
+
.reduce((prev, curr) => {
|
|
332
|
+
return prev + curr.stargazers.totalCount;
|
|
333
|
+
}, 0);
|
|
334
|
+
|
|
335
|
+
stats.rank = calculateRank({
|
|
336
|
+
all_commits: include_all_commits,
|
|
337
|
+
commits: stats.totalCommits,
|
|
338
|
+
prs: stats.totalPRs,
|
|
339
|
+
reviews: stats.totalReviews,
|
|
340
|
+
issues: stats.totalIssues,
|
|
341
|
+
repos: user.repositories.totalCount,
|
|
342
|
+
stars: stats.totalStars,
|
|
343
|
+
followers: user.followers.totalCount,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return stats;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export { fetchStats };
|
|
350
|
+
export default fetchStats;
|