Update GitHub to use NodeReference & NodePorcelain (#287)

See #286 for context. There are a few miscellaneous changes in
src/app/credExplorer to change clients to use the new API.

Test plan:
New unit test were added, and existing behavior is preserved. Most of
the functionality of the GitHub porcelain was already well tested.

Paired with @wchargin
This commit is contained in:
Dandelion Mané 2018-05-15 17:41:36 -07:00 committed by GitHub
parent 7ccef98c87
commit bb77c36626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 562 additions and 361 deletions

View File

@ -6,6 +6,7 @@ import stringify from "json-stable-stringify";
import {Graph} from "../../core/graph"; import {Graph} from "../../core/graph";
import type {Address} from "../../core/address"; import type {Address} from "../../core/address";
import {AddressMap} from "../../core/address"; import {AddressMap} from "../../core/address";
import {NodeReference} from "../../core/porcelain";
import {PLUGIN_NAME as GITHUB_PLUGIN_NAME} from "../../plugins/github/pluginName"; import {PLUGIN_NAME as GITHUB_PLUGIN_NAME} from "../../plugins/github/pluginName";
import {GIT_PLUGIN_NAME} from "../../plugins/git/types"; import {GIT_PLUGIN_NAME} from "../../plugins/git/types";
import {nodeDescription as githubNodeDescription} from "../../plugins/github/render"; import {nodeDescription as githubNodeDescription} from "../../plugins/github/render";
@ -24,16 +25,16 @@ type State = {
|}, |},
}; };
function nodeDescription(graph, address) { function nodeDescription(ref) {
switch (address.pluginName) { switch (ref.address().pluginName) {
case GITHUB_PLUGIN_NAME: { case GITHUB_PLUGIN_NAME: {
return githubNodeDescription(graph, address); return githubNodeDescription(ref);
} }
case GIT_PLUGIN_NAME: { case GIT_PLUGIN_NAME: {
return gitNodeDescription(graph, address); return gitNodeDescription(ref.graph(), ref.address());
} }
default: { default: {
return stringify(address); return stringify(ref.address());
} }
} }
} }
@ -184,7 +185,7 @@ class RecursiveTable extends React.Component<RTProps, RTState> {
> >
{expanded ? "\u2212" : "+"} {expanded ? "\u2212" : "+"}
</button> </button>
{nodeDescription(graph, address)} {nodeDescription(new NodeReference(graph, address))}
</td> </td>
<td>{(score * 100).toPrecision(3)}</td> <td>{(score * 100).toPrecision(3)}</td>
<td>{Math.log(score).toPrecision(3)}</td> <td>{Math.log(score).toPrecision(3)}</td>

View File

@ -15,9 +15,9 @@
*/ */
import stringify from "json-stable-stringify"; import stringify from "json-stable-stringify";
import {Graph} from "../../core/graph";
import type {Node} from "../../core/graph";
import type {Address} from "../../core/address"; import type {Address} from "../../core/address";
import {Graph} from "../../core/graph";
import {NodeReference, NodePorcelain} from "../../core/porcelain";
import type { import type {
AuthorNodePayload, AuthorNodePayload,
AuthorSubtype, AuthorSubtype,
@ -44,36 +44,26 @@ import {
PULL_REQUEST_NODE_TYPE, PULL_REQUEST_NODE_TYPE,
PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE, PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE,
PULL_REQUEST_REVIEW_NODE_TYPE, PULL_REQUEST_REVIEW_NODE_TYPE,
REPOSITORY_NODE_TYPE,
REFERENCES_EDGE_TYPE, REFERENCES_EDGE_TYPE,
REPOSITORY_NODE_TYPE,
} from "./types"; } from "./types";
import {PLUGIN_NAME} from "./pluginName"; import {PLUGIN_NAME} from "./pluginName";
import {COMMIT_NODE_TYPE} from "../git/types"; import {COMMIT_NODE_TYPE} from "../git/types";
export type Entity = function assertAddressType(address: Address, t: NodeType) {
| Repository if (address.type !== t) {
| Issue
| PullRequest
| Comment
| Author
| PullRequestReview
| PullRequestReviewComment;
function assertEntityType(e: Entity, t: NodeType) {
if (e.type() !== t) {
throw new Error( throw new Error(
`Expected entity at ${stringify(e.address())} to have type ${t}` `Expected entity at ${stringify(address)} to have type ${t}`
); );
} }
} }
export function asEntity( function asGithubReference(
g: Graph<NodePayload, EdgePayload>, ref: NodeReference<any>
addr: Address ): GithubReference<NodePayload> {
): Entity { const addr = ref.address();
const type: NodeType = (addr.type: any);
if (addr.pluginName !== PLUGIN_NAME) { if (addr.pluginName !== PLUGIN_NAME) {
throw new Error( throw new Error(
`Tried to make GitHub porcelain, but got the wrong plugin name: ${stringify( `Tried to make GitHub porcelain, but got the wrong plugin name: ${stringify(
@ -81,21 +71,22 @@ export function asEntity(
)}` )}`
); );
} }
const type: NodeType = (addr.type: any);
switch (type) { switch (type) {
case "ISSUE": case "ISSUE":
return new Issue(g, addr); return new IssueReference(ref);
case "PULL_REQUEST": case "PULL_REQUEST":
return new PullRequest(g, addr); return new PullRequestReference(ref);
case "COMMENT": case "COMMENT":
return new Comment(g, addr); return new CommentReference(ref);
case "AUTHOR": case "AUTHOR":
return new Author(g, addr); return new AuthorReference(ref);
case "PULL_REQUEST_REVIEW": case "PULL_REQUEST_REVIEW":
return new PullRequestReview(g, addr); return new PullRequestReviewReference(ref);
case "PULL_REQUEST_REVIEW_COMMENT": case "PULL_REQUEST_REVIEW_COMMENT":
return new PullRequestReviewComment(g, addr); return new PullRequestReviewCommentReference(ref);
case "REPOSITORY": case "REPOSITORY":
return new Repository(g, addr); return new RepositoryReference(ref);
default: default:
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
(type: empty); (type: empty);
@ -107,7 +98,7 @@ export function asEntity(
} }
} }
export class Porcelain { export class GraphPorcelain {
graph: Graph<NodePayload, EdgePayload>; graph: Graph<NodePayload, EdgePayload>;
constructor(graph: Graph<NodePayload, EdgePayload>) { constructor(graph: Graph<NodePayload, EdgePayload>) {
@ -115,216 +106,261 @@ export class Porcelain {
} }
/* Return all the repositories in the graph */ /* Return all the repositories in the graph */
repositories(): Repository[] { repositories(): RepositoryReference[] {
return this.graph return this.graph
.nodes({type: REPOSITORY_NODE_TYPE}) .nodes({type: REPOSITORY_NODE_TYPE})
.map((n) => new Repository(this.graph, n.address)); .map(
(n) => new RepositoryReference(new NodeReference(this.graph, n.address))
);
} }
/* Return the repository with the given owner and name */ /* Return the repository with the given owner and name */
repository(owner: string, name: string): Repository { repository(owner: string, name: string): ?RepositoryReference {
const repo = this.repositories().filter( for (const repo of this.repositories()) {
(r) => r.owner() === owner && r.name() === name const repoNode = repo.get();
); if (
if (repo.length > 1) { repoNode != null &&
repoNode.owner() === owner &&
repoNode.name() === name
) {
return repo;
}
}
}
}
export class GithubReference<+T: NodePayload> extends NodeReference<T> {
constructor(ref: NodeReference<any>) {
const addr = ref.address();
if (addr.pluginName !== PLUGIN_NAME) {
throw new Error( throw new Error(
`Unexpectedly found multiple repositories named ${owner}/${name}` `Wrong plugin name ${addr.pluginName} for GitHub plugin!`
); );
} }
return repo[0]; super(ref.graph(), addr);
}
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;
constructor(graph: Graph<NodePayload, EdgePayload>, nodeAddress: Address) {
this.graph = graph;
this.nodeAddress = nodeAddress;
}
node(): Node<T> {
return (this.graph.node(this.nodeAddress): Node<any>);
}
url(): string {
return this.node().payload.url;
} }
type(): NodeType { type(): NodeType {
return (this.nodeAddress.type: any); return ((super.type(): string): any);
} }
address(): Address { get(): ?GithubPorcelain<T> {
return this.nodeAddress; const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new GithubPorcelain(nodePorcelain);
}
} }
} }
export class Repository extends GithubEntity<RepositoryNodePayload> { export class GithubPorcelain<+T: NodePayload> extends NodePorcelain<T> {
static from(e: Entity): Repository { constructor(nodePorcelain: NodePorcelain<any>) {
assertEntityType(e, REPOSITORY_NODE_TYPE); if (nodePorcelain.ref().address().pluginName !== PLUGIN_NAME) {
return (e: any); throw new Error(
`Wrong plugin name ${
nodePorcelain.ref().address().pluginName
} for GitHub plugin!`
);
}
super(nodePorcelain.ref(), nodePorcelain.node());
} }
issueByNumber(number: number): ?Issue { url(): string {
for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, { return this.payload().url;
}
}
export class RepositoryReference extends GithubReference<
RepositoryNodePayload
> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), REPOSITORY_NODE_TYPE);
}
issueByNumber(number: number): ?IssueReference {
const neighbors = this.neighbors({
edgeType: CONTAINS_EDGE_TYPE, edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT", direction: "OUT",
nodeType: ISSUE_NODE_TYPE, nodeType: ISSUE_NODE_TYPE,
})) { });
const node = this.graph.node(neighbor); for (const {ref} of neighbors) {
if (node.payload.number === number) { const issueRef = new IssueReference(ref);
return new Issue(this.graph, neighbor); const node = issueRef.get();
if (node != null && node.number() === number) {
return issueRef;
} }
} }
} }
pullRequestByNumber(number: number): ?PullRequest { pullRequestByNumber(number: number): ?PullRequestReference {
for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, { const neighbors = this.neighbors({
edgeType: CONTAINS_EDGE_TYPE, edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT", direction: "OUT",
nodeType: PULL_REQUEST_NODE_TYPE, nodeType: PULL_REQUEST_NODE_TYPE,
})) { });
const node = this.graph.node(neighbor); for (const {ref} of neighbors) {
if (node.payload.number === number) { const pullRequest = new PullRequestReference(ref);
return new PullRequest(this.graph, neighbor); const node = pullRequest.get();
if (node != null && node.number() === number) {
return pullRequest;
} }
} }
} }
issues(): IssueReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT",
nodeType: ISSUE_NODE_TYPE,
}).map(({ref}) => new IssueReference(ref));
}
pullRequests(): PullRequestReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT",
nodeType: PULL_REQUEST_NODE_TYPE,
}).map(({ref}) => new PullRequestReference(ref));
}
get(): ?RepositoryPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new RepositoryPorcelain(nodePorcelain);
}
}
}
export class RepositoryPorcelain extends GithubPorcelain<
RepositoryNodePayload
> {
constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(nodePorcelain.ref().address(), REPOSITORY_NODE_TYPE);
super(nodePorcelain);
}
owner(): string { owner(): string {
return this.node().payload.owner; return this.payload().owner;
} }
name(): string { name(): string {
return this.node().payload.name; return this.payload().name;
} }
issues(): Issue[] { ref(): RepositoryReference {
return this.graph return new RepositoryReference(super.ref());
.neighborhood(this.nodeAddress, {
direction: "OUT",
edgeType: CONTAINS_EDGE_TYPE,
nodeType: ISSUE_NODE_TYPE,
})
.map(({neighbor}) => new Issue(this.graph, neighbor));
}
pullRequests(): PullRequest[] {
return this.graph
.neighborhood(this.nodeAddress, {
direction: "OUT",
edgeType: CONTAINS_EDGE_TYPE,
nodeType: PULL_REQUEST_NODE_TYPE,
})
.map(({neighbor}) => new PullRequest(this.graph, neighbor));
} }
} }
class Post< class PostReference<
T: T:
| IssueNodePayload | IssueNodePayload
| PullRequestNodePayload | PullRequestNodePayload
| CommentNodePayload | CommentNodePayload
| PullRequestReviewNodePayload | PullRequestReviewNodePayload
| PullRequestReviewCommentNodePayload | PullRequestReviewCommentNodePayload
> extends GithubEntity<T> { > extends GithubReference<T> {
authors(): Author[] { authors(): AuthorReference[] {
return this.graph return this.neighbors({
.neighborhood(this.nodeAddress, {
edgeType: AUTHORS_EDGE_TYPE, edgeType: AUTHORS_EDGE_TYPE,
nodeType: AUTHOR_NODE_TYPE, nodeType: AUTHOR_NODE_TYPE,
}) }).map(({ref}) => new AuthorReference(ref));
.map(({neighbor}) => new Author(this.graph, neighbor));
} }
body(): string { references(): GithubReference<NodePayload>[] {
return this.node().payload.body; return this.neighbors({
}
references(): Entity[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: REFERENCES_EDGE_TYPE, edgeType: REFERENCES_EDGE_TYPE,
direction: "OUT", direction: "OUT",
}) }).map(({ref}) => asGithubReference(ref));
.map(({neighbor}) => asEntity(this.graph, neighbor));
} }
} }
class Commentable<T: IssueNodePayload | PullRequestNodePayload> extends Post< class PostPorcelain<
T T:
> { | IssueNodePayload
comments(): Comment[] { | PullRequestNodePayload
return this.graph | CommentNodePayload
.neighborhood(this.nodeAddress, { | PullRequestReviewNodePayload
| PullRequestReviewCommentNodePayload
> extends GithubPorcelain<T> {
body(): string {
return this.payload().body;
}
}
class CommentableReference<
T: IssueNodePayload | PullRequestNodePayload
> extends PostReference<T> {
comments(): CommentReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE, edgeType: CONTAINS_EDGE_TYPE,
nodeType: COMMENT_NODE_TYPE, nodeType: COMMENT_NODE_TYPE,
}) direction: "OUT",
.map(({neighbor}) => new Comment(this.graph, neighbor)); }).map(({ref}) => new CommentReference(ref));
} }
} }
export class Author extends GithubEntity<AuthorNodePayload> { export class AuthorReference extends GithubReference<AuthorNodePayload> {
static from(e: Entity): Author { constructor(ref: NodeReference<any>) {
assertEntityType(e, AUTHOR_NODE_TYPE); super(ref);
return (e: any); assertAddressType(ref.address(), AUTHOR_NODE_TYPE);
} }
get(): ?AuthorPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new AuthorPorcelain(nodePorcelain);
}
}
}
export class AuthorPorcelain extends GithubPorcelain<AuthorNodePayload> {
constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(nodePorcelain.ref().address(), AUTHOR_NODE_TYPE);
super(nodePorcelain);
}
login(): string { login(): string {
return this.node().payload.login; return this.payload().login;
} }
subtype(): AuthorSubtype { subtype(): AuthorSubtype {
return this.node().payload.subtype; return this.payload().subtype;
}
ref(): AuthorReference {
return new AuthorReference(super.ref());
} }
} }
export class PullRequest extends Commentable<PullRequestNodePayload> { export class PullRequestReference extends CommentableReference<
static from(e: Entity): PullRequest { PullRequestNodePayload
assertEntityType(e, PULL_REQUEST_NODE_TYPE); > {
return (e: any); constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), PULL_REQUEST_NODE_TYPE);
} }
number(): number { parent(): RepositoryReference {
return this.node().payload.number;
}
title(): string {
return this.node().payload.title;
}
reviews(): PullRequestReview[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: CONTAINS_EDGE_TYPE,
nodeType: PULL_REQUEST_REVIEW_NODE_TYPE,
})
.map(({neighbor}) => new PullRequestReview(this.graph, neighbor));
}
parent(): Repository {
return (_parent(this): any); return (_parent(this): any);
} }
reviews(): PullRequestReviewReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
nodeType: PULL_REQUEST_REVIEW_NODE_TYPE,
direction: "OUT",
}).map(({ref}) => new PullRequestReviewReference(ref));
}
mergeCommitHash(): ?string { mergeCommitHash(): ?string {
const mergeEdge = this.graph const mergeEdge = this.neighbors({
.neighborhood(this.nodeAddress, {
edgeType: MERGED_AS_EDGE_TYPE, edgeType: MERGED_AS_EDGE_TYPE,
nodeType: COMMIT_NODE_TYPE, nodeType: COMMIT_NODE_TYPE,
direction: "OUT", direction: "OUT",
}) }).map(({edge}) => edge);
.map(({edge}) => edge);
if (mergeEdge.length > 1) { if (mergeEdge.length > 1) {
throw new Error( throw new Error(
`Node at ${this.nodeAddress.id} has too many MERGED_AS edges` `Node at ${stringify(this.address())} has too many MERGED_AS edges`
); );
} }
if (mergeEdge.length === 0) { if (mergeEdge.length === 0) {
@ -333,81 +369,188 @@ export class PullRequest extends Commentable<PullRequestNodePayload> {
const payload: MergedAsEdgePayload = (mergeEdge[0].payload: any); const payload: MergedAsEdgePayload = (mergeEdge[0].payload: any);
return payload.hash; return payload.hash;
} }
get(): ?PullRequestPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new PullRequestPorcelain(nodePorcelain);
}
}
} }
export class Issue extends Commentable<IssueNodePayload> { export class PullRequestPorcelain extends PostPorcelain<
static from(e: Entity): Issue { PullRequestNodePayload
assertEntityType(e, ISSUE_NODE_TYPE); > {
return (e: any); constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(nodePorcelain.ref().address(), PULL_REQUEST_NODE_TYPE);
super(nodePorcelain);
} }
number(): number { number(): number {
return this.node().payload.number; return this.payload().number;
} }
title(): string { title(): string {
return this.node().payload.title; return this.payload().title;
} }
parent(): Repository { ref(): PullRequestReference {
return new PullRequestReference(super.ref());
}
}
export class IssueReference extends CommentableReference<IssueNodePayload> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), ISSUE_NODE_TYPE);
}
parent(): RepositoryReference {
return (_parent(this): any); return (_parent(this): any);
} }
get(): ?IssuePorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new IssuePorcelain(nodePorcelain);
}
}
} }
export class Comment extends Post<CommentNodePayload> { export class IssuePorcelain extends PostPorcelain<IssueNodePayload> {
static from(e: Entity): Comment { constructor(nodePorcelain: NodePorcelain<any>) {
assertEntityType(e, COMMENT_NODE_TYPE); assertAddressType(nodePorcelain.ref().address(), ISSUE_NODE_TYPE);
return (e: any); super(nodePorcelain);
}
number(): number {
return this.payload().number;
} }
parent(): Issue | PullRequest { title(): string {
return this.payload().title;
}
ref(): IssueReference {
return new IssueReference(super.ref());
}
}
export class CommentReference extends PostReference<CommentNodePayload> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), COMMENT_NODE_TYPE);
}
parent(): IssueReference | PullRequestReference {
return (_parent(this): any); return (_parent(this): any);
} }
get(): ?CommentPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new CommentPorcelain(nodePorcelain);
}
}
} }
export class PullRequestReview extends Post<PullRequestReviewNodePayload> { export class CommentPorcelain extends PostPorcelain<CommentNodePayload> {
static from(e: Entity): PullRequestReview { constructor(nodePorcelain: NodePorcelain<any>) {
assertEntityType(e, PULL_REQUEST_REVIEW_NODE_TYPE); assertAddressType(nodePorcelain.ref().address(), COMMENT_NODE_TYPE);
return (e: any); super(nodePorcelain);
}
ref(): CommentReference {
return new CommentReference(super.ref());
}
}
export class PullRequestReviewReference extends PostReference<
PullRequestReviewNodePayload
> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), PULL_REQUEST_REVIEW_NODE_TYPE);
}
parent(): PullRequestReference {
return (_parent(this): any);
}
comments(): PullRequestReviewCommentReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
nodeType: PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE,
direction: "OUT",
}).map(({ref}) => new PullRequestReviewCommentReference(ref));
}
get(): ?PullRequestReviewPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new PullRequestReviewPorcelain(nodePorcelain);
}
}
}
export class PullRequestReviewPorcelain extends PostPorcelain<
PullRequestReviewNodePayload
> {
constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(
nodePorcelain.ref().address(),
PULL_REQUEST_REVIEW_NODE_TYPE
);
super(nodePorcelain);
} }
state(): PullRequestReviewState { state(): PullRequestReviewState {
return this.node().payload.state; return this.payload().state;
} }
parent(): PullRequest { ref(): PullRequestReviewReference {
return (_parent(this): any); return new PullRequestReviewReference(super.ref());
}
comments(): PullRequestReviewComment[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: CONTAINS_EDGE_TYPE,
nodeType: PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE,
})
.map(({neighbor}) => new PullRequestReviewComment(this.graph, neighbor));
} }
} }
export class PullRequestReviewComment extends Post< export class PullRequestReviewCommentReference extends PostReference<
PullRequestReviewCommentNodePayload PullRequestReviewCommentNodePayload
> { > {
static from(e: Entity): PullRequestReviewComment { constructor(ref: NodeReference<any>) {
assertEntityType(e, PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE); super(ref);
return (e: any); assertAddressType(ref.address(), PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE);
} }
parent(): PullRequestReview {
parent(): PullRequestReviewReference {
return (_parent(this): any); return (_parent(this): any);
} }
get(): ?PullRequestReviewCommentPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new PullRequestReviewCommentPorcelain(nodePorcelain);
}
}
} }
function _parent(x: Entity): Entity { export class PullRequestReviewCommentPorcelain extends PostPorcelain<
const parents = x.graph.neighborhood(x.address(), { PullRequestReviewCommentNodePayload
edgeType: "CONTAINS", > {
direction: "IN", constructor(nodePorcelain: NodePorcelain<any>) {
}); assertAddressType(
nodePorcelain.ref().address(),
PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE
);
super(nodePorcelain);
}
ref(): PullRequestReviewCommentReference {
return new PullRequestReviewCommentReference(super.ref());
}
}
function _parent(
x: GithubReference<NodePayload>
): GithubReference<NodePayload> {
const parents = x.neighbors({edgeType: CONTAINS_EDGE_TYPE, direction: "IN"});
if (parents.length !== 1) { if (parents.length !== 1) {
throw new Error(`Bad parent relationships for ${stringify(x.address())}`); throw new Error(`Bad parent relationships for ${stringify(x.address())}`);
} }
return asEntity(x.graph, parents[0].neighbor); return asGithubReference(parents[0].ref);
} }

