diff --git a/api/pin.js b/api/pin.js index ada955169910a..ac30ed9cef747 100644 --- a/api/pin.js +++ b/api/pin.js @@ -23,6 +23,10 @@ export default async (req, res) => { username, repo, hide_border, + hide_title, + hide_text, + stats_only, + all_stats, title_color, icon_color, text_color, @@ -34,6 +38,9 @@ export default async (req, res) => { border_radius, border_color, description_lines_count, + show_issues, + show_prs, + show_age, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -81,9 +88,21 @@ export default async (req, res) => { setCacheHeaders(res, cacheSeconds); + const statsOnly = parseBoolean(stats_only) === true; + const allStats = parseBoolean(all_stats) === true; + + const finalShowIssues = allStats ? true : parseBoolean(show_issues); + const finalShowPrs = allStats ? true : parseBoolean(show_prs); + const finalShowAge = allStats ? true : parseBoolean(show_age); + const finalHideTitle = statsOnly ? true : parseBoolean(hide_title); + const finalHideText = statsOnly ? true : parseBoolean(hide_text); + return res.send( renderRepoCard(repoData, { hide_border: parseBoolean(hide_border), + hide_title: finalHideTitle, + hide_text: finalHideText, + stats_only: statsOnly, title_color, icon_color, text_color, @@ -94,6 +113,9 @@ export default async (req, res) => { show_owner: parseBoolean(show_owner), locale: locale ? locale.toLowerCase() : null, description_lines_count, + show_issues: finalShowIssues, + show_prs: finalShowPrs, + show_age: finalShowAge, }), ); } catch (err) { diff --git a/readme.md b/readme.md index 78fe9ae42fb69..2a8b5071bcb5d 100644 --- a/readme.md +++ b/readme.md @@ -422,6 +422,18 @@ You can customize the appearance and behavior of the pinned repository card usin | `show_owner` | Shows the repo's owner name. | boolean | `false` | | `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` | +The following data points are also exposed via query params, however they have not been incorporated visually into any of the repo's themes as of yet: + +| Name | Description | Type | Default value | +| --- | --- | --- | --- | +| `show_issues` | Shows the number of open issues that the repo has. | boolean | `false` | +| `show_prs` | Shows the number of open PRs that the repo has. | boolean | `false` | +| `show_age` | Shows the age of the repo (based on when the repository was created on GitHub). | boolean | `false` | +| `all_stats` | Shows all the metrics listed above; shorthand for `?shows_issues=true&show_prs=true&show_age=true` | boolean | `false` | +| `stats_only` | Hides the title and the description. | boolean | `false` | + + + ### Demo ![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra\&repo=github-readme-stats) diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 7535df35bbe6f..4b8d5ac0ee9d2 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -35,6 +35,13 @@ export type StatCardOptions = CommonOptions & { export type RepoCardOptions = CommonOptions & { show_owner: boolean; description_lines_count: number; + hide_title?: boolean; + hide_text?: boolean; + stats_only?: boolean; + all_stats?: boolean; + show_issues?: boolean; + show_prs?: boolean; + show_age?: boolean; }; export type TopLangOptions = CommonOptions & { diff --git a/src/fetchers/repo.js b/src/fetchers/repo.js index 304aba5fdc23c..4e818b31385b6 100644 --- a/src/fetchers/repo.js +++ b/src/fetchers/repo.js @@ -21,9 +21,17 @@ const fetcher = (variables, token) => { isPrivate isArchived isTemplate + createdAt + pushedAt stargazers { totalCount } + issues(states: OPEN) { + totalCount + } + pullRequests(states: OPEN) { + totalCount + } description primaryLanguage { color @@ -89,26 +97,55 @@ const fetchRepo = async (username, reponame) => { const isOrg = data.user === null && data.organization; if (isUser) { - if (!data.user.repository || data.user.repository.isPrivate) { + const repository = data.user.repository; + if (!repository || repository.isPrivate) { throw new Error("User Repository Not found"); } - return { - ...data.user.repository, - starCount: data.user.repository.stargazers.totalCount, + const result = { + ...repository, + starCount: repository.stargazers.totalCount, + firstCommitDate: repository.createdAt || null, }; + if (repository.issues) { + // `issues` can be omitted entirely when disabled, so only expose the count when available. + result.openIssuesCount = repository.issues.totalCount; + } + if (repository.pullRequests) { + result.openPrsCount = repository.pullRequests.totalCount; + } + if (repository.createdAt) { + result.createdAt = repository.createdAt; + } + if (repository.pushedAt) { + result.pushedAt = repository.pushedAt; + } + return result; } if (isOrg) { - if ( - !data.organization.repository || - data.organization.repository.isPrivate - ) { + const repository = data.organization.repository; + if (!repository || repository.isPrivate) { throw new Error("Organization Repository Not found"); } - return { - ...data.organization.repository, - starCount: data.organization.repository.stargazers.totalCount, + const result = { + ...repository, + starCount: repository.stargazers.totalCount, + firstCommitDate: repository.createdAt || null, }; + if (repository.issues) { + // `issues` can be omitted entirely when disabled, so only expose the count when available. + result.openIssuesCount = repository.issues.totalCount; + } + if (repository.pullRequests) { + result.openPrsCount = repository.pullRequests.totalCount; + } + if (repository.createdAt) { + result.createdAt = repository.createdAt; + } + if (repository.pushedAt) { + result.pushedAt = repository.pushedAt; + } + return result; } throw new Error("Unexpected behavior"); diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts index affb407b816b0..ce22a5765d262 100644 --- a/src/fetchers/types.d.ts +++ b/src/fetchers/types.d.ts @@ -13,6 +13,8 @@ export type RepositoryData = { isPrivate: boolean; isArchived: boolean; isTemplate: boolean; + createdAt?: string; + pushedAt?: string; stargazers: { totalCount: number }; description: string; primaryLanguage: { @@ -22,6 +24,9 @@ export type RepositoryData = { }; forkCount: number; starCount: number; + openIssuesCount?: number; + openPrsCount?: number; + firstCommitDate: string | null; }; export type StatsData = { diff --git a/tests/fetchRepo.test.js b/tests/fetchRepo.test.js index e976dd72a47ce..162fb05bcf4c3 100644 --- a/tests/fetchRepo.test.js +++ b/tests/fetchRepo.test.js @@ -7,7 +7,19 @@ import { fetchRepo } from "../src/fetchers/repo.js"; const data_repo = { repository: { name: "convoychat", + createdAt: "2020-01-01T00:00:00Z", + pushedAt: "2020-01-02T00:00:00Z", stargazers: { totalCount: 38000 }, + issues: { totalCount: 12 }, + pullRequests: { totalCount: 3 }, + defaultBranchRef: { + name: "main", + target: { + history: { + nodes: [{ committedDate: "2019-12-01T00:00:00Z" }], + }, + }, + }, description: "Help us take over the world! React + TS + GraphQL Chat App", primaryLanguage: { color: "#2b7489", @@ -39,7 +51,7 @@ afterEach(() => { }); describe("Test fetchRepo", () => { - it("should fetch correct user repo", async () => { + it("should fetch repository for user owner", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_user); let repo = await fetchRepo("anuraghazra", "convoychat"); @@ -47,16 +59,27 @@ describe("Test fetchRepo", () => { expect(repo).toStrictEqual({ ...data_repo.repository, starCount: data_repo.repository.stargazers.totalCount, + openIssuesCount: data_repo.repository.issues.totalCount, + openPrsCount: data_repo.repository.pullRequests.totalCount, + createdAt: data_repo.repository.createdAt, + pushedAt: data_repo.repository.pushedAt, + firstCommitDate: data_repo.repository.createdAt, }); }); - it("should fetch correct org repo", async () => { + it("should fetch repository for organization owner", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_org); let repo = await fetchRepo("anuraghazra", "convoychat"); + expect(repo).toStrictEqual({ ...data_repo.repository, starCount: data_repo.repository.stargazers.totalCount, + openIssuesCount: data_repo.repository.issues.totalCount, + openPrsCount: data_repo.repository.pullRequests.totalCount, + createdAt: data_repo.repository.createdAt, + pushedAt: data_repo.repository.pushedAt, + firstCommitDate: data_repo.repository.createdAt, }); }); @@ -70,7 +93,7 @@ describe("Test fetchRepo", () => { ); }); - it("should throw error if org is found but repo is null", async () => { + it("should throw error if repo is null", async () => { mock .onPost("https://api.github.com/graphql") .reply(200, { data: { user: null, organization: { repository: null } } }); @@ -93,7 +116,9 @@ describe("Test fetchRepo", () => { it("should throw error if repository is private", async () => { mock.onPost("https://api.github.com/graphql").reply(200, { data: { - user: { repository: { ...data_repo, isPrivate: true } }, + user: { + repository: { ...data_repo.repository, isPrivate: true }, + }, organization: null, }, }); diff --git a/tests/pin.test.js b/tests/pin.test.js index 6bc8bff96c0da..c91985935da50 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -24,6 +24,9 @@ const data_repo = { }, forkCount: 100, isTemplate: false, + isPrivate: false, + isArchived: false, + firstCommitDate: "2018-10-01T00:00:00Z", }, }; @@ -76,6 +79,8 @@ describe("Test /api/pin", () => { text_color: "fff", bg_color: "fff", full_name: "1", + hide_title: true, + hide_text: true, }, }; const res = { @@ -99,6 +104,74 @@ describe("Test /api/pin", () => { ); }); + it("should make stats_only take precedence over hide flags", async () => { + const req = { + query: { + username: "anuraghazra", + repo: "convoychat", + hide_title: "false", + hide_text: "false", + stats_only: "true", + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock.onPost("https://api.github.com/graphql").reply(200, data_user); + + await pin(req, res); + + expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); + const expectedSvg = renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { + stats_only: true, + hide_title: true, + hide_text: true, + }, + ); + expect(res.send).toHaveBeenCalledWith(expectedSvg); + }); + + it("should make all_stats enable issues, PRs, and age", async () => { + const req = { + query: { + username: "anuraghazra", + repo: "convoychat", + show_issues: "false", + show_prs: "false", + show_age: "false", + all_stats: "true", + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock.onPost("https://api.github.com/graphql").reply(200, data_user); + + await pin(req, res); + + expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); + const expectedSvg = renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { + show_issues: true, + show_prs: true, + show_age: true, + all_stats: true, + }, + ); + expect(res.send).toHaveBeenCalledWith(expectedSvg); + }); + it("should render error card if user repo not found", async () => { const req = { query: {