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:
parent
7ccef98c87
commit
bb77c36626
|
@ -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>
|
||||||
|
|
|
@ -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 &&
|
||||||
throw new Error(
|
repoNode.owner() === owner &&
|
||||||
`Unexpectedly found multiple repositories named ${owner}/${name}`
|
repoNode.name() === name
|
||||||
);
|
) {
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return repo[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
authors(): Author[] {
|
|
||||||
return this.graph
|
|
||||||
.nodes({type: AUTHOR_NODE_TYPE})
|
|
||||||
.map((n) => new Author(this.graph, n.address));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GithubEntity<T: NodePayload> {
|
export class GithubReference<+T: NodePayload> extends NodeReference<T> {
|
||||||
graph: Graph<NodePayload, EdgePayload>;
|
constructor(ref: NodeReference<any>) {
|
||||||
nodeAddress: Address;
|
const addr = ref.address();
|
||||||
|
if (addr.pluginName !== PLUGIN_NAME) {
|
||||||
constructor(graph: Graph<NodePayload, EdgePayload>, nodeAddress: Address) {
|
throw new Error(
|
||||||
this.graph = graph;
|
`Wrong plugin name ${addr.pluginName} for GitHub plugin!`
|
||||||
this.nodeAddress = nodeAddress;
|
);
|
||||||
}
|
}
|
||||||
|
super(ref.graph(), addr);
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
owner(): string {
|
issues(): IssueReference[] {
|
||||||
return this.node().payload.owner;
|
return this.neighbors({
|
||||||
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
|
direction: "OUT",
|
||||||
|
nodeType: ISSUE_NODE_TYPE,
|
||||||
|
}).map(({ref}) => new IssueReference(ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
name(): string {
|
pullRequests(): PullRequestReference[] {
|
||||||
return this.node().payload.name;
|
return this.neighbors({
|
||||||
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
|
direction: "OUT",
|
||||||
|
nodeType: PULL_REQUEST_NODE_TYPE,
|
||||||
|
}).map(({ref}) => new PullRequestReference(ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
issues(): Issue[] {
|
get(): ?RepositoryPorcelain {
|
||||||
return this.graph
|
const nodePorcelain = super.get();
|
||||||
.neighborhood(this.nodeAddress, {
|
if (nodePorcelain != null) {
|
||||||
direction: "OUT",
|
return new RepositoryPorcelain(nodePorcelain);
|
||||||
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<
|
export class RepositoryPorcelain extends GithubPorcelain<
|
||||||
|
RepositoryNodePayload
|
||||||
|
> {
|
||||||
|
constructor(nodePorcelain: NodePorcelain<any>) {
|
||||||
|
assertAddressType(nodePorcelain.ref().address(), REPOSITORY_NODE_TYPE);
|
||||||
|
super(nodePorcelain);
|
||||||
|
}
|
||||||
|
|
||||||
|
owner(): string {
|
||||||
|
return this.payload().owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
name(): string {
|
||||||
|
return this.payload().name;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref(): RepositoryReference {
|
||||||
|
return new RepositoryReference(super.ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
references(): GithubReference<NodePayload>[] {
|
||||||
|
return this.neighbors({
|
||||||
|
edgeType: REFERENCES_EDGE_TYPE,
|
||||||
|
direction: "OUT",
|
||||||
|
}).map(({ref}) => asGithubReference(ref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostPorcelain<
|
||||||
|
T:
|
||||||
|
| IssueNodePayload
|
||||||
|
| PullRequestNodePayload
|
||||||
|
| CommentNodePayload
|
||||||
|
| PullRequestReviewNodePayload
|
||||||
|
| PullRequestReviewCommentNodePayload
|
||||||
|
> extends GithubPorcelain<T> {
|
||||||
body(): string {
|
body(): string {
|
||||||
return this.node().payload.body;
|
return this.payload().body;
|
||||||
}
|
|
||||||
|
|
||||||
references(): Entity[] {
|
|
||||||
return this.graph
|
|
||||||
.neighborhood(this.nodeAddress, {
|
|
||||||
edgeType: REFERENCES_EDGE_TYPE,
|
|
||||||
direction: "OUT",
|
|
||||||
})
|
|
||||||
.map(({neighbor}) => asEntity(this.graph, neighbor));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Commentable<T: IssueNodePayload | PullRequestNodePayload> extends Post<
|
class CommentableReference<
|
||||||
T
|
T: IssueNodePayload | PullRequestNodePayload
|
||||||
> {
|
> extends PostReference<T> {
|
||||||
comments(): Comment[] {
|
comments(): CommentReference[] {
|
||||||
return this.graph
|
return this.neighbors({
|
||||||
.neighborhood(this.nodeAddress, {
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
edgeType: CONTAINS_EDGE_TYPE,
|
nodeType: COMMENT_NODE_TYPE,
|
||||||
nodeType: COMMENT_NODE_TYPE,
|
direction: "OUT",
|
||||||
})
|
}).map(({ref}) => new CommentReference(ref));
|
||||||
.map(({neighbor}) => new Comment(this.graph, neighbor));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (_parent(this): any);
|
return new PullRequestReference(super.ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Comment extends Post<CommentNodePayload> {
|
export class IssueReference extends CommentableReference<IssueNodePayload> {
|
||||||
static from(e: Entity): Comment {
|
constructor(ref: NodeReference<any>) {
|
||||||
assertEntityType(e, COMMENT_NODE_TYPE);
|
super(ref);
|
||||||
return (e: any);
|
assertAddressType(ref.address(), ISSUE_NODE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
parent(): Issue | PullRequest {
|
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 PullRequestReview extends Post<PullRequestReviewNodePayload> {
|
export class IssuePorcelain extends PostPorcelain<IssueNodePayload> {
|
||||||
static from(e: Entity): PullRequestReview {
|
constructor(nodePorcelain: NodePorcelain<any>) {
|
||||||
assertEntityType(e, PULL_REQUEST_REVIEW_NODE_TYPE);
|
assertAddressType(nodePorcelain.ref().address(), ISSUE_NODE_TYPE);
|
||||||
return (e: any);
|
super(nodePorcelain);
|
||||||
|
}
|
||||||
|
number(): number {
|
||||||
|
return this.payload().number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): ?CommentPorcelain {
|
||||||
|
const nodePorcelain = super.get();
|
||||||
|
if (nodePorcelain != null) {
|
||||||
|
return new CommentPorcelain(nodePorcelain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommentPorcelain extends PostPorcelain<CommentNodePayload> {
|
||||||
|
constructor(nodePorcelain: NodePorcelain<any>) {
|
||||||
|
assertAddressType(nodePorcelain.ref().address(), COMMENT_NODE_TYPE);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
"to have type"
|
||||||
const url = w.url();
|
);
|
||||||
const type = w.type();
|
expect(() => new IssueReference(repo.ref())).toThrowError("to have type");
|
||||||
expect(addr.id).toBe(url);
|
expect(() => new CommentReference(repo.ref())).toThrowError("to have type");
|
||||||
expect(addr.type).toBe(type);
|
expect(() => new PullRequestReference(repo.ref())).toThrowError(
|
||||||
expect(addr.pluginName).toBe(PLUGIN_NAME);
|
"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");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("all wrappers provide a node() method", () => {
|
test("porcelain constructors throw errors when used incorrectly", () => {
|
||||||
allWrappers.forEach((w) => {
|
expect(() => new RepositoryPorcelain(issue)).toThrowError("to have type");
|
||||||
const node = w.node();
|
expect(() => new IssuePorcelain(repo)).toThrowError("to have type");
|
||||||
const addr = w.address();
|
expect(() => new CommentPorcelain(repo)).toThrowError("to have type");
|
||||||
expect(node.address).toEqual(addr);
|
expect(() => new PullRequestPorcelain(repo)).toThrowError("to have type");
|
||||||
});
|
expect(() => new PullRequestReviewPorcelain(repo)).toThrowError(
|
||||||
});
|
"to have type"
|
||||||
|
);
|
||||||
describe("type verifiers", () => {
|
expect(() => new PullRequestReviewCommentPorcelain(repo)).toThrowError(
|
||||||
it("are provided by all wrappers", () => {
|
"to have type"
|
||||||
// Check each one individually to verify the flowtypes
|
);
|
||||||
const _unused_repo: Repository = Repository.from(repo);
|
expect(() => new AuthorPorcelain(repo)).toThrowError("to have type");
|
||||||
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"
|
|
||||||
);
|
|
||||||
expect(() => Author.from(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())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]";
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue