Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include issue/PR numbers under contribution types #13

Merged
merged 2 commits into from
May 10, 2023
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ Additionally, based on PR [conventional commit titles](https://www.conventionalc
> import { $ } from "execa";
>
> for (const [contributor, contributions] of Object.entries(contributors)) {
> await $`npx all-contributors add ${contributor} ${contributions.join(",")}`;
> const contributionTypes = Object.keys(contributions).join(",");
> await $`npx all-contributors add ${contributor} ${contributionTypes}`;
> }
> ```

Expand Down
34 changes: 34 additions & 0 deletions src/Contributor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";

import { Contributor } from "./Contributor.js";

describe("Contributor", () => {
it("adds a new entry under a type when that type doesn't yet exist", () => {
const contributor = new Contributor();

contributor.add(1, "code");

expect(contributor.contributions).toEqual({ code: new Set([1]) });
});

it("adds a new entry under an existing type when that type already exists", () => {
const contributor = new Contributor();

contributor.add(1, "code");
contributor.add(2, "code");

expect(contributor.contributions).toEqual({ code: new Set([1, 2]) });
});

it("adds a new entry under a second type when that type does not already exist", () => {
const contributor = new Contributor();

contributor.add(1, "code");
contributor.add(2, "design");

expect(contributor.contributions).toEqual({
code: new Set([1]),
design: new Set([2]),
});
});
});
7 changes: 7 additions & 0 deletions src/Contributor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class Contributor {
readonly contributions: Record<string, Set<number>> = {};

add(number: number, type: string) {
(this.contributions[type] ??= new Set()).add(number);
}
}
24 changes: 12 additions & 12 deletions src/ContributorsCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,40 @@ describe("ContributorsCollection", () => {
it("adds a login when it is not ignored", () => {
const contributors = new ContributorsCollection(new Set());

contributors.add("abc", "bug");
contributors.add("abc", 0, "bug");

const actual = contributors.collect();

expect(actual).toEqual({ abc: ["bug"] });
expect(actual).toEqual({ abc: { bug: [0] } });
});

it("adds sorted contributions for logins when they are added in non-alphabetical order ", () => {
const contributors = new ContributorsCollection(new Set());

contributors.add("def", "tool");
contributors.add("abc", "tool");
contributors.add("abc", "bug");
contributors.add("abc", "code");
contributors.add("def", "code");
contributors.add("def", 1, "tool");
contributors.add("abc", 2, "tool");
contributors.add("abc", 3, "bug");
contributors.add("abc", 4, "code");
contributors.add("def", 5, "code");

const actual = contributors.collect();

expect(actual).toEqual({
abc: ["bug", "code", "tool"],
def: ["code", "tool"],
abc: { bug: [3], code: [4], tool: [2] },
def: { code: [5], tool: [1] },
});
});

it("does not add a login contribution when it is ignored ", () => {
const contributors = new ContributorsCollection(new Set(["ignored"]));

contributors.add("abc", "bug");
contributors.add("ignored", "code");
contributors.add("abc", 1, "bug");
contributors.add("ignored", 2, "code");

const actual = contributors.collect();

expect(actual).toEqual({
abc: ["bug"],
abc: { bug: [1] },
});
});
});
40 changes: 34 additions & 6 deletions src/ContributorsCollection.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
import { Contributor } from "./Contributor.js";

/**
* For a set of logins, the contributions under those users.
*/
export type ContributorsContributions = Record<
string,
ContributorContributions
>;

/**
* For each contribution under a login, the issue/PR numbers that count as that type.
*/
export type ContributorContributions = Record<string, number[]>;

export class ContributorsCollection {
#contributors: Record<string, Set<string>> = {};
#contributors: Record<string, Contributor> = {};
#ignoredLogins: Set<string>;

constructor(ignoredLogins: Set<string>) {
this.#ignoredLogins = ignoredLogins;
}

add(login: string | undefined, type: string) {
add(login: string | undefined, number: number, type: string) {
if (login && !this.#ignoredLogins.has(login)) {
(this.#contributors[login.toLowerCase()] ??= new Set()).add(type);
(this.#contributors[login.toLowerCase()] ??= new Contributor()).add(
number,
type
);
}
}

collect() {
collect(): ContributorsContributions {
return Object.fromEntries(
Object.entries(this.#contributors)
.map(
([contributor, types]) =>
[contributor, Array.from(types).sort()] as const
([login, contributor]) =>
[
login,
Object.fromEntries(
Object.entries(contributor.contributions).map(
([type, numbers]) => [
type,
Array.from(numbers).sort((a, b) => a - b),
]
)
),
] as const
)
.sort(([a], [b]) => a.localeCompare(b))
);
Expand Down
2 changes: 2 additions & 0 deletions src/collect/collectEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Octokit } from "octokit";

import { paginate, RequestDefaults } from "./api.js";

export type RepoEvent = Awaited<ReturnType<typeof collectEvents>>[number];

export async function collectEvents(
defaults: RequestDefaults,
octokit: Octokit
Expand Down
17 changes: 17 additions & 0 deletions src/collect/eventIsPullRequestReviewEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";

import { eventIsPullRequestReviewEvent } from "./eventIsPullRequestReviewEvent.js";

describe("eventIsPullRequestReviewEvent", () => {
it.each([
[{ type: "other" }, false],
[{ issue: {}, type: "other" }, false],
[{ issue: { number: 1 }, type: "other" }, false],
[{ type: "PullRequestReviewEvent" }, false],
[{ issue: { number: 1 }, type: "PullRequestReviewEvent" }, true],
])("when given %j, returns %s", (event, expected) => {
const actual = eventIsPullRequestReviewEvent(event);

expect(actual).toBe(expected);
});
});
16 changes: 16 additions & 0 deletions src/collect/eventIsPullRequestReviewEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RepoEvent } from "./collectEvents.js";

type PullRequestReviewEvent = RepoEvent & {
issue: {
number: number;
};
};

export function eventIsPullRequestReviewEvent(
event: Pick<RepoEvent, "type">
): event is PullRequestReviewEvent {
return (
event.type === "PullRequestReviewEvent" &&
!!(event as Partial<PullRequestReviewEvent>).issue?.number
);
}
28 changes: 19 additions & 9 deletions src/collect/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { ContributorsCollection } from "../ContributorsCollection.js";
import {
ContributorsCollection,
ContributorsContributions,
} from "../ContributorsCollection.js";
import { AllContributorsForRepositoryOptions } from "../options.js";
import { createOctokit } from "./api.js";
import { collectAcceptedIssues } from "./collectAcceptedIssues.js";
import { collectEvents } from "./collectEvents.js";
import { collectIssueEvents } from "./collectIssueEvents.js";
import { collectMergedPulls } from "./collectMergedPulls.js";
import { eventIsPullRequestReviewEvent } from "./eventIsPullRequestReviewEvent.js";
import { parseMergedPullAuthors } from "./parsing/parseMergedPullAuthors.js";
import { parseMergedPullType } from "./parsing/parseMergedPullType.js";

export async function collect(options: AllContributorsForRepositoryOptions) {
export async function collect(
options: AllContributorsForRepositoryOptions
): Promise<ContributorsContributions> {
const contributors = new ContributorsCollection(options.ignoredLogins);
const defaults = { owner: options.owner, repo: options.repo };
const octokit = createOctokit(options.auth);
Expand All @@ -34,27 +40,31 @@ export async function collect(options: AllContributorsForRepositoryOptions) {
[options.labelTypeTool, "tool"],
]) {
if (labels.some((label) => label === labelType)) {
contributors.add(acceptedIssue.user?.login, contribution);
contributors.add(
acceptedIssue.user?.login,
acceptedIssue.number,
contribution
);
}
}
}

// 💻 `code`: all PR authors and co-authors
// 💻 `code` & others: all PR authors and co-authors
for (const mergedPull of mergedPulls) {
const authors = parseMergedPullAuthors(mergedPull);
const type = parseMergedPullType(mergedPull.title);

for (const author of authors) {
contributors.add(author, type);
contributors.add(author, mergedPull.number, type);
}
}

// 🚧 `maintenance`: adding labels to issues and PRs, and merging PRs
const maintainers = new Set<string>();

for (const event of issueEvents) {
if (event.actor) {
contributors.add(event.actor.login, "maintenance");
if (event.actor && event.issue) {
contributors.add(event.actor.login, event.issue.number, "maintenance");
maintainers.add(event.actor.login);
}
}
Expand All @@ -63,10 +73,10 @@ export async function collect(options: AllContributorsForRepositoryOptions) {
// (restricted just to users marked as maintainers)
for (const event of events) {
if (
event.type === "PullRequestReviewEvent" &&
eventIsPullRequestReviewEvent(event) &&
maintainers.has(event.actor.login)
) {
contributors.add(event.actor.login, "review");
contributors.add(event.actor.login, event.issue.number, "review");
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/createAllContributorsForRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { collect } from "./collect/index.js";
import {
fillInOptions,
RawAllContributorsForRepositoryOptions,
} from "./options.js";

export async function createAllContributorsForRepository(
rawOptions: RawAllContributorsForRepositoryOptions
) {
const options = fillInOptions(rawOptions);

return await collect(options);
}
18 changes: 5 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { collect } from "./collect/index.js";
import {
fillInOptions,
RawAllContributorsForRepositoryOptions,
} from "./options.js";

export async function createAllContributorsForRepository(
rawOptions: RawAllContributorsForRepositoryOptions
) {
const options = fillInOptions(rawOptions);

return await collect(options);
}
export {
ContributorContributions,
ContributorsContributions,
} from "./ContributorsCollection.js";
export { createAllContributorsForRepository } from "./createAllContributorsForRepository.js";