feat: implement commits year option and display appropriate year label (#2107)

* add custom year

* edit fetcher

* add tests

* add documentation

* Update vercel.json

* Update fetchStats.test.js

* Update readme.md

* Update readme.md

Co-authored-by: Rick Staa <rick.staa@outlook.com>

* test: fix tests

* style: format code

* fix: fix commit stats time frame description #1053

This commit makes sure the last year label is shown when
`include_all_commits` or `year` is not provided. See #1053 for more
information.

* resolved merge conflict

* Update stats-fetcher.js

* resolve comments

* Update src/cards/types.d.ts

Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>

* Update src/fetchers/stats-fetcher.js

Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>

* graphQL year only to commit

* change argument name to commits_year

* Update readme.md

Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>

* Update readme.md

Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>

* Update readme.md

Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>

* wrap label into function

* review

* more review

* fix test

* Update readme.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* docs

---------

Co-authored-by: Touch Sungkawichai <talay@10ninox.com>
Co-authored-by: Rick Staa <rick.staa@outlook.com>
Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
my99N
2025-10-25 11:46:42 +02:00
committed by martin-mfg
co-authored by Alexandr Garbuzov Copilot Touch Sungkawichai Rick Staa
parent c7dfbecc77
commit 08517568d9
9 changed files with 118 additions and 18 deletions
+3
View File
@@ -24,6 +24,7 @@ export default async (req, res) => {
hide_rank,
show_icons,
include_all_commits,
commits_year,
line_height,
title_color,
ring_color,
@@ -130,6 +131,7 @@ export default async (req, res) => {
showStats.includes("prs_merged_percentage"),
showStats.includes("discussions_started"),
showStats.includes("discussions_answered"),
parseInt(commits_year, 10),
repository,
repoOwner,
showStats.includes("prs_authored"),
@@ -165,6 +167,7 @@ export default async (req, res) => {
card_width: parseInt(card_width, 10),
hide_rank: parseBoolean(hide_rank),
include_all_commits: parseBoolean(include_all_commits),
commits_year: parseInt(commits_year, 10),
line_height,
title_color,
ring_color,
+9
View File
@@ -53,6 +53,7 @@
- [Showing additional individual stats](#showing-additional-individual-stats)
- [Showing icons](#showing-icons)
- [Filtering by repository and owner](#filtering-by-repository-and-owner)
- [Showing commits count for specified year](#showing-commits-count-for-specified-year)
- [Themes](#themes)
- [Customization](#customization)
- [GitHub Extra Pins](#github-extra-pins)
@@ -157,6 +158,13 @@ To compute your stats for only a specific repository, you can pass a query param
(Some of these mentioned items are similar to other items which are included by default, e.g. `issues_authored` is similar to `issues`. The difference is how these values are fetched - [via GraphQL or via REST API](https://github.com/anuraghazra/github-readme-stats/discussions/1770#number-of-commits-is-incorrect). The default items use GraphQL, but filtering by repository works better via REST API.)
Alternatively, you can use the `role` parameter to specify a comma-separated list of [roles](https://docs.github.com/en/graphql/reference/enums#repositoryaffiliation). The stats will include all repositories in which the user has the specified role. By default, only repositories where the user is OWNER will be included, but you could e.g. set `&role=OWNER,ORGANIZATION_MEMBER,COLLABORATOR`. The `role` parameter is supported by all items except the following: `commits` (when used with `&include_all_commits=true`), `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` and `issues_commented`.
### Showing commits count for specified year
You can specify a year and fetch only the commits that were made in that year by passing `&commits_year=YYYY` to the parameter.
```md
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&commits_year=2020)
```
### Themes
@@ -374,6 +382,7 @@ If we don't support your language, please consider contributing! You can find mo
| `ring_color` | Color of the rank circle. | string (hex color) | `2f80ed` |
| `number_format` | Switches between two available formats for displaying the card values: `short` (i.e. `6.6k`) and `long` (i.e. `6626`). | enum | `short` |
| `show` | Shows [additional items](#showing-additional-individual-stats) on stats card (i.e. `reviews`, `discussions_started`, `discussions_answered`, `prs_merged` or `prs_merged_percentage`. And the following, which support the `repo` and `owner` filters: `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` or `issues_commented`). | string (comma-separated values) | `null` |
| `commits_year` | Filters and counts only commits made in the specified year | integer _(YYYY)_ | `<current year> (one year to date)`.
> [!NOTE]\
> When hide\_rank=`true`, the minimum card width is 270 px + the title length and padding.
+31 -8
View File
@@ -11,7 +11,7 @@ import {
measureText,
buildSearchFilter,
} from "../common/utils.js";
import { statCardLocales } from "../translations.js";
import { statCardLocales, wakatimeCardLocales } from "../translations.js";
const CARD_MIN_WIDTH = 287;
const CARD_DEFAULT_WIDTH = 287;
@@ -198,6 +198,21 @@ const getStyles = ({
`;
};
/**
* Return the label for commits according to the selected options
*
* @param {boolean} include_all_commits Option to include all years
* @param {number|undefined} commits_year Option to include only selected year
* @param {I18n} i18n The I18n instance.
* @returns {string} The label corresponding to the options.
*/
const getTotalCommitsYearLabel = (include_all_commits, commits_year, i18n) =>
include_all_commits
? ""
: commits_year
? ` (${commits_year})`
: ` (${i18n.t("wakatimecard.lastyear")})`;
/**
* @typedef {import('../fetchers/types').StatsData} StatsData
* @typedef {import('./types').StatCardOptions} StatCardOptions
@@ -244,6 +259,7 @@ const renderStatsCard = (
card_width,
hide_rank = false,
include_all_commits = false,
commits_year,
line_height = 25,
title_color,
ring_color,
@@ -279,7 +295,10 @@ const renderStatsCard = (
const apostrophe = /s$/i.test(name.trim()) ? "" : "s";
const i18n = new I18n({
locale,
translations: statCardLocales({ name, apostrophe }),
translations: {
...statCardLocales({ name, apostrophe }),
...wakatimeCardLocales,
},
});
// Meta data for creating text nodes with createTextNode function
@@ -293,9 +312,11 @@ const renderStatsCard = (
};
STATS.commits = {
icon: icons.commits,
label: `${i18n.t("statcard.commits")}${
include_all_commits ? "" : ` (${new Date().getFullYear()})`
}`,
label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel(
include_all_commits,
commits_year,
i18n,
)}`,
value: totalCommits,
id: "commits",
};
@@ -588,9 +609,11 @@ const renderStatsCard = (
.filter((key) => !hide.includes(key))
.map((key) => {
if (key === "commits") {
return `${i18n.t("statcard.commits")} ${
include_all_commits ? "" : `in ${new Date().getFullYear()}`
} : ${STATS[key].value}`;
return `${i18n.t("statcard.commits")} ${getTotalCommitsYearLabel(
include_all_commits,
commits_year,
i18n,
)} : ${STATS[key].value}`;
}
return `${STATS[key].label}: ${STATS[key].value}`;
})
+1
View File
@@ -20,6 +20,7 @@ export type StatCardOptions = CommonOptions & {
card_width: number;
hide_rank: boolean;
include_all_commits: boolean;
commits_year: number;
line_height: number | string;
custom_title: string;
disable_animations: boolean;
+13 -5
View File
@@ -42,12 +42,14 @@ const GRAPHQL_REPOS_QUERY = `
`;
const GRAPHQL_STATS_QUERY = `
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $ownerAffiliations: [RepositoryAffiliation]) {
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {
user(login: $login) {
name
login
contributionsCollection {
commits: contributionsCollection (from: $startTime) {
totalCommitContributions,
}
reviews: contributionsCollection {
totalPullRequestReviewContributions
}
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
@@ -111,6 +113,7 @@ const fetcher = (variables, token) => {
* @param {boolean} variables.includeMergedPullRequests Include merged pull requests.
* @param {boolean} variables.includeDiscussions Include discussions.
* @param {boolean} variables.includeDiscussionsAnswers Include discussions answers.
* @param {string|undefined} variables.startTime Time to start the count of total commits.
* @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER.
* @returns {Promise<AxiosResponse>} Axios response.
*
@@ -121,6 +124,7 @@ const statsFetcher = async ({
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
ownerAffiliations,
}) => {
let stats;
@@ -134,6 +138,7 @@ const statsFetcher = async ({
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
ownerAffiliations,
};
let res = await retryer(fetcher, variables);
@@ -295,6 +300,8 @@ const fetchRepoUserStats = async (
* @param {boolean} include_discussions Include discussions.
* @param {boolean} include_discussions_answers Include discussions answers.
* @param {string[]} ownerAffiliations Owner affiliations. Default: OWNER.
* @param {number|undefined} commits_year Year to count total commits
* @param {string[]} ownerAffiliations Owner affiliations. Default: OWNER.
* @returns {Promise<StatsData>} Stats data.
*/
const fetchStats = async (
@@ -304,6 +311,7 @@ const fetchStats = async (
include_merged_pull_requests = false,
include_discussions = false,
include_discussions_answers = false,
commits_year,
repo = [],
owner = [],
include_prs_authored = false,
@@ -343,6 +351,7 @@ const fetchStats = async (
includeMergedPullRequests: include_merged_pull_requests,
includeDiscussions: include_discussions,
includeDiscussionsAnswers: include_discussions_answers,
startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined,
ownerAffiliations,
});
@@ -381,7 +390,7 @@ const fetchStats = async (
`author:${username}`,
);
} else {
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
stats.totalCommits = user.commits.totalCommitContributions;
}
let repoUserStats = await fetchRepoUserStats(
username,
@@ -401,8 +410,7 @@ const fetchStats = async (
stats.mergedPRsPercentage =
(user.mergedPullRequests.totalCount / user.pullRequests.totalCount) * 100;
}
stats.totalReviews =
user.contributionsCollection.totalPullRequestReviewContributions;
stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
if (include_discussions) {
stats.totalDiscussionsStarted = user.repositoryDiscussions.totalCount;
+3 -1
View File
@@ -38,8 +38,10 @@ const data_stats = {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
contributionsCollection: {
commits: {
totalCommitContributions: stats.totalCommits,
},
reviews: {
totalPullRequestReviewContributions: stats.totalReviews,
},
pullRequests: { totalCount: stats.totalPRs },
+3 -1
View File
@@ -24,8 +24,10 @@ const data_stats = {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
contributionsCollection: {
commits: {
totalCommitContributions: stats.totalCommits,
},
reviews: {
totalPullRequestReviewContributions: stats.totalReviews,
},
pullRequests: { totalCount: stats.totalPRs },
+54 -2
View File
@@ -11,8 +11,10 @@ const data_stats = {
user: {
name: "Anurag Hazra",
repositoriesContributedTo: { totalCount: 61 },
contributionsCollection: {
commits: {
totalCommitContributions: 100,
},
reviews: {
totalPullRequestReviewContributions: 50,
},
pullRequests: { totalCount: 300 },
@@ -38,6 +40,9 @@ const data_stats = {
},
};
const data_year2003 = JSON.parse(JSON.stringify(data_stats));
data_year2003.data.user.commits.totalCommitContributions = 428;
const data_repo = {
data: {
user: {
@@ -91,9 +96,18 @@ const mock = new MockAdapter(axios);
beforeEach(() => {
process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars.
mock.onPost("https://api.github.com/graphql").reply((cfg) => {
let req = JSON.parse(cfg.data);
if (
req.variables &&
req.variables.startTime &&
req.variables.startTime.startsWith("2003")
) {
return [200, data_year2003];
}
return [
200,
cfg.data.includes("contributionsCollection") ? data_stats : data_repo,
req.query.includes("totalCommitContributions") ? data_stats : data_repo,
];
});
});
@@ -460,4 +474,42 @@ describe("Test fetchStats", () => {
rank,
});
});
it("should get commits of provided year", async () => {
let stats = await fetchStats(
"anuraghazra",
false,
[],
false,
false,
false,
2003,
);
const rank = calculateRank({
all_commits: false,
commits: 428,
prs: 300,
reviews: 50,
issues: 200,
repos: 5,
stars: 300,
followers: 100,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 428,
totalIssues: 200,
totalPRs: 300,
totalPRsMerged: 0,
mergedPRsPercentage: 0,
totalReviews: 50,
totalStars: 300,
totalDiscussionsStarted: 0,
totalDiscussionsAnswered: 0,
rank,
});
});
});
+1 -1
View File
@@ -381,7 +381,7 @@ describe("Test renderStatsCard", () => {
document.querySelector(
'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"累计提交总数 (${new Date().getFullYear()}):"`);
).toMatchInlineSnapshot(`"累计提交总数 (去年):"`);
expect(
document.querySelector(
'g[transform="translate(0, 50)"]>.stagger>.stat.bold',