@stats-organization/github-readme-stats-core 2.1.5 → 2.2.0
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/build/api/gist.d.ts +2 -7
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +9 -41
- package/build/api/index.d.ts +3 -8
- package/build/api/index.d.ts.map +1 -1
- package/build/api/index.js +23 -45
- package/build/api/pin.d.ts +2 -7
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +9 -41
- package/build/api/top-langs.d.ts +2 -7
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +11 -55
- package/build/api/wakatime.d.ts +2 -7
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +9 -41
- package/build/cards/common-options.d.ts +13 -0
- package/build/cards/common-options.d.ts.map +1 -0
- package/build/cards/common-options.js +1 -0
- package/build/cards/gist.d.ts +11 -16
- package/build/cards/gist.d.ts.map +1 -1
- package/build/cards/gist.js +26 -31
- package/build/cards/repo.d.ts +19 -16
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +58 -60
- package/build/cards/stats.d.ts +29 -46
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +113 -181
- package/build/cards/top-languages.d.ts +81 -77
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +174 -200
- package/build/cards/wakatime.d.ts +21 -11
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +73 -111
- package/build/common/Card.d.ts +34 -13
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +73 -40
- package/build/common/I18n.d.ts +4 -2
- package/build/common/I18n.d.ts.map +1 -1
- package/build/common/color.d.ts +60 -18
- package/build/common/color.d.ts.map +1 -1
- package/build/common/color.js +102 -5
- package/build/common/config.d.ts +2 -1
- package/build/common/config.d.ts.map +1 -1
- package/build/common/config.js +16 -26
- package/build/common/constants.d.ts +2 -1
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/date.d.ts +37 -0
- package/build/common/date.d.ts.map +1 -0
- package/build/common/date.js +31 -0
- package/build/common/http.d.ts +25 -2
- package/build/common/http.d.ts.map +1 -1
- package/build/common/http.js +26 -3
- package/build/common/icons.d.ts +1 -0
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +1 -0
- package/build/common/languageColors.d.ts +11 -0
- package/build/common/languageColors.d.ts.map +1 -0
- package/build/common/languageColors.js +15 -0
- package/build/common/languageColors.json +1 -0
- package/build/common/ops.d.ts +3 -10
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +3 -15
- package/build/common/render.d.ts +43 -6
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +77 -11
- package/build/common/retryer.d.ts +3 -2
- package/build/common/retryer.d.ts.map +1 -1
- package/build/common/retryer.js +1 -0
- package/build/fetchers/gist.d.ts.map +1 -1
- package/build/fetchers/gist.js +8 -39
- package/build/fetchers/repo.d.ts +12 -11
- package/build/fetchers/repo.d.ts.map +1 -1
- package/build/fetchers/repo.js +26 -67
- package/build/fetchers/stats.d.ts +24 -17
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +302 -183
- package/build/fetchers/top-languages.d.ts +10 -15
- package/build/fetchers/top-languages.d.ts.map +1 -1
- package/build/fetchers/top-languages.js +52 -103
- package/build/fetchers/types.d.ts +24 -20
- package/build/fetchers/types.d.ts.map +1 -1
- package/build/fetchers/wakatime.d.ts +9 -5
- package/build/fetchers/wakatime.d.ts.map +1 -1
- package/build/fetchers/wakatime.js +8 -4
- package/build/graphql/contributionsDocument.d.ts +18 -0
- package/build/graphql/contributionsDocument.d.ts.map +1 -0
- package/build/graphql/contributionsDocument.js +30 -0
- package/build/graphql/generated/common.d.ts +42 -0
- package/build/graphql/generated/common.d.ts.map +1 -0
- package/build/graphql/generated/common.js +2 -0
- package/build/graphql/generated/gist.d.ts +44 -0
- package/build/graphql/generated/gist.d.ts.map +1 -0
- package/build/graphql/generated/gist.js +27 -0
- package/build/graphql/generated/repo.d.ts +67 -0
- package/build/graphql/generated/repo.d.ts.map +1 -0
- package/build/graphql/generated/repo.js +30 -0
- package/build/graphql/generated/stats.d.ts +150 -0
- package/build/graphql/generated/stats.d.ts.map +1 -0
- package/build/graphql/generated/stats.js +95 -0
- package/build/graphql/generated/top-languages.d.ts +54 -0
- package/build/graphql/generated/top-languages.d.ts.map +1 -0
- package/build/graphql/generated/top-languages.js +27 -0
- package/build/graphql/graphqlDocument.d.ts +22 -0
- package/build/graphql/graphqlDocument.d.ts.map +1 -0
- package/build/graphql/graphqlDocument.js +8 -0
- package/build/graphql/reposContributedToDocument.d.ts +27 -0
- package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
- package/build/graphql/reposContributedToDocument.js +59 -0
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/themes/index.d.ts +32 -0
- package/build/themes/index.d.ts.map +1 -1
- package/build/themes/index.js +28 -0
- package/build/translations.d.ts +17 -0
- package/build/translations.d.ts.map +1 -1
- package/build/translations.js +17 -0
- package/package.json +13 -7
- package/src/_github-username-regex.d.ts +4 -0
- package/src/api/gist.js +10 -46
- package/src/api/index.js +30 -51
- package/src/api/pin.js +10 -46
- package/src/api/top-langs.js +13 -60
- package/src/api/wakatime.js +10 -46
- package/src/cards/common-options.ts +13 -0
- package/src/cards/{gist.js → gist.ts} +37 -37
- package/src/cards/{repo.js → repo.ts} +86 -68
- package/src/cards/{stats.js → stats.ts} +168 -219
- package/src/cards/{top-languages.js → top-languages.ts} +309 -265
- package/src/cards/{wakatime.js → wakatime.ts} +136 -130
- package/src/common/Card.ts +94 -49
- package/src/common/I18n.ts +6 -2
- package/src/common/color.ts +163 -14
- package/src/common/config.ts +22 -29
- package/src/common/constants.ts +3 -1
- package/src/common/date.ts +45 -0
- package/src/common/http.ts +43 -3
- package/src/common/icons.ts +1 -0
- package/src/common/languageColors.json +1 -0
- package/src/common/languageColors.ts +19 -0
- package/src/common/ops.ts +8 -18
- package/src/common/render.ts +114 -14
- package/src/common/retryer.ts +6 -5
- package/src/fetchers/gist.ts +15 -72
- package/src/fetchers/repo.ts +114 -0
- package/src/fetchers/stats.ts +743 -0
- package/src/fetchers/top-languages.ts +135 -0
- package/src/fetchers/types.ts +28 -20
- package/src/fetchers/{wakatime.js → wakatime.ts} +19 -5
- package/src/graphql/contributionsDocument.ts +44 -0
- package/src/graphql/generated/common.ts +26 -0
- package/src/graphql/generated/gist.ts +59 -0
- package/src/graphql/generated/repo.ts +90 -0
- package/src/graphql/generated/stats.ts +194 -0
- package/src/graphql/generated/top-languages.ts +75 -0
- package/src/graphql/graphqlDocument.ts +25 -0
- package/src/graphql/queries/gist.graphql +25 -0
- package/src/graphql/queries/repo.graphql +28 -0
- package/src/graphql/queries/stats.graphql +120 -0
- package/src/graphql/queries/top-languages.graphql +34 -0
- package/src/graphql/reposContributedToDocument.ts +86 -0
- package/src/index.ts +3 -1
- package/src/themes/index.ts +33 -0
- package/src/translations.ts +17 -0
- package/src/cards/types.d.ts +0 -78
- package/src/fetchers/repo.js +0 -161
- package/src/fetchers/stats.js +0 -473
- package/src/fetchers/top-languages.js +0 -175
- package/src/themes/README.md +0 -229
package/build/fetchers/stats.js
CHANGED
|
@@ -2,166 +2,106 @@ import axios from "axios";
|
|
|
2
2
|
import githubUsernameRegex from "github-username-regex";
|
|
3
3
|
import { calculateRank } from "../calculateRank.js";
|
|
4
4
|
import { getConfig } from "../common/config.js";
|
|
5
|
+
import { getGitHubYearRange, toGitHubDateTime } from "../common/date.js";
|
|
5
6
|
import { CustomError, MissingParamError } from "../common/error.js";
|
|
6
7
|
import { wrapTextMultiline } from "../common/fmt.js";
|
|
7
|
-
import {
|
|
8
|
+
import { createGraphQLFetcher } from "../common/http.js";
|
|
8
9
|
import { logger } from "../common/log.js";
|
|
9
10
|
import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js";
|
|
10
11
|
import { retryer } from "../common/retryer.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stargazerCount
|
|
18
|
-
}
|
|
19
|
-
pageInfo {
|
|
20
|
-
hasNextPage
|
|
21
|
-
endCursor
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
25
|
-
const GRAPHQL_REPOS_QUERY = `
|
|
26
|
-
query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) {
|
|
27
|
-
user(login: $login) {
|
|
28
|
-
${GRAPHQL_REPOS_FIELD}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
`;
|
|
32
|
-
const GRAPHQL_STATS_QUERY = `
|
|
33
|
-
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {
|
|
34
|
-
user(login: $login) {
|
|
35
|
-
name
|
|
36
|
-
login
|
|
37
|
-
commits: contributionsCollection (from: $startTime) {
|
|
38
|
-
totalCommitContributions,
|
|
39
|
-
}
|
|
40
|
-
reviews: contributionsCollection {
|
|
41
|
-
totalPullRequestReviewContributions
|
|
42
|
-
}
|
|
43
|
-
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
|
44
|
-
totalCount
|
|
45
|
-
}
|
|
46
|
-
pullRequests(first: 1) {
|
|
47
|
-
totalCount
|
|
48
|
-
}
|
|
49
|
-
mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
|
|
50
|
-
totalCount
|
|
51
|
-
}
|
|
52
|
-
openIssues: issues(states: OPEN) {
|
|
53
|
-
totalCount
|
|
54
|
-
}
|
|
55
|
-
closedIssues: issues(states: CLOSED) {
|
|
56
|
-
totalCount
|
|
57
|
-
}
|
|
58
|
-
followers {
|
|
59
|
-
totalCount
|
|
60
|
-
}
|
|
61
|
-
repositoryDiscussions @include(if: $includeDiscussions) {
|
|
62
|
-
totalCount
|
|
63
|
-
}
|
|
64
|
-
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
|
|
65
|
-
totalCount
|
|
66
|
-
}
|
|
67
|
-
${GRAPHQL_REPOS_FIELD}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
`;
|
|
71
|
-
/**
|
|
72
|
-
* Stats fetcher object.
|
|
73
|
-
*
|
|
74
|
-
* @param {object & { after: string | null }} variables Fetcher variables.
|
|
75
|
-
* @param {string} token GitHub token.
|
|
76
|
-
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
|
77
|
-
*/
|
|
78
|
-
const fetcher = (variables, token) => {
|
|
79
|
-
const query = variables.after ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY;
|
|
80
|
-
return request({
|
|
81
|
-
query,
|
|
82
|
-
variables,
|
|
83
|
-
}, {
|
|
84
|
-
Authorization: `bearer ${token}`,
|
|
85
|
-
});
|
|
86
|
-
};
|
|
12
|
+
import { buildContributionsDocument } from "../graphql/contributionsDocument.js";
|
|
13
|
+
import { UserInfoDocument, UserReposDocument, } from "../graphql/generated/stats.js";
|
|
14
|
+
import { MAX_REPOSITORIES_LIMIT, buildReposContributedToDocument, } from "../graphql/reposContributedToDocument.js";
|
|
15
|
+
const fetcher = createGraphQLFetcher(UserInfoDocument, "bearer");
|
|
16
|
+
/** Fetcher for the pages after the first, which only need `repositories`. */
|
|
17
|
+
const reposFetcher = createGraphQLFetcher(UserReposDocument, "bearer");
|
|
87
18
|
/**
|
|
88
19
|
* Fetch stats information for a given username.
|
|
89
20
|
*
|
|
90
|
-
* @param
|
|
91
|
-
* @param
|
|
92
|
-
* @param
|
|
93
|
-
* @param
|
|
94
|
-
* @param
|
|
95
|
-
* @param
|
|
96
|
-
* @param
|
|
97
|
-
* @param
|
|
98
|
-
* @
|
|
21
|
+
* @param variables Fetcher variables.
|
|
22
|
+
* @param variables.username GitHub username.
|
|
23
|
+
* @param variables.includeMergedPullRequests Include merged pull requests.
|
|
24
|
+
* @param variables.includeDiscussions Include discussions.
|
|
25
|
+
* @param variables.includeDiscussionsAnswers Include discussions answers.
|
|
26
|
+
* @param variables.startTime Time to start the count of total commits.
|
|
27
|
+
* @param variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER.
|
|
28
|
+
* @param variables.includeUserRepositories Whether to include the user's own repositories in the repos contributed to.
|
|
29
|
+
* @param variables.pat PAT override or null.
|
|
30
|
+
* @returns The stats response, with every fetched page's repos merged in.
|
|
99
31
|
*
|
|
100
|
-
* @description
|
|
32
|
+
* @description Supports multi-page fetching when the `FETCH_MULTI_PAGE_STARS`
|
|
33
|
+
* env variable is `true` or a fetch limit.
|
|
101
34
|
*/
|
|
102
|
-
const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, startTime, ownerAffiliations, pat, }) => {
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
|
35
|
+
const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, startTime, ownerAffiliations, includeUserRepositories, pat, }) => {
|
|
36
|
+
// only the first request carries the stats themselves
|
|
37
|
+
let stats = await retryer(fetcher, {
|
|
38
|
+
login: username,
|
|
39
|
+
after: null,
|
|
40
|
+
includeMergedPullRequests,
|
|
41
|
+
includeDiscussions,
|
|
42
|
+
includeDiscussionsAnswers,
|
|
43
|
+
startTime,
|
|
44
|
+
ownerAffiliations,
|
|
45
|
+
includeUserRepositories,
|
|
46
|
+
}, pat);
|
|
47
|
+
if (stats.data.errors) {
|
|
48
|
+
return stats;
|
|
49
|
+
}
|
|
50
|
+
const pageLimit = getConfig().fetchMultiPageStars;
|
|
51
|
+
const extraRepoNodes = [];
|
|
52
|
+
let pageRepositories = stats.data.data.user?.repositories;
|
|
53
|
+
let previousCursor = null;
|
|
54
|
+
let fetchedPages = 1;
|
|
55
|
+
while (fetchedPages < pageLimit &&
|
|
56
|
+
// an unstarred repo on the page means the starred ones are exhausted
|
|
57
|
+
!pageRepositories?.nodes?.some((node) => node?.stargazerCount === 0) &&
|
|
58
|
+
pageRepositories?.pageInfo.hasNextPage) {
|
|
59
|
+
const after = pageRepositories.pageInfo.endCursor;
|
|
60
|
+
// a null or non-advancing cursor would refetch the same page forever
|
|
61
|
+
if (after === null || after === previousCursor) {
|
|
62
|
+
break;
|
|
133
63
|
}
|
|
134
|
-
|
|
135
|
-
|
|
64
|
+
previousCursor = after;
|
|
65
|
+
const page = await retryer(reposFetcher, { login: username, after, ownerAffiliations }, pat);
|
|
66
|
+
if (page.data.errors) {
|
|
67
|
+
return {
|
|
68
|
+
data: { ...stats.data, errors: page.data.errors },
|
|
69
|
+
statusText: page.statusText,
|
|
70
|
+
};
|
|
136
71
|
}
|
|
72
|
+
pageRepositories = page.data.data.user?.repositories;
|
|
73
|
+
extraRepoNodes.push(...(pageRepositories?.nodes ?? []));
|
|
137
74
|
fetchedPages++;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
75
|
+
}
|
|
76
|
+
if (extraRepoNodes.length > 0) {
|
|
77
|
+
// deep copy to avoid mutating the response cached by the frontend
|
|
78
|
+
stats = structuredClone({
|
|
79
|
+
data: stats.data,
|
|
80
|
+
statusText: stats.statusText,
|
|
81
|
+
});
|
|
82
|
+
stats.data.data.user?.repositories.nodes?.push(...extraRepoNodes);
|
|
145
83
|
}
|
|
146
84
|
return stats;
|
|
147
85
|
};
|
|
148
86
|
/**
|
|
149
|
-
* Fetch total
|
|
87
|
+
* Fetch total items count using the REST search API.
|
|
150
88
|
*
|
|
151
|
-
* @param
|
|
152
|
-
* @param
|
|
153
|
-
* @returns
|
|
89
|
+
* @param variables Fetcher variables.
|
|
90
|
+
* @param token GitHub token.
|
|
91
|
+
* @returns Axios response.
|
|
154
92
|
*
|
|
155
93
|
* @see https://developer.github.com/v3/search/#search-commits
|
|
156
94
|
*/
|
|
157
95
|
const fetchTotalItems = (variables, token) => {
|
|
96
|
+
const type = String(variables["type"]);
|
|
97
|
+
const filter = String(variables["filter"]);
|
|
98
|
+
const repo = variables["repo"];
|
|
99
|
+
const owner = variables["owner"];
|
|
158
100
|
return axios({
|
|
159
101
|
method: "get",
|
|
160
|
-
url: `https://api.github.com/search
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
buildSearchFilter(variables.repo, variables.owner).replaceAll(" ", "+") +
|
|
164
|
-
variables.filter,
|
|
102
|
+
url: `https://api.github.com/search/${type}?per_page=1&q=` +
|
|
103
|
+
buildSearchFilter(repo, owner).replaceAll(" ", "+") +
|
|
104
|
+
filter,
|
|
165
105
|
headers: {
|
|
166
106
|
"Content-Type": "application/json",
|
|
167
107
|
Accept: "application/vnd.github.cloak-preview",
|
|
@@ -170,13 +110,14 @@ const fetchTotalItems = (variables, token) => {
|
|
|
170
110
|
});
|
|
171
111
|
};
|
|
172
112
|
/**
|
|
173
|
-
* Fetch
|
|
113
|
+
* Fetch a total count for a given username via the REST search API.
|
|
174
114
|
*
|
|
175
|
-
* @param
|
|
176
|
-
* @returns
|
|
115
|
+
* @param username GitHub username.
|
|
116
|
+
* @returns Total count.
|
|
177
117
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
118
|
+
* The GraphQL API can't return this.
|
|
119
|
+
* @see https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467
|
|
120
|
+
* @see https://github.com/anuraghazra/github-readme-stats/pull/211
|
|
180
121
|
*/
|
|
181
122
|
const totalItemsFetcher = async (username, repo, owner, type, filter, pat) => {
|
|
182
123
|
if (!githubUsernameRegex.test(username)) {
|
|
@@ -185,27 +126,21 @@ const totalItemsFetcher = async (username, repo, owner, type, filter, pat) => {
|
|
|
185
126
|
}
|
|
186
127
|
let res;
|
|
187
128
|
try {
|
|
188
|
-
res = await retryer(fetchTotalItems, {
|
|
189
|
-
login: username,
|
|
190
|
-
repo,
|
|
191
|
-
owner,
|
|
192
|
-
type,
|
|
193
|
-
filter,
|
|
194
|
-
}, pat);
|
|
129
|
+
res = await retryer(fetchTotalItems, { login: username, repo, owner, type, filter }, pat);
|
|
195
130
|
}
|
|
196
131
|
catch (err) {
|
|
197
132
|
logger.log(err);
|
|
198
133
|
throw err;
|
|
199
134
|
}
|
|
200
135
|
const totalCount = res.data.total_count;
|
|
201
|
-
if (isNaN(totalCount)) {
|
|
136
|
+
if (totalCount === undefined || isNaN(totalCount)) {
|
|
202
137
|
logger.error("GitHub error: " + JSON.stringify(res.data));
|
|
203
138
|
throw new CustomError("Could not fetch data from GitHub REST API.", CustomError.GITHUB_REST_API_ERROR);
|
|
204
139
|
}
|
|
205
140
|
return totalCount;
|
|
206
141
|
};
|
|
207
142
|
const fetchRepoUserStats = async (username, repo, owner, include_prs_authored, include_prs_commented, include_prs_reviewed, include_issues_authored, include_issues_commented, pat) => {
|
|
208
|
-
|
|
143
|
+
const stats = {};
|
|
209
144
|
if (include_prs_authored) {
|
|
210
145
|
stats.totalPRsAuthored = await totalItemsFetcher(username, repo, owner, "issues", `author:${username}+type:pr`, pat);
|
|
211
146
|
}
|
|
@@ -223,20 +158,192 @@ const fetchRepoUserStats = async (username, repo, owner, include_prs_authored, i
|
|
|
223
158
|
}
|
|
224
159
|
return stats;
|
|
225
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Turn a GraphQL `errors` payload into the error to throw.
|
|
163
|
+
*
|
|
164
|
+
* @param errors Errors from the response envelope.
|
|
165
|
+
* @param statusText HTTP status text, used as the error type when GitHub gave a message.
|
|
166
|
+
* @param fallback Message when GitHub gave none.
|
|
167
|
+
*/
|
|
168
|
+
const graphqlError = (errors, statusText, fallback) => {
|
|
169
|
+
logger.error(errors);
|
|
170
|
+
const message = errors[0]?.message;
|
|
171
|
+
return message
|
|
172
|
+
? new CustomError(wrapTextMultiline(message, 525, 12)[0] ?? "", statusText)
|
|
173
|
+
: new CustomError(fallback, CustomError.GRAPHQL_ERROR);
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Fetch all-time contributions by building a single GraphQL query
|
|
177
|
+
* for all the given years.
|
|
178
|
+
*
|
|
179
|
+
* Whether private contributions are included depends on the user's profile settings:
|
|
180
|
+
* https://docs.github.com/en/account-and-profile/how-tos/contribution-settings/manage-visibility-settings-for-private-contributions-and-achievements#changing-the-visibility-of-your-private-contributions
|
|
181
|
+
*/
|
|
182
|
+
const fetchTotalContributions = async (username, years, pat = null) => {
|
|
183
|
+
if (years.length === 0) {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
const contributionsFetcher = createGraphQLFetcher(buildContributionsDocument(years), "bearer");
|
|
187
|
+
const contribRes = await retryer(contributionsFetcher, { login: username }, pat);
|
|
188
|
+
if (contribRes.data.errors) {
|
|
189
|
+
throw graphqlError(contribRes.data.errors, contribRes.statusText, "Something went wrong while trying to retrieve the contributions data using the GraphQL API.");
|
|
190
|
+
}
|
|
191
|
+
const user = contribRes.data.data.user;
|
|
192
|
+
if (!user) {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
let total = 0;
|
|
196
|
+
for (const year of years) {
|
|
197
|
+
const yearBlock = user[`year_${year}`];
|
|
198
|
+
if (yearBlock?.contributionCalendar.totalContributions) {
|
|
199
|
+
total += yearBlock.contributionCalendar.totalContributions;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return total;
|
|
203
|
+
};
|
|
204
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
205
|
+
/**
|
|
206
|
+
* Ranges per request.
|
|
207
|
+
* Each costs roughly `4 * MAX_REPOSITORIES_LIMIT` nodes,
|
|
208
|
+
* so an unchunked request for a heavily split account would breach GitHub's 500k node ceiling.
|
|
209
|
+
* GitHub can still reject a request of this size, in which case the chunk size is halved
|
|
210
|
+
* and the request retried; it is increased back up to this maximum after every success.
|
|
211
|
+
*/
|
|
212
|
+
const MAX_RANGES_PER_REQUEST = 400;
|
|
213
|
+
const REPOS_CONTRIBUTED_TO_ERROR = "Something went wrong while trying to retrieve the repository contributions data using the GraphQL API.";
|
|
214
|
+
/** GraphQL error type GitHub returns when a query exceeds its node/complexity budget. */
|
|
215
|
+
const RESOURCE_LIMITS_EXCEEDED = "RESOURCE_LIMITS_EXCEEDED";
|
|
216
|
+
/**
|
|
217
|
+
* Whether GitHub rejected the query for returning too many results.
|
|
218
|
+
*
|
|
219
|
+
* @see https://docs.github.com/en/graphql/overview/rate-limits-and-query-limits-for-the-graphql-api#other-resource-limits
|
|
220
|
+
*
|
|
221
|
+
* @param errors Errors from the response envelope.
|
|
222
|
+
* @returns True if the query has to be retried with fewer ranges.
|
|
223
|
+
*/
|
|
224
|
+
const isResourceLimitsExceeded = (errors) => errors.some((error) => error.type === RESOURCE_LIMITS_EXCEEDED);
|
|
225
|
+
/**
|
|
226
|
+
* Count the repositories a user contributed to across every contribution year.
|
|
227
|
+
*
|
|
228
|
+
* `repositoriesContributedTo` spans at most one year,
|
|
229
|
+
* so every year is fetched as an aliased `contributionsCollection(from, to)` in one request and the repos de-duplicated.
|
|
230
|
+
* Ranges are worked off a queue, multiple ranges at a time.
|
|
231
|
+
* A range returning `MAX_REPOSITORIES_LIMIT` results may have more,
|
|
232
|
+
* so it is halved and both halves are queued again.
|
|
233
|
+
* When GitHub rejects a request with `RESOURCE_LIMITS_EXCEEDED` or a timeout,
|
|
234
|
+
* it is retried with half as many ranges.
|
|
235
|
+
* When GitHub rejects a request with an empty response, it is retried up to 3 times.
|
|
236
|
+
*
|
|
237
|
+
* Whether private contributions are included depends on the used PAT.
|
|
238
|
+
*
|
|
239
|
+
* @param canonicalUsername Username as GitHub reports it; `nameWithOwner` uses its
|
|
240
|
+
* casing, which the raw query-string username need not match.
|
|
241
|
+
* @param years Contribution years to walk.
|
|
242
|
+
* @param includeOwnRepos Whether to count the user's own repositories.
|
|
243
|
+
* @param pat Optional PAT override.
|
|
244
|
+
* @returns Count of repositories.
|
|
245
|
+
*/
|
|
246
|
+
const fetchAllTimeReposContributedTo = async (canonicalUsername, years, includeOwnRepos, pat) => {
|
|
247
|
+
const repos = new Set();
|
|
248
|
+
const pending = years.map(getGitHubYearRange);
|
|
249
|
+
// halved when GitHub rejects a request for exceeding its resource limits, increases on success
|
|
250
|
+
let rangesPerRequest = MAX_RANGES_PER_REQUEST;
|
|
251
|
+
let emptyResponseRetries = 0;
|
|
252
|
+
const MAX_EMPTY_RESPONSE_RETRIES = 3;
|
|
253
|
+
while (pending.length > 0) {
|
|
254
|
+
const chunk = pending.slice(0, rangesPerRequest);
|
|
255
|
+
const fetcher = createGraphQLFetcher(buildReposContributedToDocument(chunk, includeOwnRepos), "bearer");
|
|
256
|
+
const res = await retryer(fetcher, { login: canonicalUsername, maxRepositories: MAX_REPOSITORIES_LIMIT }, pat);
|
|
257
|
+
if (res.status === 502 || res.status === 504) {
|
|
258
|
+
if (chunk.length > 1) {
|
|
259
|
+
rangesPerRequest = Math.floor(chunk.length / 2);
|
|
260
|
+
logger.log(`Gateway timeout (${res.status}), retrying with ${rangesPerRequest} range(s) per request...`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
|
|
264
|
+
}
|
|
265
|
+
if (res.data.errors) {
|
|
266
|
+
if (isResourceLimitsExceeded(res.data.errors) && chunk.length > 1) {
|
|
267
|
+
rangesPerRequest = Math.floor(chunk.length / 2);
|
|
268
|
+
logger.log(`Resource limits exceeded, retrying with ${rangesPerRequest} range(s) per request...`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
throw graphqlError(res.data.errors, res.statusText, REPOS_CONTRIBUTED_TO_ERROR);
|
|
272
|
+
}
|
|
273
|
+
if (typeof res.data !== "object") {
|
|
274
|
+
if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
|
|
275
|
+
emptyResponseRetries++;
|
|
276
|
+
logger.log(`Empty response from GitHub, retrying (${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})...`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
|
|
280
|
+
}
|
|
281
|
+
emptyResponseRetries = 0;
|
|
282
|
+
const user = res.data.data.user;
|
|
283
|
+
if (!user) {
|
|
284
|
+
throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
|
|
285
|
+
}
|
|
286
|
+
pending.splice(0, chunk.length);
|
|
287
|
+
rangesPerRequest = Math.min(MAX_RANGES_PER_REQUEST, Math.ceil(rangesPerRequest * 1.25));
|
|
288
|
+
for (const [index, range] of chunk.entries()) {
|
|
289
|
+
const rangeResponse = user[`range_${index}`];
|
|
290
|
+
if (!rangeResponse) {
|
|
291
|
+
throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
|
|
292
|
+
}
|
|
293
|
+
const lists = [
|
|
294
|
+
rangeResponse.commitContributionsByRepository,
|
|
295
|
+
rangeResponse.issueContributionsByRepository,
|
|
296
|
+
rangeResponse.pullRequestContributionsByRepository,
|
|
297
|
+
(rangeResponse.repositoryContributions?.nodes ?? []).filter((node) => node !== null),
|
|
298
|
+
];
|
|
299
|
+
const isSaturated = lists.some((list) => list.length >= MAX_REPOSITORIES_LIMIT);
|
|
300
|
+
const rangeDays = Math.round((range.to.getTime() - range.from.getTime()) / MS_PER_DAY);
|
|
301
|
+
if (isSaturated && rangeDays >= 2) {
|
|
302
|
+
const mid = new Date(range.from.getTime() + Math.floor(rangeDays / 2) * MS_PER_DAY);
|
|
303
|
+
logger.log(`Range ${range.from.toISOString()} - ${range.to.toISOString()} is saturated, splitting and retrying...`);
|
|
304
|
+
// GitHub only reads the date portion,
|
|
305
|
+
// so the first half ends 1 second before `mid` to keep the halves from sharing a day
|
|
306
|
+
pending.push({ from: range.from, to: new Date(mid.getTime() - 1000) }, { from: mid, to: range.to });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (isSaturated) {
|
|
310
|
+
logger.log(`Range ${range.from.toISOString()} - ${range.to.toISOString()} is saturated but cannot be split further.`);
|
|
311
|
+
}
|
|
312
|
+
for (const { repository } of lists.flat()) {
|
|
313
|
+
const name = repository.nameWithOwner;
|
|
314
|
+
if (includeOwnRepos || !name.startsWith(`${canonicalUsername}/`)) {
|
|
315
|
+
repos.add(name);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return repos.size;
|
|
321
|
+
};
|
|
226
322
|
/**
|
|
227
323
|
* Fetch stats for a given username.
|
|
228
324
|
*
|
|
229
|
-
* @param
|
|
230
|
-
* @param
|
|
231
|
-
* @param
|
|
232
|
-
* @param
|
|
233
|
-
* @param
|
|
234
|
-
* @param
|
|
235
|
-
* @param
|
|
236
|
-
* @param
|
|
237
|
-
* @
|
|
325
|
+
* @param username GitHub username.
|
|
326
|
+
* @param include_all_commits Include all commits.
|
|
327
|
+
* @param exclude_repo Repositories to exclude.
|
|
328
|
+
* @param include_merged_pull_requests Include merged pull requests.
|
|
329
|
+
* @param include_discussions Include discussions.
|
|
330
|
+
* @param include_discussions_answers Include discussions answers.
|
|
331
|
+
* @param commits_year Year to count total commits.
|
|
332
|
+
* @param repo Repositories to scope the REST search to.
|
|
333
|
+
* @param owner Owners to scope the REST search to.
|
|
334
|
+
* @param include_prs_authored Include count of PRs authored.
|
|
335
|
+
* @param include_prs_commented Include count of PRs commented.
|
|
336
|
+
* @param include_prs_reviewed Include count of PRs reviewed.
|
|
337
|
+
* @param include_issues_authored Include count of issues authored.
|
|
338
|
+
* @param include_issues_commented Include count of issues commented.
|
|
339
|
+
* @param ownerAffiliations Owner affiliations. Default: OWNER.
|
|
340
|
+
* @param include_contributions Include all-time contributions.
|
|
341
|
+
* @param include_all_time_contribs Include all-time count of repos contributed to.
|
|
342
|
+
* @param contribs_include_own_repos Include user-owned repos in contributed-to counts.
|
|
343
|
+
* @param pat Optional PAT override.
|
|
344
|
+
* @returns Stats data.
|
|
238
345
|
*/
|
|
239
|
-
const fetchStats = async (username, include_all_commits = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, commits_year, repo = [], owner = [], include_prs_authored = false, include_prs_commented = false, include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, ownerAffiliations = [], pat = null) => {
|
|
346
|
+
const fetchStats = async (username, include_all_commits = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, commits_year, repo = [], owner = [], include_prs_authored = false, include_prs_commented = false, include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, ownerAffiliations = [], include_contributions = false, include_all_time_contribs = false, contribs_include_own_repos = false, pat = null) => {
|
|
240
347
|
if (!username) {
|
|
241
348
|
throw new MissingParamError(["username"]);
|
|
242
349
|
}
|
|
@@ -252,34 +359,44 @@ const fetchStats = async (username, include_all_commits = false, exclude_repo =
|
|
|
252
359
|
totalDiscussionsStarted: 0,
|
|
253
360
|
totalDiscussionsAnswered: 0,
|
|
254
361
|
contributedTo: 0,
|
|
362
|
+
allTimeContributedTo: 0,
|
|
255
363
|
totalPRsAuthored: 0,
|
|
256
364
|
totalPRsCommented: 0,
|
|
257
365
|
totalPRsReviewed: 0,
|
|
258
366
|
totalIssuesAuthored: 0,
|
|
259
367
|
totalIssuesCommented: 0,
|
|
368
|
+
totalContributions: 0,
|
|
260
369
|
rank: { level: "C", percentile: 100 },
|
|
261
370
|
};
|
|
262
|
-
|
|
263
|
-
|
|
371
|
+
const affiliations = parseOwnerAffiliations(ownerAffiliations);
|
|
372
|
+
const res = await statsFetcher({
|
|
264
373
|
username,
|
|
265
374
|
includeMergedPullRequests: include_merged_pull_requests,
|
|
266
375
|
includeDiscussions: include_discussions,
|
|
267
376
|
includeDiscussionsAnswers: include_discussions_answers,
|
|
268
|
-
startTime: commits_year
|
|
269
|
-
|
|
270
|
-
|
|
377
|
+
startTime: commits_year === undefined
|
|
378
|
+
? undefined
|
|
379
|
+
: toGitHubDateTime(getGitHubYearRange(commits_year).from),
|
|
380
|
+
ownerAffiliations: affiliations,
|
|
381
|
+
includeUserRepositories: contribs_include_own_repos,
|
|
382
|
+
pat,
|
|
383
|
+
});
|
|
271
384
|
// Catch GraphQL errors.
|
|
272
385
|
if (res.data.errors) {
|
|
273
386
|
logger.error(res.data.errors);
|
|
274
|
-
|
|
275
|
-
|
|
387
|
+
const firstError = res.data.errors[0];
|
|
388
|
+
if (firstError?.type === "NOT_FOUND") {
|
|
389
|
+
throw new CustomError(firstError.message || "Could not fetch user.", CustomError.USER_NOT_FOUND);
|
|
276
390
|
}
|
|
277
|
-
if (
|
|
278
|
-
throw new CustomError(wrapTextMultiline(
|
|
391
|
+
if (firstError?.message) {
|
|
392
|
+
throw new CustomError(wrapTextMultiline(firstError.message, 525, 12)[0] ?? "", res.statusText);
|
|
279
393
|
}
|
|
280
394
|
throw new CustomError("Something went wrong while trying to retrieve the stats data using the GraphQL API.", CustomError.GRAPHQL_ERROR);
|
|
281
395
|
}
|
|
282
396
|
const user = res.data.data.user;
|
|
397
|
+
if (!user) {
|
|
398
|
+
throw new CustomError("Could not fetch user.", CustomError.USER_NOT_FOUND);
|
|
399
|
+
}
|
|
283
400
|
stats.name = user.name || user.login;
|
|
284
401
|
// if include_all_commits, fetch all commits using the REST API.
|
|
285
402
|
if (include_all_commits) {
|
|
@@ -288,38 +405,40 @@ const fetchStats = async (username, include_all_commits = false, exclude_repo =
|
|
|
288
405
|
else {
|
|
289
406
|
stats.totalCommits = user.commits.totalCommitContributions;
|
|
290
407
|
}
|
|
291
|
-
|
|
408
|
+
const repoUserStats = await fetchRepoUserStats(username, repo, owner, include_prs_authored, include_prs_commented, include_prs_reviewed, include_issues_authored, include_issues_commented, pat);
|
|
292
409
|
Object.assign(stats, repoUserStats);
|
|
293
410
|
stats.totalPRs = user.pullRequests.totalCount;
|
|
294
411
|
if (include_merged_pull_requests) {
|
|
295
|
-
|
|
412
|
+
const mergedCount = user.mergedPullRequests?.totalCount ?? 0;
|
|
413
|
+
stats.totalPRsMerged = mergedCount;
|
|
296
414
|
stats.mergedPRsPercentage =
|
|
297
|
-
(
|
|
298
|
-
100 || 0;
|
|
415
|
+
(mergedCount / user.pullRequests.totalCount) * 100 || 0;
|
|
299
416
|
}
|
|
300
417
|
stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
|
|
301
418
|
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
|
|
302
419
|
if (include_discussions) {
|
|
303
|
-
stats.totalDiscussionsStarted = user.repositoryDiscussions
|
|
420
|
+
stats.totalDiscussionsStarted = user.repositoryDiscussions?.totalCount ?? 0;
|
|
304
421
|
}
|
|
305
422
|
if (include_discussions_answers) {
|
|
306
423
|
stats.totalDiscussionsAnswered =
|
|
307
|
-
user.repositoryDiscussionComments
|
|
424
|
+
user.repositoryDiscussionComments?.totalCount ?? 0;
|
|
308
425
|
}
|
|
309
426
|
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
|
427
|
+
if (include_contributions) {
|
|
428
|
+
stats.totalContributions = await fetchTotalContributions(username, user.contributionsCollection.contributionYears, pat);
|
|
429
|
+
}
|
|
430
|
+
if (include_all_time_contribs) {
|
|
431
|
+
stats.allTimeContributedTo = await fetchAllTimeReposContributedTo(user.login, user.contributionsCollection.contributionYears, contribs_include_own_repos, pat);
|
|
432
|
+
}
|
|
310
433
|
// Retrieve stars while filtering out repositories to be hidden.
|
|
311
434
|
const allExcludedRepos = [
|
|
312
435
|
...exclude_repo,
|
|
313
436
|
...getConfig().excludeRepositories,
|
|
314
437
|
];
|
|
315
|
-
|
|
316
|
-
stats.totalStars = user.repositories.nodes
|
|
317
|
-
.filter((data) =>
|
|
318
|
-
|
|
319
|
-
})
|
|
320
|
-
.reduce((prev, curr) => {
|
|
321
|
-
return prev + curr.stargazerCount;
|
|
322
|
-
}, 0);
|
|
438
|
+
const repoToHide = new Set(allExcludedRepos);
|
|
439
|
+
stats.totalStars = (user.repositories.nodes ?? [])
|
|
440
|
+
.filter((data) => !!data && !repoToHide.has(data.name))
|
|
441
|
+
.reduce((prev, curr) => prev + (curr?.stargazerCount ?? 0), 0);
|
|
323
442
|
stats.rank = calculateRank({
|
|
324
443
|
all_commits: include_all_commits,
|
|
325
444
|
commits: stats.totalCommits,
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
* Top languages data.
|
|
3
|
-
*/
|
|
4
|
-
export type TopLangData = any;
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {import("./types").TopLangData} TopLangData Top languages data.
|
|
7
|
-
*/
|
|
1
|
+
import type { TopLangData } from "./types.js";
|
|
8
2
|
/**
|
|
9
3
|
* Fetch top languages for a given username.
|
|
10
4
|
*
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
13
|
-
* @param
|
|
14
|
-
* @param
|
|
15
|
-
* @param
|
|
16
|
-
* @param
|
|
17
|
-
* @returns
|
|
5
|
+
* @param username GitHub username.
|
|
6
|
+
* @param exclude_repo List of repositories to exclude. Default: [].
|
|
7
|
+
* @param size_weight Weightage to be given to size.
|
|
8
|
+
* @param count_weight Weightage to be given to count.
|
|
9
|
+
* @param ownerAffiliations The owner affiliations to filter by. Default: OWNER.
|
|
10
|
+
* @param pat Optional PAT override.
|
|
11
|
+
* @returns Top languages data.
|
|
18
12
|
*/
|
|
19
|
-
|
|
13
|
+
declare const fetchTopLanguages: (username: string, exclude_repo?: Array<string>, size_weight?: number, count_weight?: number, ownerAffiliations?: Array<string>, pat?: string | null) => Promise<TopLangData>;
|
|
14
|
+
export { fetchTopLanguages };
|
|
20
15
|
//# sourceMappingURL=top-languages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.
|
|
1
|
+
{"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpD;;;;;;;;;;GAUG;AACH,QAAA,MAAM,iBAAiB,GACrB,UAAU,MAAM,EAChB,eAAc,KAAK,CAAC,MAAM,CAAM,EAChC,oBAAe,EACf,qBAAgB,EAChB,oBAAmB,KAAK,CAAC,MAAM,CAAM,EACrC,MAAK,MAAM,GAAG,IAAW,KACxB,OAAO,CAAC,WAAW,CAiGrB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|