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
22 changes: 22 additions & 0 deletions api/pin.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default async (req, res) => {
username,
repo,
hide_border,
hide_title,
hide_text,
stats_only,
Comment on lines +26 to +28
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably remove these

all_stats,
title_color,
icon_color,
text_color,
Expand All @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
<!-- NOTE: Maybe should omit `stats_only` - more strictly layout-related. --->
<!-- NOTE: Consider including `show` and `hide` options following the pattern in the stats card - increases complexity. --->

### Demo

![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra\&repo=github-readme-stats)
Expand Down
7 changes: 7 additions & 0 deletions src/cards/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down
59 changes: 48 additions & 11 deletions src/fetchers/repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down
5 changes: 5 additions & 0 deletions src/fetchers/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type RepositoryData = {
isPrivate: boolean;
isArchived: boolean;
isTemplate: boolean;
createdAt?: string;
pushedAt?: string;
stargazers: { totalCount: number };
description: string;
primaryLanguage: {
Expand All @@ -22,6 +24,9 @@ export type RepositoryData = {
};
forkCount: number;
starCount: number;
openIssuesCount?: number;
openPrsCount?: number;
firstCommitDate: string | null;
};

export type StatsData = {
Expand Down
33 changes: 29 additions & 4 deletions tests/fetchRepo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -39,24 +51,35 @@ 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");

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,
});
});

Expand All @@ -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 } } });
Expand All @@ -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,
},
});
Expand Down
73 changes: 73 additions & 0 deletions tests/pin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const data_repo = {
},
forkCount: 100,
isTemplate: false,
isPrivate: false,
isArchived: false,
firstCommitDate: "2018-10-01T00:00:00Z",
},
};

Expand Down Expand Up @@ -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 = {
Expand All @@ -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: {
Expand Down