Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ exports[`Test /api contract > should match the public many-params response snaps

</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n ...RepoNode\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\\nfragment RepoNode on Repository {\\n name\\n stargazerCount\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n ...RepoNode\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\\nfragment RepoNode on Repository {\\n name\\n stargazerCount\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"headers": [
[
"Cache-Control",
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/tests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export const data_stats = {
name: "Anurag Hazra",
login: "anuraghazra",
repositoriesContributedTo: { totalCount: 51 },
contributionsCollection: {
contributionYears: [2022, 2024],
},
commits: {
totalCommitContributions: 200,
},
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/pages/Home/buildCardUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("buildCardUrl", () => {
"?username=john" +
"&hide_title=true" +
// commas are percent-encoded (%2C) now that params go through URLSearchParams
"&show=reviews%2Cdiscussions_started%2Cdiscussions_answered%2Cprs_merged%2Cprs_merged_percentage%2Cprs_commented%2Cprs_reviewed%2Cissues_commented" +
"&show=reviews%2Cdiscussions_started%2Cdiscussions_answered%2Cprs_merged%2Cprs_merged_percentage%2Cprs_commented%2Cprs_reviewed%2Cissues_commented%2Ccontributions" +
"&show_icons=true" +
"&include_all_commits=true",
);
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/pages/Home/buildCardUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function buildCardUrl(
}
if (showAllStats) {
url = url.show(
"reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented",
"reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented,contributions",
);
}
if (showIcons) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export default async (
showStats.includes("issues_authored"),
showStats.includes("issues_commented"),
parseArray(role),
showStats.includes("contributions"),
pat,
);

Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/cards/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const renderStatsCard = (
totalPRsReviewed,
totalIssuesAuthored,
totalIssuesCommented,
totalContributions,
rank,
} = stats;
const {
Expand Down Expand Up @@ -293,6 +294,16 @@ const renderStatsCard = (
value: totalStars,
id: "stars",
};

if (show.includes("contributions")) {
STATS["contributions"] = {
icon: icons.contributions,
label: i18n.t("statcard.contributions"),
value: totalContributions,
id: "contributions",
};
}

STATS["commits"] = {
icon: icons.commits,
label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel(
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/common/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const icons = {
discussions_started: `<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z" />`,
discussions_answered: `<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />`,
comments: `<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />`,
contributions: `<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7.25-3.25v2.5h2.5a.75.75 0 0 1 0 1.5h-2.5v2.5a.75.75 0 0 1-1.5 0v-2.5h-2.5a.75.75 0 0 1 0-1.5h2.5v-2.5a.75.75 0 0 1 1.5 0Z"/>`,
gist: `<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />`,
};

Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/fetchers/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import { getConfig } from "../common/config.js";
import { CustomError, MissingParamError } from "../common/error.js";
import { wrapTextMultiline } from "../common/fmt.js";
import { request } from "../common/http.js";

Check warning on line 9 in packages/core/src/fetchers/stats.ts

View workflow job for this annotation

GitHub Actions / Code checks

'/home/runner/work/github-stats-extended/github-stats-extended/packages/core/src/common/http.ts' imported multiple times
import { createGraphQLFetcher } from "../common/http.js";

Check warning on line 10 in packages/core/src/fetchers/stats.ts

View workflow job for this annotation

GitHub Actions / Code checks

'/home/runner/work/github-stats-extended/github-stats-extended/packages/core/src/common/http.ts' imported multiple times
import type { GraphQLResponse } from "../common/http.js";
import { logger } from "../common/log.js";
import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js";
Expand All @@ -19,6 +20,7 @@
RepoNodeFragment,
UserInfoQuery,
UserInfoQueryVariables,
YearContributionsFragment,
} from "../graphql/generated/stats.js";

import type { RepoUserStats, StatsData } from "./types.js";
Expand Down Expand Up @@ -274,6 +276,72 @@
return stats;
};

interface ContributionsQuery {
user: Record<string, YearContributionsFragment> | null;
}

