diff --git a/src/plugins/github/__snapshots__/api.test.js.snap b/src/plugins/github/__snapshots__/api.test.js.snap new file mode 100644 index 0000000..02ee5c2 --- /dev/null +++ b/src/plugins/github/__snapshots__/api.test.js.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GitHub porcelain API Author 1`] = `2`; + +exports[`GitHub porcelain API Author 2`] = ` +Object { + "address": Object { + "id": "https://github.com/decentralion", + "pluginName": "sourcecred/github-beta", + "repositoryName": "sourcecred/example-repo", + "type": "AUTHOR", + }, + "payload": Object { + "login": "decentralion", + "subtype": "USER", + "url": "https://github.com/decentralion", + }, +} +`; + +exports[`GitHub porcelain API Comment 1`] = ` +Object { + "address": Object { + "id": "https://github.com/sourcecred/example-repo/issues/6#issuecomment-373768442", + "pluginName": "sourcecred/github-beta", + "repositoryName": "sourcecred/example-repo", + "type": "COMMENT", + }, + "payload": Object { + "body": "A wild COMMENT appeared!", + "url": "https://github.com/sourcecred/example-repo/issues/6#issuecomment-373768442", + }, +} +`; + +exports[`GitHub porcelain API Issue 1`] = ` +Object { + "address": Object { + "id": "https://github.com/sourcecred/example-repo/issues/1", + "pluginName": "sourcecred/github-beta", + "repositoryName": "sourcecred/example-repo", + "type": "ISSUE", + }, + "payload": Object { + "body": "This is just an example issue.", + "number": 1, + "title": "An example issue.", + "url": "https://github.com/sourcecred/example-repo/issues/1", + }, +} +`; + +exports[`GitHub porcelain API PullRequest 1`] = ` +Object { + "address": Object { + "id": "https://github.com/sourcecred/example-repo/pull/3", + "pluginName": "sourcecred/github-beta", + "repositoryName": "sourcecred/example-repo", + "type": "PULL_REQUEST", + }, + "payload": Object { + "body": "Oh look, it's a pull request.", + "number": 3, + "title": "Add README, merge via PR.", + "url": "https://github.com/sourcecred/example-repo/pull/3", + }, +} +`; diff --git a/src/plugins/github/api.js b/src/plugins/github/api.js new file mode 100644 index 0000000..9489cb5 --- /dev/null +++ b/src/plugins/github/api.js @@ -0,0 +1,153 @@ +// @flow + +import {Graph} from "../../core/graph"; +import type {Node} from "../../core/graph"; +import type {Address} from "../../core/address"; +import type { + NodePayload, + EdgePayload, + NodeType, + IssueNodePayload, + PullRequestNodePayload, + CommentNodePayload, + AuthorNodePayload, + AuthorSubtype, +} from "./types"; + +import { + CONTAINS_EDGE_TYPE, + COMMENT_NODE_TYPE, + AUTHORS_EDGE_TYPE, + AUTHOR_NODE_TYPE, + ISSUE_NODE_TYPE, + PULL_REQUEST_NODE_TYPE, +} from "./types"; + +export class Repository { + repositoryName: string; + graph: Graph; + + constructor(repositoryName: string, graph: Graph) { + this.repositoryName = repositoryName; + this.graph = graph; + } + + issueOrPRByNumber(number: number): ?(Issue | PullRequest) { + let result: Issue | PullRequest; + this.graph.nodes({type: ISSUE_NODE_TYPE}).forEach((n) => { + if (n.payload.number === number) { + result = new Issue(this.graph, n.address); + } + }); + this.graph.nodes({type: PULL_REQUEST_NODE_TYPE}).forEach((n) => { + if (n.payload.number === number) { + result = new PullRequest(this.graph, n.address); + } + }); + return result; + } + + issues(): Issue[] { + return this.graph + .nodes({type: ISSUE_NODE_TYPE}) + .map((n) => new Issue(this.graph, n.address)); + } + + pullRequests(): PullRequest[] { + return this.graph + .nodes({type: PULL_REQUEST_NODE_TYPE}) + .map((n) => new PullRequest(this.graph, n.address)); + } + + authors(): Author[] { + return this.graph + .nodes({type: AUTHOR_NODE_TYPE}) + .map((n) => new Author(this.graph, n.address)); + } +} + +class GithubEntity { + graph: Graph; + nodeAddress: Address; + + constructor(graph: Graph, nodeAddress: Address) { + this.graph = graph; + this.nodeAddress = nodeAddress; + } + + node(): Node { + return (this.graph.node(this.nodeAddress): Node); + } + + url(): string { + return this.node().payload.url; + } + + type(): NodeType { + return (this.nodeAddress.type: any); + } + + address(): Address { + return this.nodeAddress; + } +} + +class Post< + T: IssueNodePayload | PullRequestNodePayload | CommentNodePayload +> extends GithubEntity { + authors(): Author[] { + return this.graph + .neighborhood(this.nodeAddress, { + edgeType: AUTHORS_EDGE_TYPE, + nodeType: AUTHOR_NODE_TYPE, + }) + .map(({neighborAddress}) => new Author(this.graph, neighborAddress)); + } + + body(): string { + return this.node().payload.body; + } +} + +class Commentable extends Post< + T +> { + comments(): Comment[] { + return this.graph + .neighborhood(this.nodeAddress, { + edgeType: CONTAINS_EDGE_TYPE, + nodeType: COMMENT_NODE_TYPE, + }) + .map(({neighborAddress}) => new Comment(this.graph, neighborAddress)); + } +} + +export class Author extends GithubEntity { + login(): string { + return this.node().payload.login; + } + + subtype(): AuthorSubtype { + return this.node().payload.subtype; + } +} + +export class PullRequest extends Commentable { + number(): number { + return this.node().payload.number; + } + title(): string { + return this.node().payload.title; + } +} + +export class Issue extends Commentable { + number(): number { + return this.node().payload.number; + } + title(): string { + return this.node().payload.title; + } +} + +export class Comment extends Post {} diff --git a/src/plugins/github/api.test.js b/src/plugins/github/api.test.js new file mode 100644 index 0000000..26ce7b6 --- /dev/null +++ b/src/plugins/github/api.test.js @@ -0,0 +1,81 @@ +// @flow + +import {parse} from "./parser"; +import exampleRepoData from "./demoData/example-repo.json"; +import {Repository, Issue, PullRequest, Comment, Author} from "./api"; +import { + AUTHOR_NODE_TYPE, + COMMENT_NODE_TYPE, + ISSUE_NODE_TYPE, + PULL_REQUEST_NODE_TYPE, +} from "./types"; +describe("GitHub porcelain API", () => { + const graph = parse("sourcecred/example-repo", exampleRepoData); + const repo = new Repository("sourcecred/example-repo", graph); + + it("Issue", () => { + const issue = repo.issueOrPRByNumber(1); + if (issue == null) { + throw new Error("Issue reaching issue!"); + } + expect(issue.title()).toBe("An example issue."); + expect(issue.body()).toBe("This is just an example issue."); + expect(issue.number()).toBe(1); + expect(issue.type()).toBe(ISSUE_NODE_TYPE); + expect(issue.url()).toBe( + "https://github.com/sourcecred/example-repo/issues/1" + ); + expect(issue.node()).toMatchSnapshot(); + expect(issue.address()).toEqual(issue.node().address); + expect(issue.authors().map((x) => x.login())).toEqual(["decentralion"]); + }); + + it("PullRequest", () => { + const pullRequest = repo.issueOrPRByNumber(3); + if (pullRequest == null) { + throw new Error("Issue reaching PR!"); + } + expect(pullRequest.body()).toBe("Oh look, it's a pull request."); + expect(pullRequest.url()).toBe( + "https://github.com/sourcecred/example-repo/pull/3" + ); + expect(pullRequest.number()).toBe(3); + expect(pullRequest.type()).toBe(PULL_REQUEST_NODE_TYPE); + expect(pullRequest.node()).toMatchSnapshot(); + expect(pullRequest.address()).toEqual(pullRequest.node().address); + }); + + it("Comment", () => { + const issue = repo.issueOrPRByNumber(6); + if (issue == null) { + throw new Error("Issue reaching issue!"); + } + const comments = issue.comments(); + expect(comments).toHaveLength(2); + const comment = comments[0]; + expect(comment.type()).toBe(COMMENT_NODE_TYPE); + expect(comment.body()).toBe("A wild COMMENT appeared!"); + expect(comment.url()).toBe( + "https://github.com/sourcecred/example-repo/issues/6#issuecomment-373768442" + ); + expect(comment.node()).toMatchSnapshot(); + expect(comment.address()).toEqual(comment.node().address); + expect(comment.authors().map((x) => x.login())).toEqual(["decentralion"]); + }); + + it("Author", () => { + const authors = repo.authors(); + // So we don't need to manually update the test if a new person posts + expect(authors.length).toMatchSnapshot(); + + const decentralion = authors.find((x) => x.login() === "decentralion"); + if (decentralion == null) { + throw new Error("Who let the lions out?"); + } + expect(decentralion.url()).toBe("https://github.com/decentralion"); + expect(decentralion.type()).toBe(AUTHOR_NODE_TYPE); + expect(decentralion.subtype()).toBe("USER"); + expect(decentralion.node()).toMatchSnapshot(); + expect(decentralion.address()).toEqual(decentralion.node().address); + }); +});