View File

@ -1,20 +1,26 @@
// @flow // @flow
import type {Address} from "../../core/address";
import {parse} from "./parser"; import {parse} from "./parser";
import exampleRepoData from "./demoData/example-github.json"; import exampleRepoData from "./demoData/example-github.json";
import type {Entity} from "./porcelain";
import { import {
asEntity, AuthorReference,
Porcelain, AuthorPorcelain,
Repository, CommentReference,
Issue, CommentPorcelain,
PullRequest, GithubReference,
PullRequestReview, GraphPorcelain,
PullRequestReviewComment, IssueReference,
Comment, IssuePorcelain,
Author, PullRequestReference,
PullRequestPorcelain,
PullRequestReviewReference,
PullRequestReviewPorcelain,
PullRequestReviewCommentReference,
PullRequestReviewCommentPorcelain,
RepositoryReference,
RepositoryPorcelain,
} from "./porcelain"; } from "./porcelain";
import type {NodePayload} from "./types";
import { import {
AUTHOR_NODE_TYPE, AUTHOR_NODE_TYPE,
COMMENT_NODE_TYPE, COMMENT_NODE_TYPE,
@ -26,14 +32,19 @@ import {
import {nodeDescription} from "./render"; import {nodeDescription} from "./render";
import {PLUGIN_NAME} from "./pluginName";
describe("GitHub porcelain", () => { describe("GitHub porcelain", () => {
const graph = parse(exampleRepoData); const graph = parse(exampleRepoData);
const porcelain = new Porcelain(graph); const porcelain = new GraphPorcelain(graph);
const repo = porcelain.repository("sourcecred", "example-github"); const repoRef = porcelain.repository("sourcecred", "example-github");
if (repoRef == null) {
throw new Error("Where did the repository go?");
}
const repo = repoRef.get();
if (repo == null) {
throw new Error("Where did the repository go?");
}
function expectPropertiesToMatchSnapshot<T: Entity>( function expectPropertiesToMatchSnapshot<T: {+url: () => string}>(
entities: $ReadOnlyArray<T>, entities: $ReadOnlyArray<T>,
extractor: (T) => mixed extractor: (T) => mixed
) { ) {
@ -47,36 +58,75 @@ describe("GitHub porcelain", () => {
expect(urlToProperty).toMatchSnapshot(); expect(urlToProperty).toMatchSnapshot();
} }
function issueByNumber(n: number): Issue { function issueByNumber(n: number): IssuePorcelain {
const result = repo.issueByNumber(n); const ref = repo.ref().issueByNumber(n);
if (ref == null) {
throw new Error(`Expected issue #${n} to exist`);
}
const result = ref.get();
if (result == null) { if (result == null) {
throw new Error(`Expected Issue #${n} to exist`); throw new Error(`Expected issue #${n} to exist`);
} }
return result; return result;
} }
function prByNumber(n: number): PullRequest { function prByNumber(n: number): PullRequestPorcelain {
const result = repo.pullRequestByNumber(n); const ref = repo.ref().pullRequestByNumber(n);
if (ref == null) {
throw new Error(`Expected pull request #${n} to exist`);
}
const result = ref.get();
if (result == null) { if (result == null) {
throw new Error(`Expected PR #${n} to exist`); throw new Error(`Expected pull request #${n} to exist`);
} }
return result; return result;
} }
function issueOrPrByNumber(n: number): Issue | PullRequest { function issueOrPrByNumber(n: number): IssuePorcelain | PullRequestPorcelain {
const result = repo.issueByNumber(n) || repo.pullRequestByNumber(n); const ref =
repo.ref().issueByNumber(n) || repo.ref().pullRequestByNumber(n);
if (ref == null) {
throw new Error(`Expected Issue/PR #${n} to exist`);
}
const result = ref.get();
if (result == null) { if (result == null) {
throw new Error(`Expected Issue/PR #${n} to exist`); throw new Error(`Expected Issue/PR #${n} to exist`);
} }
return result; return result;
} }
const issue = issueByNumber(2); function really<T>(x: ?T): T {
const comment = issue.comments()[0]; if (x == null) {
const pullRequest = prByNumber(5); throw new Error(String(x));
const pullRequestReview = pullRequest.reviews()[0]; }
const pullRequestReviewComment = pullRequestReview.comments()[0]; return x;
const author = issue.authors()[0]; }
const issue = really(issueByNumber(2));
const comment = really(
issue
.ref()
.comments()[0]
.get()
);
const pullRequest = really(prByNumber(5));
const pullRequestReview = really(
pullRequest
.ref()
.reviews()[0]
.get()
);
const pullRequestReviewComment = really(
pullRequestReview
.ref()
.comments()[0]
.get()
);
const author = really(
issue
.ref()
.authors()[0]
.get()
);
const allWrappers = [ const allWrappers = [
issue, issue,
pullRequest, pullRequest,
@ -87,62 +137,43 @@ describe("GitHub porcelain", () => {
]; ];
it("all wrappers provide a type() method", () => { it("all wrappers provide a type() method", () => {
expectPropertiesToMatchSnapshot(allWrappers, (e) => e.type()); expectPropertiesToMatchSnapshot(allWrappers, (e) => e.ref().type());
}); });
it("all wrappers provide a url() method", () => { it("all wrappers provide a url() method", () => {
expectPropertiesToMatchSnapshot(allWrappers, (e) => e.url()); expectPropertiesToMatchSnapshot(allWrappers, (e) => e.url());
}); });
it("all wrappers provide an address() method", () => { test("reference constructors throw errors when used incorrectly", () => {
allWrappers.forEach((w) => { expect(() => new RepositoryReference(issue.ref())).toThrowError(
const addr = w.address();
const url = w.url();
const type = w.type();
expect(addr.id).toBe(url);
expect(addr.type).toBe(type);
expect(addr.pluginName).toBe(PLUGIN_NAME);
});
});
it("all wrappers provide a node() method", () => {
allWrappers.forEach((w) => {
const node = w.node();
const addr = w.address();
expect(node.address).toEqual(addr);
});
});
describe("type verifiers", () => {
it("are provided by all wrappers", () => {
// Check each one individually to verify the flowtypes
const _unused_repo: Repository = Repository.from(repo);
const _unused_issue: Issue = Issue.from(issue);
const _unused_pullRequest: PullRequest = PullRequest.from(pullRequest);
const _unused_comment: Comment = Comment.from(comment);
const _unused_pullRequestReview: PullRequestReview = PullRequestReview.from(
pullRequestReview
);
const _unused_pullRequestReviewComment: PullRequestReviewComment = PullRequestReviewComment.from(
pullRequestReviewComment
);
const _unused_author: Author = Author.from(author);
// Check them programatically so that if we add another wrapper, we can't forget to update.
allWrappers.forEach((e) => {
expect(e.constructor.from(e)).toEqual(e);
});
});
it("and errors are thrown when used incorrectly", () => {
expect(() => Repository.from(issue)).toThrowError("to have type");
expect(() => Issue.from(repo)).toThrowError("to have type");
expect(() => Comment.from(repo)).toThrowError("to have type");
expect(() => PullRequest.from(repo)).toThrowError("to have type");
expect(() => PullRequestReview.from(repo)).toThrowError("to have type");
expect(() => PullRequestReviewComment.from(repo)).toThrowError(
"to have type" "to have type"
); );
expect(() => Author.from(repo)).toThrowError("to have type"); expect(() => new IssueReference(repo.ref())).toThrowError("to have type");
expect(() => new CommentReference(repo.ref())).toThrowError("to have type");
expect(() => new PullRequestReference(repo.ref())).toThrowError(
"to have type"
);
expect(() => new PullRequestReviewReference(repo.ref())).toThrowError(
"to have type"
);
expect(
() => new PullRequestReviewCommentReference(repo.ref())
).toThrowError("to have type");
expect(() => new AuthorReference(repo.ref())).toThrowError("to have type");
}); });
test("porcelain constructors throw errors when used incorrectly", () => {
expect(() => new RepositoryPorcelain(issue)).toThrowError("to have type");
expect(() => new IssuePorcelain(repo)).toThrowError("to have type");
expect(() => new CommentPorcelain(repo)).toThrowError("to have type");
expect(() => new PullRequestPorcelain(repo)).toThrowError("to have type");
expect(() => new PullRequestReviewPorcelain(repo)).toThrowError(
"to have type"
);
expect(() => new PullRequestReviewCommentPorcelain(repo)).toThrowError(
"to have type"
);
expect(() => new AuthorPorcelain(repo)).toThrowError("to have type");
}); });
describe("posts", () => { describe("posts", () => {
@ -154,19 +185,32 @@ describe("GitHub porcelain", () => {
comment, comment,
]; ];
it("have parents", () => { it("have parents", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => e.parent().url()); expectPropertiesToMatchSnapshot(allPosts, (e) =>
really(
e
.ref()
.parent()
.get()
).url()
);
}); });
it("have bodies", () => { it("have bodies", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => e.body()); expectPropertiesToMatchSnapshot(allPosts, (e) => e.body());
}); });
it("have authors", () => { it("have authors", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => expectPropertiesToMatchSnapshot(allPosts, (e) =>
e.authors().map((a) => a.login()) e
.ref()
.authors()
.map((a) => really(a.get()).login())
); );
}); });
it("have references", () => { it("have references", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => expectPropertiesToMatchSnapshot(allPosts, (e) =>
e.references().map((r) => r.url()) e
.ref()
.references()
.map((r: GithubReference<NodePayload>) => really(r.get()).url())
); );
}); });
}); });
@ -183,7 +227,10 @@ describe("GitHub porcelain", () => {
}); });
it("have comments", () => { it("have comments", () => {
expectPropertiesToMatchSnapshot(issuesAndPRs, (e) => expectPropertiesToMatchSnapshot(issuesAndPRs, (e) =>
e.comments().map((c) => c.url()) e
.ref()
.comments()
.map((c) => really(c.get()).url())
); );
}); });
}); });
@ -191,49 +238,36 @@ describe("GitHub porcelain", () => {
describe("pull requests", () => { describe("pull requests", () => {
const prs = [prByNumber(3), prByNumber(5), prByNumber(9)]; const prs = [prByNumber(3), prByNumber(5), prByNumber(9)];
it("have mergeCommitHashes", () => { it("have mergeCommitHashes", () => {
expectPropertiesToMatchSnapshot(prs, (e) => e.mergeCommitHash()); expectPropertiesToMatchSnapshot(prs, (e) => e.ref().mergeCommitHash());
}); });
it("have reviews", () => { it("have reviews", () => {
expectPropertiesToMatchSnapshot(prs, (e) => expectPropertiesToMatchSnapshot(prs, (e) =>
e.reviews().map((r) => r.url()) e
.ref()
.reviews()
.map((r) => really(r.get()).url())
); );
}); });
}); });
describe("pull request reviews", () => { describe("pull request reviews", () => {
const reviews = pullRequest.reviews(); const reviews = pullRequest.ref().reviews();
it("have review comments", () => { it("have review comments", () => {
expectPropertiesToMatchSnapshot(reviews, (e) => expectPropertiesToMatchSnapshot(
e.comments().map((e) => e.url()) reviews.map((r) => really(r.get())),
(e) =>
e
.ref()
.comments()
.map((e) => really(e.get()).url())
); );
}); });
it("have states", () => { it("have states", () => {
expectPropertiesToMatchSnapshot(reviews, (e) => e.state()); expectPropertiesToMatchSnapshot(
}); reviews.map((r) => really(r.get())),
}); (e) => e.state()
);
describe("asEntity", () => {
it("works for each wrapper", () => {
allWrappers.forEach((w) => {
expect(asEntity(w.graph, w.address())).toEqual(w);
});
});
it("errors when given an address with the wrong plugin name", () => {
const addr: Address = {
pluginName: "the magnificent foo plugin",
id: "who are you to ask an id of the magnificent foo plugin?",
type: "ISSUE",
};
expect(() => asEntity(graph, addr)).toThrow("wrong plugin name");
});
it("errors when given an address with a bad node type", () => {
const addr: Address = {
pluginName: PLUGIN_NAME,
id: "if you keep asking for my id you will make me angry",
type: "the foo plugin's magnificence extends to many plugins",
};
expect(() => asEntity(graph, addr)).toThrow("invalid type");
}); });
}); });
@ -252,22 +286,24 @@ describe("GitHub porcelain", () => {
describe("References", () => { describe("References", () => {
it("via #-number", () => { it("via #-number", () => {
const srcIssue = issueByNumber(2); const srcIssue = issueByNumber(2);
const references = srcIssue.references(); const references = srcIssue.ref().references();
expect(references).toHaveLength(1); expect(references).toHaveLength(1);
// Note: this verifies that we are not counting in-references, as // Note: this verifies that we are not counting in-references, as
// https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316 // https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316
// references #2. // references #2.
const referenced = Issue.from(references[0]); const referenced = new IssuePorcelain(really(references[0].get()));
expect(referenced.number()).toBe(1); expect(referenced.number()).toBe(1);
}); });
describe("by exact url", () => { describe("by exact url", () => {
function expectCommentToHaveSingleReference({commentNumber, type, url}) { function expectCommentToHaveSingleReference({commentNumber, type, url}) {
const comments = issueByNumber(2).comments(); const comments = issueByNumber(2)
.ref()
.comments();
const references = comments[commentNumber].references(); const references = comments[commentNumber].references();
expect(references).toHaveLength(1); expect(references).toHaveLength(1);
expect(references[0].url()).toBe(url); expect(really(references[0].get()).url()).toBe(url);
expect(references[0].type()).toBe(type); expect(references[0].type()).toBe(type);
} }
@ -324,6 +360,7 @@ describe("GitHub porcelain", () => {
it("to multiple entities", () => { it("to multiple entities", () => {
const references = issueByNumber(2) const references = issueByNumber(2)
.ref()
.comments()[6] .comments()[6]
.references(); .references();
expect(references).toHaveLength(5); expect(references).toHaveLength(5);
@ -331,6 +368,7 @@ describe("GitHub porcelain", () => {
it("to no entities", () => { it("to no entities", () => {
const references = issueByNumber(2) const references = issueByNumber(2)
.ref()
.comments()[7] .comments()[7]
.references(); .references();
expect(references).toHaveLength(0); expect(references).toHaveLength(0);
@ -339,10 +377,11 @@ describe("GitHub porcelain", () => {
it("References by @-author", () => { it("References by @-author", () => {
const pr = prByNumber(5); const pr = prByNumber(5);
const references = pr.references(); const references = pr.ref().references();
expect(references).toHaveLength(1); expect(references).toHaveLength(1);
const referenced = Author.from(references[0]); const referenced = new AuthorReference(references[0]);
expect(referenced.login()).toBe("wchargin"); const login = really(referenced.get()).login();
expect(login).toBe("wchargin");
}); });
}); });
@ -352,7 +391,7 @@ describe("GitHub porcelain", () => {
// file, and move this test to render.test.js (assuming we don't move the // file, and move this test to render.test.js (assuming we don't move the
// description method into the porcelain anyway...) // description method into the porcelain anyway...)
expectPropertiesToMatchSnapshot(allWrappers, (e) => expectPropertiesToMatchSnapshot(allWrappers, (e) =>
nodeDescription(e.graph, e.address()) nodeDescription(e.ref())
); );
}); });
}); });

View File

@ -4,64 +4,82 @@
* Methods for rendering and displaying GitHub nodes. * Methods for rendering and displaying GitHub nodes.
*/ */
import stringify from "json-stable-stringify"; import stringify from "json-stable-stringify";
import {Graph} from "../../core/graph"; import type {NodeReference} from "../../core/porcelain";
import type {Address} from "../../core/address"; import type {NodePayload} from "./types";
import { import {
asEntity, GithubReference,
Issue, AuthorReference,
PullRequest, AuthorPorcelain,
Comment, CommentReference,
PullRequestReview, IssueReference,
PullRequestReviewComment, IssuePorcelain,
Author, PullRequestReference,
Repository, PullRequestPorcelain,
PullRequestReviewReference,
PullRequestReviewCommentReference,
RepositoryPorcelain,
} from "./porcelain"; } from "./porcelain";
/* Give a short description for the GitHub node at given address. /* Give a short description for the GitHub node at given address.
* Useful for e.g. displaying a title. * Useful for e.g. displaying a title.
*/ */
export function nodeDescription(graph: Graph<any, any>, addr: Address) { export function nodeDescription(ref: NodeReference<NodePayload>) {
const entity = asEntity(graph, addr); const porcelain = ref.get();
const type = entity.type(); if (porcelain == null) {
return `[unknown ${ref.type()}]`;
}
const type = new GithubReference(ref).type();
switch (type) { switch (type) {
case "REPOSITORY": { case "REPOSITORY": {
const repo = Repository.from(entity); const repo = new RepositoryPorcelain(porcelain);
return `${repo.owner()}/${repo.name()}`; return `${repo.owner()}/${repo.name()}`;
} }
case "ISSUE": { case "ISSUE": {
const issue = Issue.from(entity); const issue = new IssuePorcelain(porcelain);
return `#${issue.number()}: ${issue.title()}`; return `#${issue.number()}: ${issue.title()}`;
} }
case "PULL_REQUEST": { case "PULL_REQUEST": {
const pr = PullRequest.from(entity); const pr = new PullRequestPorcelain(porcelain);
return `#${pr.number()}: ${pr.title()}`; return `#${pr.number()}: ${pr.title()}`;
} }
case "COMMENT": { case "COMMENT": {
const comment = Comment.from(entity); const comment = new CommentReference(ref);
const author = comment.authors()[0]; const issue = comment.parent();
return `comment by @${author.login()} on #${comment.parent().number()}`; return `comment by @${authors(comment)} on #${num(issue)}`;
} }
case "PULL_REQUEST_REVIEW": { case "PULL_REQUEST_REVIEW": {
const review = PullRequestReview.from(entity); const review = new PullRequestReviewReference(ref);
const author = review.authors()[0]; const pr = review.parent();
return `review by @${author.login()} on #${review.parent().number()}`; return `review by @${authors(review)} on #${num(pr)}`;
} }
case "PULL_REQUEST_REVIEW_COMMENT": { case "PULL_REQUEST_REVIEW_COMMENT": {
const comment = PullRequestReviewComment.from(entity); const comment = new PullRequestReviewCommentReference(ref);
const author = comment.authors()[0];
const pr = comment.parent().parent(); const pr = comment.parent().parent();
return `review comment by @${author.login()} on #${pr.number()}`; return `review comment by @${authors(comment)} on #${num(pr)}`;
} }
case "AUTHOR": { case "AUTHOR": {
const author = Author.from(entity); return `@${new AuthorPorcelain(porcelain).login()}`;
return `@${author.login()}`;
} }
default: { default: {
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
(type: empty); (type: empty);
throw new Error( throw new Error(
`Tried to write description for invalid type ${stringify(addr)}` `Tried to write description for invalid type ${stringify(
ref.address()
)}`
); );
} }
} }
} }
function num(x: IssueReference | PullRequestReference) {
const np = x.get();
return np == null ? "[unknown]" : np.number();
}
function authors(authorable: {+authors: () => AuthorReference[]}) {
// TODO: modify to accomodate multi-authorship
const authorRefs = authorable.authors();
const firstAuthor = authorRefs[0].get();
return firstAuthor != null ? firstAuthor.login() : "[unknown]";
}