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

View File

@ -15,9 +15,9 @@
*/
import stringify from "json-stable-stringify";
import {Graph} from "../../core/graph";
import type {Node} from "../../core/graph";
import type {Address} from "../../core/address";
import {Graph} from "../../core/graph";
import {NodeReference, NodePorcelain} from "../../core/porcelain";
import type {
AuthorNodePayload,
AuthorSubtype,
@ -44,36 +44,26 @@ import {
PULL_REQUEST_NODE_TYPE,
PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE,
PULL_REQUEST_REVIEW_NODE_TYPE,
REPOSITORY_NODE_TYPE,
REFERENCES_EDGE_TYPE,
REPOSITORY_NODE_TYPE,
} from "./types";
import {PLUGIN_NAME} from "./pluginName";
import {COMMIT_NODE_TYPE} from "../git/types";
export type Entity =
| Repository
| Issue
| PullRequest
| Comment
| Author
| PullRequestReview
| PullRequestReviewComment;
function assertEntityType(e: Entity, t: NodeType) {
if (e.type() !== t) {
function assertAddressType(address: Address, t: NodeType) {
if (address.type !== t) {
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(
g: Graph<NodePayload, EdgePayload>,
addr: Address
): Entity {
const type: NodeType = (addr.type: any);
function asGithubReference(
ref: NodeReference<any>
): GithubReference<NodePayload> {
const addr = ref.address();
if (addr.pluginName !== PLUGIN_NAME) {
throw new Error(
`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) {
case "ISSUE":
return new Issue(g, addr);
return new IssueReference(ref);
case "PULL_REQUEST":
return new PullRequest(g, addr);
return new PullRequestReference(ref);
case "COMMENT":
return new Comment(g, addr);
return new CommentReference(ref);
case "AUTHOR":
return new Author(g, addr);
return new AuthorReference(ref);
case "PULL_REQUEST_REVIEW":
return new PullRequestReview(g, addr);
return new PullRequestReviewReference(ref);
case "PULL_REQUEST_REVIEW_COMMENT":
return new PullRequestReviewComment(g, addr);
return new PullRequestReviewCommentReference(ref);
case "REPOSITORY":
return new Repository(g, addr);
return new RepositoryReference(ref);
default:
// eslint-disable-next-line no-unused-expressions
(type: empty);
@ -107,7 +98,7 @@ export function asEntity(
}
}
export class Porcelain {
export class GraphPorcelain {
graph: Graph<NodePayload, EdgePayload>;
constructor(graph: Graph<NodePayload, EdgePayload>) {
@ -115,216 +106,261 @@ export class Porcelain {
}
/* Return all the repositories in the graph */
repositories(): Repository[] {
repositories(): RepositoryReference[] {
return this.graph
.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 */
repository(owner: string, name: string): Repository {
const repo = this.repositories().filter(
(r) => r.owner() === owner && r.name() === name
);
if (repo.length > 1) {
throw new Error(
`Unexpectedly found multiple repositories named ${owner}/${name}`
);
repository(owner: string, name: string): ?RepositoryReference {
for (const repo of this.repositories()) {
const repoNode = repo.get();
if (
repoNode != null &&
repoNode.owner() === owner &&
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> {
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;
export class GithubReference<+T: NodePayload> extends NodeReference<T> {
constructor(ref: NodeReference<any>) {
const addr = ref.address();
if (addr.pluginName !== PLUGIN_NAME) {
throw new Error(
`Wrong plugin name ${addr.pluginName} for GitHub plugin!`
);
}
super(ref.graph(), addr);
}
type(): NodeType {
return (this.nodeAddress.type: any);
return ((super.type(): string): any);
}
address(): Address {
return this.nodeAddress;
get(): ?GithubPorcelain<T> {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new GithubPorcelain(nodePorcelain);
}
}
}
export class Repository extends GithubEntity<RepositoryNodePayload> {
static from(e: Entity): Repository {
assertEntityType(e, REPOSITORY_NODE_TYPE);
return (e: any);
export class GithubPorcelain<+T: NodePayload> extends NodePorcelain<T> {
constructor(nodePorcelain: NodePorcelain<any>) {
if (nodePorcelain.ref().address().pluginName !== PLUGIN_NAME) {
throw new Error(
`Wrong plugin name ${
nodePorcelain.ref().address().pluginName
} for GitHub plugin!`
);
}
super(nodePorcelain.ref(), nodePorcelain.node());
}
issueByNumber(number: number): ?Issue {
for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, {
url(): string {
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,
direction: "OUT",
nodeType: ISSUE_NODE_TYPE,
})) {
const node = this.graph.node(neighbor);
if (node.payload.number === number) {
return new Issue(this.graph, neighbor);
});
for (const {ref} of neighbors) {
const issueRef = new IssueReference(ref);
const node = issueRef.get();
if (node != null && node.number() === number) {
return issueRef;
}
}
}
pullRequestByNumber(number: number): ?PullRequest {
for (const {neighbor} of this.graph.neighborhood(this.nodeAddress, {
pullRequestByNumber(number: number): ?PullRequestReference {
const neighbors = this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT",
nodeType: PULL_REQUEST_NODE_TYPE,
})) {
const node = this.graph.node(neighbor);
if (node.payload.number === number) {
return new PullRequest(this.graph, neighbor);
});
for (const {ref} of neighbors) {
const pullRequest = new PullRequestReference(ref);
const node = pullRequest.get();
if (node != null && node.number() === number) {
return pullRequest;
}
}
}
owner(): string {
return this.node().payload.owner;
issues(): IssueReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT",
nodeType: ISSUE_NODE_TYPE,
}).map(({ref}) => new IssueReference(ref));
}
name(): string {
return this.node().payload.name;
pullRequests(): PullRequestReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
direction: "OUT",
nodeType: PULL_REQUEST_NODE_TYPE,
}).map(({ref}) => new PullRequestReference(ref));
}
issues(): Issue[] {
return this.graph
.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));
get(): ?RepositoryPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new RepositoryPorcelain(nodePorcelain);
}
}
}
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:
| IssueNodePayload
| PullRequestNodePayload
| CommentNodePayload
| PullRequestReviewNodePayload
| PullRequestReviewCommentNodePayload
> extends GithubEntity<T> {
authors(): Author[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: AUTHORS_EDGE_TYPE,
nodeType: AUTHOR_NODE_TYPE,
})
.map(({neighbor}) => new Author(this.graph, neighbor));
> extends GithubReference<T> {
authors(): AuthorReference[] {
return this.neighbors({
edgeType: AUTHORS_EDGE_TYPE,
nodeType: AUTHOR_NODE_TYPE,
}).map(({ref}) => new AuthorReference(ref));
}
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 {
return this.node().payload.body;
}
references(): Entity[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: REFERENCES_EDGE_TYPE,
direction: "OUT",
})
.map(({neighbor}) => asEntity(this.graph, neighbor));
return this.payload().body;
}
}
class Commentable<T: IssueNodePayload | PullRequestNodePayload> extends Post<
T
> {
comments(): Comment[] {
return this.graph
.neighborhood(this.nodeAddress, {
edgeType: CONTAINS_EDGE_TYPE,
nodeType: COMMENT_NODE_TYPE,
})
.map(({neighbor}) => new Comment(this.graph, neighbor));
class CommentableReference<
T: IssueNodePayload | PullRequestNodePayload
> extends PostReference<T> {
comments(): CommentReference[] {
return this.neighbors({
edgeType: CONTAINS_EDGE_TYPE,
nodeType: COMMENT_NODE_TYPE,
direction: "OUT",
}).map(({ref}) => new CommentReference(ref));
}
}
export class Author extends GithubEntity<AuthorNodePayload> {
static from(e: Entity): Author {
assertEntityType(e, AUTHOR_NODE_TYPE);
return (e: any);
export class AuthorReference extends GithubReference<AuthorNodePayload> {
constructor(ref: NodeReference<any>) {
super(ref);
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 {
return this.node().payload.login;
return this.payload().login;
}
subtype(): AuthorSubtype {
return this.node().payload.subtype;
return this.payload().subtype;
}
ref(): AuthorReference {
return new AuthorReference(super.ref());
}
}
export class PullRequest extends Commentable<PullRequestNodePayload> {
static from(e: Entity): PullRequest {
assertEntityType(e, PULL_REQUEST_NODE_TYPE);
return (e: any);
export class PullRequestReference extends CommentableReference<
PullRequestNodePayload
> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), PULL_REQUEST_NODE_TYPE);
}
number(): number {
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 {
parent(): RepositoryReference {
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 {
const mergeEdge = this.graph
.neighborhood(this.nodeAddress, {
edgeType: MERGED_AS_EDGE_TYPE,
nodeType: COMMIT_NODE_TYPE,
direction: "OUT",
})
.map(({edge}) => edge);
const mergeEdge = this.neighbors({
edgeType: MERGED_AS_EDGE_TYPE,
nodeType: COMMIT_NODE_TYPE,
direction: "OUT",
}).map(({edge}) => edge);
if (mergeEdge.length > 1) {
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) {
@ -333,81 +369,188 @@ export class PullRequest extends Commentable<PullRequestNodePayload> {
const payload: MergedAsEdgePayload = (mergeEdge[0].payload: any);
return payload.hash;
}
get(): ?PullRequestPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new PullRequestPorcelain(nodePorcelain);
}
}
}
export class Issue extends Commentable<IssueNodePayload> {
static from(e: Entity): Issue {
assertEntityType(e, ISSUE_NODE_TYPE);
return (e: any);
export class PullRequestPorcelain extends PostPorcelain<
PullRequestNodePayload
> {
constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(nodePorcelain.ref().address(), PULL_REQUEST_NODE_TYPE);
super(nodePorcelain);
}
number(): number {
return this.node().payload.number;
return this.payload().number;
}
title(): string {
return this.node().payload.title;
return this.payload().title;
}
parent(): Repository {
return (_parent(this): any);
ref(): PullRequestReference {
return new PullRequestReference(super.ref());
}
}
export class Comment extends Post<CommentNodePayload> {
static from(e: Entity): Comment {
assertEntityType(e, COMMENT_NODE_TYPE);
return (e: any);
export class IssueReference extends CommentableReference<IssueNodePayload> {
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), ISSUE_NODE_TYPE);
}
parent(): Issue | PullRequest {
parent(): RepositoryReference {
return (_parent(this): any);
}
get(): ?IssuePorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new IssuePorcelain(nodePorcelain);
}
}
}
export class PullRequestReview extends Post<PullRequestReviewNodePayload> {
static from(e: Entity): PullRequestReview {
assertEntityType(e, PULL_REQUEST_REVIEW_NODE_TYPE);
return (e: any);
export class IssuePorcelain extends PostPorcelain<IssueNodePayload> {
constructor(nodePorcelain: NodePorcelain<any>) {
assertAddressType(nodePorcelain.ref().address(), ISSUE_NODE_TYPE);
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 {
return this.node().payload.state;
return this.payload().state;
}
parent(): PullRequest {
return (_parent(this): any);
}
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));
ref(): PullRequestReviewReference {
return new PullRequestReviewReference(super.ref());
}
}
export class PullRequestReviewComment extends Post<
export class PullRequestReviewCommentReference extends PostReference<
PullRequestReviewCommentNodePayload
> {
static from(e: Entity): PullRequestReviewComment {
assertEntityType(e, PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE);
return (e: any);
constructor(ref: NodeReference<any>) {
super(ref);
assertAddressType(ref.address(), PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE);
}
parent(): PullRequestReview {
parent(): PullRequestReviewReference {
return (_parent(this): any);
}
get(): ?PullRequestReviewCommentPorcelain {
const nodePorcelain = super.get();
if (nodePorcelain != null) {
return new PullRequestReviewCommentPorcelain(nodePorcelain);
}
}
}
function _parent(x: Entity): Entity {
const parents = x.graph.neighborhood(x.address(), {
edgeType: "CONTAINS",
direction: "IN",
});
export class PullRequestReviewCommentPorcelain extends PostPorcelain<
PullRequestReviewCommentNodePayload
> {
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) {
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
import type {Address} from "../../core/address";
import {parse} from "./parser";
import exampleRepoData from "./demoData/example-github.json";
import type {Entity} from "./porcelain";
import {
asEntity,
Porcelain,
Repository,
Issue,
PullRequest,
PullRequestReview,
PullRequestReviewComment,
Comment,
Author,
AuthorReference,
AuthorPorcelain,
CommentReference,
CommentPorcelain,
GithubReference,
GraphPorcelain,
IssueReference,
IssuePorcelain,
PullRequestReference,
PullRequestPorcelain,
PullRequestReviewReference,
PullRequestReviewPorcelain,
PullRequestReviewCommentReference,
PullRequestReviewCommentPorcelain,
RepositoryReference,
RepositoryPorcelain,
} from "./porcelain";
import type {NodePayload} from "./types";
import {
AUTHOR_NODE_TYPE,
COMMENT_NODE_TYPE,
@ -26,14 +32,19 @@ import {
import {nodeDescription} from "./render";
import {PLUGIN_NAME} from "./pluginName";
describe("GitHub porcelain", () => {
const graph = parse(exampleRepoData);
const porcelain = new Porcelain(graph);
const repo = porcelain.repository("sourcecred", "example-github");
const porcelain = new GraphPorcelain(graph);
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>,
extractor: (T) => mixed
) {
@ -47,36 +58,75 @@ describe("GitHub porcelain", () => {
expect(urlToProperty).toMatchSnapshot();
}
function issueByNumber(n: number): Issue {
const result = repo.issueByNumber(n);
function issueByNumber(n: number): IssuePorcelain {
const ref = repo.ref().issueByNumber(n);
if (ref == null) {
throw new Error(`Expected issue #${n} to exist`);
}
const result = ref.get();
if (result == null) {
throw new Error(`Expected Issue #${n} to exist`);
throw new Error(`Expected issue #${n} to exist`);
}
return result;
}
function prByNumber(n: number): PullRequest {
const result = repo.pullRequestByNumber(n);
function prByNumber(n: number): PullRequestPorcelain {
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) {
throw new Error(`Expected PR #${n} to exist`);
throw new Error(`Expected pull request #${n} to exist`);
}
return result;
}
function issueOrPrByNumber(n: number): Issue | PullRequest {
const result = repo.issueByNumber(n) || repo.pullRequestByNumber(n);
function issueOrPrByNumber(n: number): IssuePorcelain | PullRequestPorcelain {
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) {
throw new Error(`Expected Issue/PR #${n} to exist`);
}
return result;
}
const issue = issueByNumber(2);
const comment = issue.comments()[0];
const pullRequest = prByNumber(5);
const pullRequestReview = pullRequest.reviews()[0];
const pullRequestReviewComment = pullRequestReview.comments()[0];
const author = issue.authors()[0];
function really<T>(x: ?T): T {
if (x == null) {
throw new Error(String(x));
}
return x;
}
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 = [
issue,
pullRequest,
@ -87,62 +137,43 @@ describe("GitHub porcelain", () => {
];
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", () => {
expectPropertiesToMatchSnapshot(allWrappers, (e) => e.url());
});
it("all wrappers provide an address() method", () => {
allWrappers.forEach((w) => {
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);
});
test("reference constructors throw errors when used incorrectly", () => {
expect(() => new RepositoryReference(issue.ref())).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");
});
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"
);
expect(() => Author.from(repo)).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", () => {
@ -154,19 +185,32 @@ describe("GitHub porcelain", () => {
comment,
];
it("have parents", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => e.parent().url());
expectPropertiesToMatchSnapshot(allPosts, (e) =>
really(
e
.ref()
.parent()
.get()
).url()
);
});
it("have bodies", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) => e.body());
});
it("have authors", () => {
expectPropertiesToMatchSnapshot(allPosts, (e) =>
e.authors().map((a) => a.login())
e
.ref()
.authors()
.map((a) => really(a.get()).login())
);
});
it("have references", () => {
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", () => {
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", () => {
const prs = [prByNumber(3), prByNumber(5), prByNumber(9)];
it("have mergeCommitHashes", () => {
expectPropertiesToMatchSnapshot(prs, (e) => e.mergeCommitHash());
expectPropertiesToMatchSnapshot(prs, (e) => e.ref().mergeCommitHash());
});
it("have reviews", () => {
expectPropertiesToMatchSnapshot(prs, (e) =>
e.reviews().map((r) => r.url())
e
.ref()
.reviews()
.map((r) => really(r.get()).url())
);
});
});
describe("pull request reviews", () => {
const reviews = pullRequest.reviews();
const reviews = pullRequest.ref().reviews();
it("have review comments", () => {
expectPropertiesToMatchSnapshot(reviews, (e) =>
e.comments().map((e) => e.url())
expectPropertiesToMatchSnapshot(
reviews.map((r) => really(r.get())),
(e) =>
e
.ref()
.comments()
.map((e) => really(e.get()).url())
);
});
it("have states", () => {
expectPropertiesToMatchSnapshot(reviews, (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");
expectPropertiesToMatchSnapshot(
reviews.map((r) => really(r.get())),
(e) => e.state()
);
});
});
@ -252,22 +286,24 @@ describe("GitHub porcelain", () => {
describe("References", () => {
it("via #-number", () => {
const srcIssue = issueByNumber(2);
const references = srcIssue.references();
const references = srcIssue.ref().references();
expect(references).toHaveLength(1);
// Note: this verifies that we are not counting in-references, as
// https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316
// references #2.
const referenced = Issue.from(references[0]);
const referenced = new IssuePorcelain(really(references[0].get()));
expect(referenced.number()).toBe(1);
});
describe("by exact url", () => {
function expectCommentToHaveSingleReference({commentNumber, type, url}) {
const comments = issueByNumber(2).comments();
const comments = issueByNumber(2)
.ref()
.comments();
const references = comments[commentNumber].references();
expect(references).toHaveLength(1);
expect(references[0].url()).toBe(url);
expect(really(references[0].get()).url()).toBe(url);
expect(references[0].type()).toBe(type);
}
@ -324,6 +360,7 @@ describe("GitHub porcelain", () => {
it("to multiple entities", () => {
const references = issueByNumber(2)
.ref()
.comments()[6]
.references();
expect(references).toHaveLength(5);
@ -331,6 +368,7 @@ describe("GitHub porcelain", () => {
it("to no entities", () => {
const references = issueByNumber(2)
.ref()
.comments()[7]
.references();
expect(references).toHaveLength(0);
@ -339,10 +377,11 @@ describe("GitHub porcelain", () => {
it("References by @-author", () => {
const pr = prByNumber(5);
const references = pr.references();
const references = pr.ref().references();
expect(references).toHaveLength(1);
const referenced = Author.from(references[0]);
expect(referenced.login()).toBe("wchargin");
const referenced = new AuthorReference(references[0]);
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
// description method into the porcelain anyway...)
expectPropertiesToMatchSnapshot(allWrappers, (e) =>
nodeDescription(e.graph, e.address())
nodeDescription(e.ref())
);
});
});

View File

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