Connect PRs and commits via MERGED_AS edges (#200)

This adds MERGED_AS edges which link from a PullRequest to a Commit. It
adds a corresponding `mergedCommitHash` method on the porcelain PR that
returns the hash of the merged commit (if available).

I would have preferred to return a porcelain wrapper over the commit,
but since we don't have a porcelain Git api, it seemed preferrable to
return the hash as a string. Returning a Node would both break
consistency in the porcelain api, and be problematic as the node does
not necessarily exist in the api. To ensure that the hash is available
without parsing Addresses, I used the edge payload. :)

Test plan:
Inspect the snapshot changes in the graph (they are fairly readable) and
the api testing in api.test.js.
This commit is contained in:
Dandelion Mané 2018-05-03 13:29:44 -07:00 committed by GitHub
parent 723efeb05f
commit a76d01ab75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 25 deletions

View File

@ -49,7 +49,7 @@ Object {
}
`;
exports[`GitHub porcelain API has wrappers for PullRequests 1`] = `
exports[`GitHub porcelain API has wrappers for PullRequests Merged 1`] = `
Object {
"address": Object {
"id": "https://github.com/sourcecred/example-github/pull/3",

View File

@ -199,6 +199,21 @@ Object {
"type": "PULL_REQUEST_REVIEW",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/git-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMIT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"MERGED_AS\\"}": Object {
"dst": Object {
"id": "6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
"pluginName": "sourcecred/git-beta",
"type": "COMMIT",
},
"payload": Object {
"hash": "6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
},
"src": Object {
"id": "https://github.com/sourcecred/example-github/pull/5",
"pluginName": "sourcecred/github-beta",
"type": "PULL_REQUEST",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST_REVIEW\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"CONTAINS\\"}": Object {
"dst": Object {
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
@ -358,6 +373,21 @@ Object {
"type": "PULL_REQUEST",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"0a223346b4e6dec0127b1e6aa892c4ee0424b66a\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/git-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMIT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"MERGED_AS\\"}": Object {
"dst": Object {
"id": "0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
"pluginName": "sourcecred/git-beta",
"type": "COMMIT",
},
"payload": Object {
"hash": "0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
},
"src": Object {
"id": "https://github.com/sourcecred/example-github/pull/3",
"pluginName": "sourcecred/github-beta",
"type": "PULL_REQUEST",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMENT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"CONTAINS\\"}": Object {
"dst": Object {
"id": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
@ -1830,6 +1860,21 @@ Object {
"type": "COMMENT",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"0a223346b4e6dec0127b1e6aa892c4ee0424b66a\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/git-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMIT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"MERGED_AS\\"}": Object {
"dst": Object {
"id": "0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
"pluginName": "sourcecred/git-beta",
"type": "COMMIT",
},
"payload": Object {
"hash": "0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
},
"src": Object {
"id": "https://github.com/sourcecred/example-github/pull/3",
"pluginName": "sourcecred/github-beta",
"type": "PULL_REQUEST",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMENT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"CONTAINS\\"}": Object {
"dst": Object {
"id": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
@ -1856,6 +1901,21 @@ Object {
"type": "PULL_REQUEST_REVIEW",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/git-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"COMMIT\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"MERGED_AS\\"}": Object {
"dst": Object {
"id": "6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
"pluginName": "sourcecred/git-beta",
"type": "COMMIT",
},
"payload": Object {
"hash": "6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
},
"src": Object {
"id": "https://github.com/sourcecred/example-github/pull/5",
"pluginName": "sourcecred/github-beta",
"type": "PULL_REQUEST",
},
},
"{\\"id\\":\\"[{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST\\\\\\"},{\\\\\\"id\\\\\\":\\\\\\"https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899\\\\\\",\\\\\\"pluginName\\\\\\":\\\\\\"sourcecred/github-beta\\\\\\",\\\\\\"type\\\\\\":\\\\\\"PULL_REQUEST_REVIEW\\\\\\"}]\\",\\"pluginName\\":\\"sourcecred/github-beta\\",\\"type\\":\\"CONTAINS\\"}": Object {
"dst": Object {
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",

View File

@ -6,31 +6,35 @@ import {Graph} from "../../core/graph";
import type {Node} from "../../core/graph";
import type {Address} from "../../core/address";
import type {
NodePayload,
EdgePayload,
NodeType,
IssueNodePayload,
PullRequestNodePayload,
PullRequestReviewNodePayload,
PullRequestReviewCommentNodePayload,
PullRequestReviewState,
CommentNodePayload,
AuthorNodePayload,
AuthorSubtype,
CommentNodePayload,
EdgePayload,
IssueNodePayload,
MergedAsEdgePayload,
NodePayload,
NodeType,
PullRequestNodePayload,
PullRequestReviewCommentNodePayload,
PullRequestReviewNodePayload,
PullRequestReviewState,
} from "./types";
import {
CONTAINS_EDGE_TYPE,
COMMENT_NODE_TYPE,
AUTHORS_EDGE_TYPE,
AUTHOR_NODE_TYPE,
COMMENT_NODE_TYPE,
CONTAINS_EDGE_TYPE,
ISSUE_NODE_TYPE,
MERGED_AS_EDGE_TYPE,
PULL_REQUEST_NODE_TYPE,
PULL_REQUEST_REVIEW_NODE_TYPE,
PULL_REQUEST_REVIEW_COMMENT_NODE_TYPE,
PULL_REQUEST_REVIEW_NODE_TYPE,
REFERENCES_EDGE_TYPE,
} from "./types";
import {COMMIT_NODE_TYPE} from "../git/types";
export type Entity =
| Issue
| PullRequest
@ -221,6 +225,25 @@ export class PullRequest extends Commentable<PullRequestNodePayload> {
})
.map(({neighbor}) => new PullRequestReview(this.graph, neighbor));
}
mergeCommitHash(): ?string {
const mergeEdge = this.graph
.neighborhood(this.nodeAddress, {
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`
);
}
if (mergeEdge.length === 0) {
return null;
}
const payload: MergedAsEdgePayload = (mergeEdge[0].payload: any);
return payload.hash;
}
}
export class Issue extends Commentable<IssueNodePayload> {

View File

@ -35,18 +35,26 @@ describe("GitHub porcelain API", () => {
expect(issue.address()).toEqual(issue.node().address);
expect(issue.authors().map((x) => x.login())).toEqual(["decentralion"]);
});
it("PullRequests", () => {
const pullRequest = issueOrPRByNumber(3);
expect(pullRequest.body()).toBe("Oh look, it's a pull request.");
expect(pullRequest.title()).toBe("Add README, merge via PR.");
expect(pullRequest.url()).toBe(
"https://github.com/sourcecred/example-github/pull/3"
);
expect(pullRequest.number()).toBe(3);
expect(pullRequest.type()).toBe(PULL_REQUEST_NODE_TYPE);
expect(pullRequest.node()).toMatchSnapshot();
expect(pullRequest.address()).toEqual(pullRequest.node().address);
describe("PullRequests", () => {
it("Merged", () => {
const pullRequest = PullRequest.from(issueOrPRByNumber(3));
expect(pullRequest.body()).toBe("Oh look, it's a pull request.");
expect(pullRequest.title()).toBe("Add README, merge via PR.");
expect(pullRequest.url()).toBe(
"https://github.com/sourcecred/example-github/pull/3"
);
expect(pullRequest.number()).toBe(3);
expect(pullRequest.type()).toBe(PULL_REQUEST_NODE_TYPE);
expect(pullRequest.node()).toMatchSnapshot();
expect(pullRequest.address()).toEqual(pullRequest.node().address);
expect(pullRequest.mergeCommitHash()).toEqual(
"0a223346b4e6dec0127b1e6aa892c4ee0424b66a"
);
});
it("Unmerged", () => {
const pullRequest = PullRequest.from(issueOrPRByNumber(9));
expect(pullRequest.mergeCommitHash()).toEqual(null);
});
});
it("Pull Request Reviews", () => {

View File

@ -19,6 +19,7 @@ import type {
AuthorSubtype,
} from "./types";
import {MERGED_AS_EDGE_TYPE} from "./types";
import type {
RepositoryJSON,
PullRequestReviewJSON,
@ -32,6 +33,7 @@ import type {Address} from "../../core/address";
import {PLUGIN_NAME} from "./pluginName";
import {Graph, edgeID} from "../../core/graph";
import {findReferences} from "./findReferences";
import {commitAddress} from "../git/address";
export function parse(
repositoryJSON: RepositoryJSON
@ -217,6 +219,22 @@ class GithubParser {
prJson.reviews.nodes.forEach((r) =>
this.addPullRequestReview(pullRequestNode, r)
);
if (prJson.mergeCommit != null) {
const hash = prJson.mergeCommit.oid;
const dstAddr = commitAddress(hash);
const mergedAsEdge = {
address: this.makeEdgeAddress(
MERGED_AS_EDGE_TYPE,
pullRequestNode.address,
dstAddr
),
payload: {hash},
src: pullRequestNode.address,
dst: dstAddr,
};
this.graph.addEdge(mergedAsEdge);
}
}
addPullRequestReview(

View File

@ -98,6 +98,8 @@ export type AuthorsEdgePayload = {};
export const AUTHORS_EDGE_TYPE: "AUTHORS" = "AUTHORS";
export type ContainsEdgePayload = {};
export const CONTAINS_EDGE_TYPE: "CONTAINS" = "CONTAINS";
export type MergedAsEdgePayload = {|+hash: string|};
export const MERGED_AS_EDGE_TYPE: "MERGED_AS" = "MERGED_AS";
export type ReferencesEdgePayload = {};
export const REFERENCES_EDGE_TYPE: "REFERENCES" = "REFERENCES";
@ -110,6 +112,10 @@ export type EdgeTypes = {|
payload: ContainsEdgePayload,
type: typeof CONTAINS_EDGE_TYPE,
},
MERGED_AS: {
payload: MergedAsEdgePayload,
type: typeof MERGED_AS_EDGE_TYPE,
},
REFERENCES: {
payload: ReferencesEdgePayload,
type: typeof REFERENCES_EDGE_TYPE,
@ -119,6 +125,7 @@ export type EdgeTypes = {|
export type EdgeType =
| typeof AUTHORS_EDGE_TYPE
| typeof CONTAINS_EDGE_TYPE
| typeof MERGED_AS_EDGE_TYPE
| typeof REFERENCES_EDGE_TYPE;
export type EdgePayload =