From 08517568d91a52796c7c6935254477c5a27cead1 Mon Sep 17 00:00:00 2001 From: my99N Date: Thu, 18 Sep 2025 02:01:37 +0700 Subject: [PATCH] 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 * 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 * Update src/fetchers/stats-fetcher.js Co-authored-by: Alexandr Garbuzov * graphQL year only to commit * change argument name to commits_year * Update readme.md Co-authored-by: Alexandr Garbuzov * Update readme.md Co-authored-by: Alexandr Garbuzov * Update readme.md Co-authored-by: Alexandr Garbuzov * 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 Co-authored-by: Rick Staa Co-authored-by: Alexandr Garbuzov Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/api-renamed/index.js | 3 ++ backend/readme.md | 9 +++++ backend/src/cards/stats.js | 39 +++++++++++++++---- backend/src/cards/types.d.ts | 1 + backend/src/fetchers/stats.js | 18 ++++++--- backend/tests/api.test.js | 4 +- backend/tests/bench/api.bench.js | 4 +- backend/tests/fetchStats.test.js | 56 ++++++++++++++++++++++++++- backend/tests/renderStatsCard.test.js | 2 +- 9 files changed, 118 insertions(+), 18 deletions(-) diff --git a/backend/api-renamed/index.js b/backend/api-renamed/index.js index a1a7b90b..ab74cdc3 100644 --- a/backend/api-renamed/index.js +++ b/backend/api-renamed/index.js @@ -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, diff --git a/backend/readme.md b/backend/readme.md index 4781a21b..c7d9c14e 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -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)_ | ` (one year to date)`. > [!NOTE]\ > When hide\_rank=`true`, the minimum card width is 270 px + the title length and padding. diff --git a/backend/src/cards/stats.js b/backend/src/cards/stats.js index 4893ce65..468ead9f 100644 --- a/backend/src/cards/stats.js +++ b/backend/src/cards/stats.js @@ -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}`; }) diff --git a/backend/src/cards/types.d.ts b/backend/src/cards/types.d.ts index 74f84598..bf0fd302 100644 --- a/backend/src/cards/types.d.ts +++ b/backend/src/cards/types.d.ts @@ -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; diff --git a/backend/src/fetchers/stats.js b/backend/src/fetchers/stats.js index c55dd044..2467b5fa 100644 --- a/backend/src/fetchers/stats.js +++ b/backend/src/fetchers/stats.js @@ -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} 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} 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; diff --git a/backend/tests/api.test.js b/backend/tests/api.test.js index 43f1c8ac..12f7319c 100644 --- a/backend/tests/api.test.js +++ b/backend/tests/api.test.js @@ -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 }, diff --git a/backend/tests/bench/api.bench.js b/backend/tests/bench/api.bench.js index 4796b643..f5f192d9 100644 --- a/backend/tests/bench/api.bench.js +++ b/backend/tests/bench/api.bench.js @@ -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 }, diff --git a/backend/tests/fetchStats.test.js b/backend/tests/fetchStats.test.js index a6197427..0ef91113 100644 --- a/backend/tests/fetchStats.test.js +++ b/backend/tests/fetchStats.test.js @@ -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, + }); + }); }); diff --git a/backend/tests/renderStatsCard.test.js b/backend/tests/renderStatsCard.test.js index cf6c7f44..ee42e0a5 100644 --- a/backend/tests/renderStatsCard.test.js +++ b/backend/tests/renderStatsCard.test.js @@ -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',