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-09-17 22:01:37 +03:00
committed by GitHub
co-authored by Alexandr Garbuzov Copilot Touch Sungkawichai Rick Staa
parent 63b402177d
commit b594b57b56
9 changed files with 118 additions and 18 deletions
+3
View File
@@ -21,6 +21,7 @@ export default async (req, res) => {
hide_rank,
show_icons,
include_all_commits,
commits_year,
line_height,
title_color,
ring_color,
@@ -98,6 +99,7 @@ export default async (req, res) => {
showStats.includes("prs_merged_percentage"),
showStats.includes("discussions_started"),
showStats.includes("discussions_answered"),
parseInt(commits_year, 10),
);
let cacheSeconds = clampValue(
@@ -123,6 +125,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,
+10
View File
@@ -52,6 +52,7 @@
- [Hiding individual stats](#hiding-individual-stats)
- [Showing additional individual stats](#showing-additional-individual-stats)
- [Showing icons](#showing-icons)
- [Showing commits count for specified year](#showing-commits-count-for-specified-year)
- [Themes](#themes)
- [Customization](#customization)
- [GitHub Extra Pins](#github-extra-pins)
@@ -149,6 +150,14 @@ To enable icons, you can pass `&show_icons=true` in the query param, like so:
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true)
```
### 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
With inbuilt themes, you can customize the look of the card without doing any [manual customization](#customization).
@@ -362,6 +371,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`). | 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
@@ -10,7 +10,7 @@ import {
kFormatter,
measureText,
} 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;
@@ -187,6 +187,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
@@ -222,6 +237,7 @@ const renderStatsCard = (stats, options = {}) => {
card_width,
hide_rank = false,
include_all_commits = false,
commits_year,
line_height = 25,
title_color,
ring_color,
@@ -257,7 +273,10 @@ const renderStatsCard = (stats, options = {}) => {
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
@@ -271,9 +290,11 @@ const renderStatsCard = (stats, options = {}) => {
};
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",
};
@@ -515,9 +536,11 @@ const renderStatsCard = (stats, options = {}) => {
.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;
+12 -5
View File
@@ -40,12 +40,14 @@ const GRAPHQL_REPOS_QUERY = `
`;
const GRAPHQL_STATS_QUERY = `
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!) {
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null) {
user(login: $login) {
name
login
contributionsCollection {
commits: contributionsCollection (from: $startTime) {
totalCommitContributions,
}
reviews: contributionsCollection {
totalPullRequestReviewContributions
}
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
@@ -109,6 +111,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.
* @returns {Promise<AxiosResponse>} Axios response.
*
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
@@ -118,6 +121,7 @@ const statsFetcher = async ({
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
}) => {
let stats;
let hasNextPage = true;
@@ -130,6 +134,7 @@ const statsFetcher = async ({
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
};
let res = await retryer(fetcher, variables);
if (res.data.errors) {
@@ -217,6 +222,7 @@ const totalCommitsFetcher = async (username) => {
* @param {boolean} include_merged_pull_requests Include merged pull requests.
* @param {boolean} include_discussions Include discussions.
* @param {boolean} include_discussions_answers Include discussions answers.
* @param {number|undefined} commits_year Year to count total commits
* @returns {Promise<StatsData>} Stats data.
*/
const fetchStats = async (
@@ -226,6 +232,7 @@ const fetchStats = async (
include_merged_pull_requests = false,
include_discussions = false,
include_discussions_answers = false,
commits_year,
) => {
if (!username) {
throw new MissingParamError(["username"]);
@@ -251,6 +258,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,
});
// Catch GraphQL errors.
@@ -282,7 +290,7 @@ const fetchStats = async (
if (include_all_commits) {
stats.totalCommits = await totalCommitsFetcher(username);
} else {
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
stats.totalCommits = user.commits.totalCommitContributions;
}
stats.totalPRs = user.pullRequests.totalCount;
@@ -291,8 +299,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,
];
});
});
@@ -409,4 +423,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
@@ -387,7 +387,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',