/**
* Fetch all-time contributions by building a single GraphQL query
* for all the given years.
*
* Whether private contributions are included depends on the user's profile settings:
* 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
*/
const fetchTotalContributions = async (
username: string,
years: Array<number>,
pat: string | null = null,
): Promise<number> => {
if (years.length === 0) {
return 0;
}

const yearFields = years
.map(
(year) =>
// without the "to" field, 2024-01-01 would be included for year=2023
`year_${year}: contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") { contributionCalendar { totalContributions } }`,
)
.join("\n");

const query = `
query userContributions($login: String!) {
user(login: $login) {
${yearFields}
}
}
`;

const contributionsFetcher = (
variables: Record<string, unknown>,
token: string,
): Promise<GraphQLResponse<ContributionsQuery>> => {
return request(
{ query, variables },
{ Authorization: `bearer ${token}` },
) as Promise<GraphQLResponse<ContributionsQuery>>;
};

const contribRes = await retryer(
contributionsFetcher,
{ login: username },
pat,
);
const user = contribRes.data.data.user;
if (!user) {
return 0;
}

let total = 0;
for (const year of years) {
const yearBlock = user[`year_${year}`];
if (yearBlock?.contributionCalendar.totalContributions) {
total += yearBlock.contributionCalendar.totalContributions;
}
}
return total;
};

/**
* Fetch stats for a given username.
*
Expand All @@ -292,6 +360,7 @@
* @param include_issues_authored Include count of issues authored.
* @param include_issues_commented Include count of issues commented.
* @param ownerAffiliations Owner affiliations. Default: OWNER.
* @param include_contributions Include all-time contributions.
* @param pat Optional PAT override.
* @returns Stats data.
*/
Expand All @@ -311,6 +380,7 @@
include_issues_authored = false,
include_issues_commented = false,
ownerAffiliations: Array<string> = [],
include_contributions = false,
pat: string | null = null,
): Promise<StatsData> => {
if (!username) {
Expand All @@ -334,6 +404,7 @@
totalPRsReviewed: 0,
totalIssuesAuthored: 0,
totalIssuesCommented: 0,
totalContributions: 0,
rank: { level: "C", percentile: 100 },
};
const affiliations = parseOwnerAffiliations(ownerAffiliations);
Expand Down Expand Up @@ -421,6 +492,14 @@
}
stats.contributedTo = user.repositoriesContributedTo.totalCount;

if (include_contributions) {
stats.totalContributions = await fetchTotalContributions(
username,
user.contributionsCollection.contributionYears,
pat,
);
}

// Retrieve stars while filtering out repositories to be hidden.
const allExcludedRepos = [
...exclude_repo,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/fetchers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface StatsData {
totalPRsReviewed: number;
totalIssuesAuthored: number;
totalIssuesCommented: number;
totalContributions: number;
rank: { level: string; percentile: number };
}

Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/graphql/generated/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type UserInfoQuery = {
followers: { totalCount: number };
repositoryDiscussions?: { totalCount: number };
repositoryDiscussionComments?: { totalCount: number };
contributionsCollection: { contributionYears: Array<number> };
repositories: {
totalCount: number;
nodes: Array<{ name: string; stargazerCount: number } | null> | null;
Expand All @@ -71,6 +72,10 @@ export type UserInfoQuery = {
} | null;
};

export type YearContributionsFragment = {
contributionCalendar: { totalContributions: number };
};

export const UserReposDocument = graphqlDocument<
UserReposQuery,
UserReposQueryVariables
Expand Down Expand Up @@ -143,6 +148,9 @@ query userInfo($login: String!, $after: String, $includeMergedPullRequests: Bool
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
totalCount
}
contributionsCollection {
contributionYears
}
...RepoStars
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/graphql/queries/stats.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ query userInfo(
@include(if: $includeDiscussionsAnswers) {
totalCount
}
contributionsCollection {
contributionYears
}
...RepoStars
}
}

fragment YearContributions on ContributionsCollection {
contributionCalendar {
totalContributions
}
}
3 changes: 3 additions & 0 deletions packages/core/src/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,9 @@ const statCardLocales = ({
"statcard.issues-commented": {
en: "Issues Commented",
},
"statcard.contributions": {
en: "Total Contributions",
},
"statcard.prs-merged": {
en: "Total PRs Merged",
ar: "مجموع طلبات السحب المُدمجة",
Expand Down
Loading