diff --git a/src/plugins/github/specToProject.js b/src/plugins/github/specToProject.js new file mode 100644 index 0000000..5ab1864 --- /dev/null +++ b/src/plugins/github/specToProject.js @@ -0,0 +1,48 @@ +// @flow + +import {type Project} from "../../core/project"; +import { + stringToRepoId, + githubOwnerPattern, + githubRepoPattern, +} from "../../core/repoId"; +import {fetchGithubOrg} from "./fetchGithubOrg"; + +/** + * Convert a string repository spec into a project. + * + * The spec may take one of two forms: + * - $REPO_OWNER/$REPO_NAME, as in 'sourcecred/example-github' + * - @$REPO_OWNER, as in '@sourcecred' + * + * In either case, we will create a project with the spec as its + * id. In the first construction, the project will have a single + * RepoId, matching the spec string. In the second construction, + * the project will have a RepoId for every repository owned by that + * owner. + * + * A valid GitHub token must be provided, so that it's possible to + * enumerate the repos for an org. + */ +export async function specToProject( + spec: string, + token: string +): Promise { + const repoSpecMatcher = new RegExp( + `^${githubOwnerPattern}/${githubRepoPattern}$` + ); + const ownerSpecMatcher = new RegExp(`^@${githubOwnerPattern}$`); + if (spec.match(repoSpecMatcher)) { + const project: Project = { + id: spec, + repoIds: [stringToRepoId(spec)], + }; + return project; + } else if (spec.match(ownerSpecMatcher)) { + const owner = spec.slice(1); + const org = await fetchGithubOrg(owner, token); + const project: Project = {id: spec, repoIds: org.repos}; + return project; + } + throw new Error(`invalid spec: ${spec}`); +} diff --git a/src/plugins/github/specToProject.test.js b/src/plugins/github/specToProject.test.js new file mode 100644 index 0000000..c3085c6 --- /dev/null +++ b/src/plugins/github/specToProject.test.js @@ -0,0 +1,63 @@ +// @flow + +import {specToProject} from "./specToProject"; +import {stringToRepoId} from "../../core/repoId"; +jest.mock("./fetchGithubOrg", () => ({fetchGithubOrg: jest.fn()})); +type JestMockFn = $Call; +const fetchGithubOrg: JestMockFn = (require("./fetchGithubOrg") + .fetchGithubOrg: any); + +describe("plugins/github/specToProject", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("works for a repoId", async () => { + const spec = "foo/bar"; + const expected = { + id: spec, + repoIds: [stringToRepoId(spec)], + }; + const actual = await specToProject(spec, "FAKE_TOKEN"); + expect(expected).toEqual(actual); + expect(fetchGithubOrg).not.toHaveBeenCalled(); + }); + it("works for an owner", async () => { + const repos = [stringToRepoId("foo/bar"), stringToRepoId("foo/zod")]; + const spec = "@foo"; + const token = "FAKE_TOKEN"; + const fakeOrg = {name: "foo", repos}; + fetchGithubOrg.mockResolvedValueOnce(fakeOrg); + const actual = await specToProject(spec, token); + expect(fetchGithubOrg).toHaveBeenCalledWith(fakeOrg.name, token); + const expected = {id: spec, repoIds: repos}; + expect(actual).toEqual(expected); + }); + describe("fails for malformed spec strings", () => { + const bad = [ + "foo", + "foo_bar", + "@@foo", + " @foo ", + "foo / bar", + "", + "@foo/bar", + ]; + for (const b of bad) { + it(`fails for "${b}"`, () => { + expect.assertions(2); + const fail = specToProject(b, "FAKE_TOKEN"); + return ( + expect(fail) + .rejects.toThrow(`invalid spec: ${b}`) + // The typedef says toThrow returns void, but this promise chain does + // actually work. We don't need help from flow, since tests will fail + // if the type is wrong. + // $ExpectFlowError + .then(() => { + expect(fetchGithubOrg).toHaveBeenCalledTimes(0); + }) + ); + }); + } + }); +});