Create "REPOSITORY" nodes in GitHub plugin graph (#229)

This commit creates a new node type in the GitHub graph: the REPOSITORY
node. The REPOSITORY node has the following payload properties:
- url (string)
- name (string)
- owner (string)

Things this commit does:
- Add new node type and payload type (RepositoryNodePayload)
- Update parser to instantiate the new node type
- Update api.js to have Repository wrap the new node type (thus
Repository is a GitHub entity)
- Update snapshots
- Update users of GitHub node types to ensure they are exhaustive

Things that will come in a followon commit:
- Add CONTAINS edges from the repository to all its PRs and Issues
- Update the Repository porcelain to use those edges, rather than
scanning the graph for every possible Issue/PR (eventually those might
belong to other Repositories)
- Create a GitHubGraph abstraction in the porcelain, which makes it easy
to find all of the Repositories in a graph

Note that retrieving the repository owner technically involved fetching
the whole owner representation (as a GitHub user). I could have chosen
to add that user to the graph, with a "OWNS" edge pointing to the
repository. For simplicity's sake, I've declined to do that, and instead
just parse the owner's name directly.

Test plan:
Added tests to verify that the Repository porcelain entity has the right
properties. Combined with the snapshot tests, that should be sufficient.
This commit is contained in:
Dandelion Mané 2018-05-07 17:28:47 -07:00 committed by GitHub
parent 9d4ae8b901
commit f219636a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 42 deletions

View File

@ -17,6 +17,21 @@ Array [
"title": "decentralion",
"type": "AUTHOR",
},
Object {
"id": "https://github.com/sourcecred/example-github",
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
"rendered": <div>
type:
REPOSITORY
(details to be implemented)
</div>,
"title": "sourcecred/example-github",
"type": "REPOSITORY",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/1",
"payload": Object {

View File

@ -7,6 +7,7 @@ import type {Node} from "../../../../core/graph";
import type {
NodePayload,
NodeType,
RepositoryNodePayload,
IssueNodePayload,
PullRequestNodePayload,
CommentNodePayload,
@ -46,6 +47,9 @@ const adapter: PluginAdapter<NodePayload> = {
return adapter.extractTitle(graph, graph.node(neighbor));
});
}
function extractRepositoryTitle(node: Node<RepositoryNodePayload>) {
return `${node.payload.owner}/${node.payload.name}`;
}
function extractIssueOrPrTitle(
node: Node<IssueNodePayload | PullRequestNodePayload>
) {
@ -80,6 +84,8 @@ const adapter: PluginAdapter<NodePayload> = {
const anyNode: Node<any> = node;
const type: NodeType = (node.address.type: any);
switch (type) {
case "REPOSITORY":
return extractRepositoryTitle(anyNode);
case "ISSUE":
case "PULL_REQUEST":
return extractIssueOrPrTitle(anyNode);

View File

@ -25,6 +25,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/issues/1\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"ISSUE\\"}": Object {
"payload": Object {
"body": "This is just an example issue.",
@ -140,6 +147,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/issues/6\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"ISSUE\\"}": Object {
"payload": Object {
"body": "This issue shall shortly have a few comments.",
@ -301,6 +315,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/pull/5\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"PULL_REQUEST\\"}": Object {
"payload": Object {
"body": "@wchargin could you please do the following:
@ -410,6 +431,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/pull/3\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"PULL_REQUEST\\"}": Object {
"payload": Object {
"body": "Oh look, it's a pull request.",
@ -843,6 +871,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/issues/1\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"ISSUE\\"}": Object {
"payload": Object {
"body": "This is just an example issue.",
@ -1191,6 +1226,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/issues/2\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"ISSUE\\"}": Object {
"payload": Object {
"body": "This issue references another issue, namely #1",
@ -2016,6 +2058,13 @@ Object {
"url": "https://github.com/decentralion",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"REPOSITORY\\"}": Object {
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
},
"{\\"id\\":\\"https://github.com/sourcecred/example-github/issues/1\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"ISSUE\\"}": Object {
"payload": Object {
"body": "This is just an example issue.",

View File

@ -18,6 +18,7 @@ import type {
PullRequestReviewCommentNodePayload,
PullRequestReviewNodePayload,
PullRequestReviewState,
RepositoryNodePayload,
} from "./types";
import {
@ -36,6 +37,7 @@ import {
import {COMMIT_NODE_TYPE} from "../git/types";
export type Entity =
| Repository
| Issue
| PullRequest
| Comment
@ -51,47 +53,6 @@ function assertEntityType(e: Entity, t: NodeType) {
}
}
export class Repository {
graph: Graph<NodePayload, EdgePayload>;
constructor(graph: Graph<NodePayload, EdgePayload>) {
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<T: NodePayload> {
graph: Graph<NodePayload, EdgePayload>;
nodeAddress: Address;
@ -118,6 +79,53 @@ class GithubEntity<T: NodePayload> {
}
}
export class Repository extends GithubEntity<RepositoryNodePayload> {
// TODO: Now that the Repository is a node in the graph, re-write methods
// that find issues and PRs to find neighbors of the repository rather than
// any matching nodes in the graph. Then, behavior will be correct in the
// case where we have multiple repositories in the same 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;
}
owner(): string {
return this.node().payload.owner;
}
name(): string {
return this.node().payload.name;
}
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 Post<
T:
| IssueNodePayload
@ -167,6 +175,9 @@ class Post<
case "PULL_REQUEST_REVIEW_COMMENT":
result.push(new PullRequestReviewComment(this.graph, neighbor));
break;
case "REPOSITORY":
result.push(new Repository(this.graph, neighbor));
break;
default:
// eslint-disable-next-line no-unused-expressions
(type: empty);

View File

@ -13,7 +13,9 @@ import {
} from "./types";
describe("GitHub porcelain API", () => {
const graph = parse(exampleRepoData);
const repo = new Repository(graph);
// TODO: Create a higher level API that contains all the repositories
const repoNode = graph.nodes({type: "REPOSITORY"})[0];
const repo = new Repository(graph, repoNode.address);
function issueOrPRByNumber(n: number): Issue | PullRequest {
const result = repo.issueOrPRByNumber(n);
if (result == null) {
@ -22,6 +24,11 @@ describe("GitHub porcelain API", () => {
return result;
}
describe("has wrappers for", () => {
it("Repositories", () => {
expect(repo.url()).toBe("https://github.com/sourcecred/example-github");
expect(repo.owner()).toBe("sourcecred");
expect(repo.name()).toBe("example-github");
});
it("Issues", () => {
const issue = issueOrPRByNumber(1);
expect(issue.title()).toBe("An example issue.");

View File

@ -9,6 +9,7 @@ import type {
NodePayload,
EdgePayload,
PullRequestReviewNodePayload,
RepositoryNodePayload,
AuthorNodePayload,
AuthorsEdgePayload,
PullRequestReviewCommentNodePayload,
@ -276,6 +277,8 @@ class GithubParser {
const anyNode: Node<any> = node;
const type: NodeType = (node.address.type: any);
switch (type) {
case "REPOSITORY":
break;
case "ISSUE":
case "PULL_REQUEST":
const thisPayload: IssueNodePayload | PullRequestNodePayload =
@ -325,6 +328,16 @@ class GithubParser {
}
addRepository(repositoryJSON: RepositoryJSON) {
const repositoryPayload: RepositoryNodePayload = {
url: repositoryJSON.url,
name: repositoryJSON.name,
owner: repositoryJSON.owner.login,
};
const repositoryNode: Node<RepositoryNodePayload> = {
address: this.makeNodeAddress("REPOSITORY", repositoryJSON.url),
payload: repositoryPayload,
};
this.graph.addNode(repositoryNode);
repositoryJSON.issues.nodes.forEach((i) => this.addIssue(i));
repositoryJSON.pullRequests.nodes.forEach((pr) => this.addPullRequest(pr));
}

View File

@ -1,6 +1,13 @@
// @flow
/** Node Types */
export const REPOSITORY_NODE_TYPE: "REPOSITORY" = "REPOSITORY";
export type RepositoryNodePayload = {|
+name: string,
+owner: string,
+url: string,
|};
export const ISSUE_NODE_TYPE: "ISSUE" = "ISSUE";
export type IssueNodePayload = {|
+url: string,
@ -60,6 +67,10 @@ export type AuthorNodePayload = {|
// useful at the value layer as $ElementType<NodeTypes, "ISSUE">, for
// instance.
export type NodeTypes = {|
REPOSITORY: {
payload: RepositoryNodePayload,
type: typeof REPOSITORY_NODE_TYPE,
},
ISSUE: {payload: IssueNodePayload, type: typeof ISSUE_NODE_TYPE},
PULL_REQUEST: {
payload: PullRequestNodePayload,
@ -78,6 +89,7 @@ export type NodeTypes = {|
|};
export type NodeType =
| typeof REPOSITORY_NODE_TYPE
| typeof ISSUE_NODE_TYPE
| typeof PULL_REQUEST_NODE_TYPE
| typeof COMMENT_NODE_TYPE
@ -86,6 +98,7 @@ export type NodeType =
| typeof AUTHOR_NODE_TYPE;
export type NodePayload =
| RepositoryNodePayload
| IssueNodePayload
| PullRequestNodePayload
| CommentNodePayload