Add GitHub commit entity (#819)

This adds a `Commit` entity to the GitHub relational view. It has all
the standard methods: commits can be retrieved en masse or by particular
address, they have a URL and authors, and (de)serialize appropriately.

The code for adding pull requests has been modified so that the merge
commits are added as commit entities. This does not have any effect on
the ultimate graph being created; the same edge is added either way.

Test plan: I've extended the standard RelationalView tests to cover the
`Commit` entity. The case where the commit has 0 authors is not yet
tested, but will be once I add support for getting all of the commits
from the example-github (we have one example of a commit that doesn't
map to a user).

Progress on #815.
This commit is contained in:
Dandelion Mané 2018-09-12 19:44:26 -07:00 committed by GitHub
parent 3e06c054db
commit 7d0d4fb2fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 102 additions and 14 deletions

View File

@ -18,6 +18,16 @@ Object {
exports[`plugins/github/relationalView Comment has url 1`] = `"https://github.com/sourcecred/example-github/pull/5#discussion_r171460198"`;
exports[`plugins/github/relationalView Commit authors has expected number of authors 1`] = `1`;
exports[`plugins/github/relationalView Commit authors have expected urls 1`] = `
Array [
"https://github.com/decentralion",
]
`;
exports[`plugins/github/relationalView Commit has url 1`] = `"https://github.com/sourcecred/example-github/commit/0a223346b4e6dec0127b1e6aa892c4ee0424b66a"`;
exports[`plugins/github/relationalView Issue authors has expected number of authors 1`] = `1`;
exports[`plugins/github/relationalView Issue authors have expected urls 1`] = `
@ -137,6 +147,15 @@ Array [
]
`;
exports[`plugins/github/relationalView RelationalView entity: commits has expected number of them 1`] = `2`;
exports[`plugins/github/relationalView RelationalView entity: commits they have expected urls 1`] = `
Array [
"https://github.com/sourcecred/example-github/commit/0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
"https://github.com/sourcecred/example-github/commit/6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6",
]
`;
exports[`plugins/github/relationalView RelationalView entity: issues has expected number of them 1`] = `8`;
exports[`plugins/github/relationalView RelationalView entity: issues they have expected urls 1`] = `

View File

@ -3,6 +3,7 @@
exports[`plugins/github/render descriptions are as expected 1`] = `
Object {
"comment": "comment by @wchargin on review by @wchargin of #5 (+1/0): This pull request will be more contentious. I can feel it...",
"commit": "commit 0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
"issue": "#2: A referencing issue.",
"pull": "#5 (+1/0): This pull request will be more contentious. I can feel it...",
"repo": "sourcecred/example-github",

View File

@ -58,7 +58,7 @@ class GraphCreator {
this.graph.addNode(N.toRaw(addr));
}
addAuthors(entity: R.Issue | R.Pull | R.Comment | R.Review) {
addAuthors(entity: R.AuthoredEntity) {
for (const author of entity.authors()) {
this.graph.addEdge(
createEdge.authors(author.address(), entity.address())

View File

@ -27,6 +27,7 @@ export function exampleEntities() {
const pull = Array.from(repo.pulls())[1];
const review = Array.from(pull.reviews())[0];
const comment = Array.from(review.comments())[0];
const commit = Array.from(view.commits())[0];
const userlike = Array.from(review.authors())[0];
return {
repo,
@ -34,6 +35,7 @@ export function exampleEntities() {
pull,
review,
comment,
commit,
userlike,
};
}

View File

@ -82,7 +82,8 @@ export type AuthorableAddress =
| IssueAddress
| PullAddress
| ReviewAddress
| CommentAddress;
| CommentAddress
| GitNode.CommitAddress;
// Each of these types has text content, which means
// it may be the source of a reference to a ReferentAddress.

View File

@ -20,6 +20,7 @@ import type {
PullJSON,
ReviewJSON,
CommentJSON,
CommitJSON,
NullableAuthorJSON,
ReviewState,
} from "./graphql";
@ -36,7 +37,7 @@ import {
const COMPAT_INFO = {
type: "sourcecred/github/relationalView",
version: "0.1.0",
version: "0.2.0",
};
export class RelationalView {
@ -44,6 +45,7 @@ export class RelationalView {
_issues: Map<N.RawAddress, IssueEntry>;
_pulls: Map<N.RawAddress, PullEntry>;
_comments: Map<N.RawAddress, CommentEntry>;
_commits: Map<N.RawAddress, CommitEntry>;
_reviews: Map<N.RawAddress, ReviewEntry>;
_userlikes: Map<N.RawAddress, UserlikeEntry>;
_mapReferences: Map<N.RawAddress, N.ReferentAddress[]>;
@ -54,6 +56,7 @@ export class RelationalView {
this._issues = new Map();
this._pulls = new Map();
this._comments = new Map();
this._commits = new Map();
this._reviews = new Map();
this._userlikes = new Map();
this._mapReferences = new Map();
@ -140,6 +143,17 @@ export class RelationalView {
return entry == null ? entry : new Comment(this, entry);
}
*commits(): Iterator<Commit> {
for (const entry of this._commits.values()) {
yield new Commit(this, entry);
}
}
commit(address: GitNode.CommitAddress): ?Commit {
const entry = this._commits.get(N.toRaw(address));
return entry == null ? entry : new Commit(this, entry);
}
*reviews(): Iterator<Review> {
for (const entry of this._reviews.values()) {
yield new Review(this, entry);
@ -177,7 +191,7 @@ export class RelationalView {
case "USERLIKE":
return this.userlike(address);
case "COMMIT":
return null;
return this.commit(address);
default:
throw new Error(`Unexpected address type: ${(address.type: empty)}`);
}
@ -226,6 +240,7 @@ export class RelationalView {
yield* this.pulls();
yield* this.reviews();
yield* this.comments();
yield* this.commits();
yield* this.userlikes();
}
@ -236,6 +251,7 @@ export class RelationalView {
pulls: MapUtil.toObject(this._pulls),
reviews: MapUtil.toObject(this._reviews),
comments: MapUtil.toObject(this._comments),
commits: MapUtil.toObject(this._commits),
userlikes: MapUtil.toObject(this._userlikes),
references: MapUtil.toObject(this._mapReferences),
referencedBy: MapUtil.toObject(this._mapReferencedBy),
@ -251,6 +267,7 @@ export class RelationalView {
rv._pulls = MapUtil.fromObject(json.pulls);
rv._reviews = MapUtil.fromObject(json.reviews);
rv._comments = MapUtil.fromObject(json.comments);
rv._commits = MapUtil.fromObject(json.commits);
rv._userlikes = MapUtil.fromObject(json.userlikes);
rv._mapReferences = MapUtil.fromObject(json.references);
rv._mapReferencedBy = MapUtil.fromObject(json.referencedBy);
@ -291,19 +308,29 @@ export class RelationalView {
return address;
}
_addCommit(json: CommitJSON): GitNode.CommitAddress {
const address = {type: GitNode.COMMIT_TYPE, hash: json.oid};
const authors =
json.author == null ? [] : this._addNullableAuthor(json.author.user);
const entry: CommitEntry = {
address,
url: json.url,
authors,
};
this._commits.set(N.toRaw(address), entry);
return address;
}
_addPull(repo: RepoAddress, json: PullJSON): PullAddress {
const address: PullAddress = {
type: N.PULL_TYPE,
number: String(json.number),
repo,
};
// TODO(@decentralion): Rewrite so that pulls actually have
// the commit attached (not just oid)
const mergedAs =
json.mergeCommit == null
? null
: {
type: GitNode.COMMIT_TYPE,
hash: json.mergeCommit.oid,
};
json.mergeCommit == null ? null : this._addCommit(json.mergeCommit);
const entry: PullEntry = {
address,
@ -558,6 +585,7 @@ type Entry =
| PullEntry
| ReviewEntry
| CommentEntry
| CommitEntry
| UserlikeEntry;
export class _Entity<+T: Entry> {
@ -803,6 +831,21 @@ export class Comment extends _Entity<CommentEntry> {
}
}
type CommitEntry = {|
+address: GitNode.CommitAddress,
+url: string,
+authors: UserlikeAddress[],
|};
export class Commit extends _Entity<CommitEntry> {
constructor(view: RelationalView, entry: CommitEntry) {
super(view, entry);
}
authors(): Iterator<Userlike> {
return getAuthors(this._view, this._entry);
}
}
type UserlikeEntry = {|
+address: UserlikeAddress,
+url: string,
@ -831,7 +874,7 @@ function assertExists<T>(item: ?T, address: N.StructuredAddress): T {
function* getAuthors(
view: RelationalView,
entry: IssueEntry | PullEntry | ReviewEntry | CommentEntry
entry: IssueEntry | PullEntry | ReviewEntry | CommentEntry | CommitEntry
) {
for (const address of entry.authors) {
const author = view.userlike(address);
@ -845,6 +888,7 @@ export type MatchHandlers<T> = {|
+pull: (x: Pull) => T,
+review: (x: Review) => T,
+comment: (x: Comment) => T,
+commit: (x: Commit) => T,
+userlike: (x: Userlike) => T,
|};
export function match<T>(handlers: MatchHandlers<T>, x: Entity): T {
@ -863,14 +907,17 @@ export function match<T>(handlers: MatchHandlers<T>, x: Entity): T {
if (x instanceof Comment) {
return handlers.comment(x);
}
if (x instanceof Commit) {
return handlers.commit(x);
}
if (x instanceof Userlike) {
return handlers.userlike(x);
}
throw new Error(`Unexpected entity ${x}`);
}
export type Entity = Repo | Issue | Pull | Review | Comment | Userlike;
export type AuthoredEntity = Issue | Pull | Review | Comment;
export type Entity = Repo | Issue | Pull | Review | Comment | Commit | Userlike;
export type AuthoredEntity = Issue | Pull | Review | Comment | Commit;
export type TextContentEntity = Issue | Pull | Review | Comment;
export type ParentEntity = Repo | Issue | Pull | Review;
export type ChildEntity = Issue | Pull | Review | Comment;
@ -883,6 +930,7 @@ export opaque type RelationalViewJSON = Compatible<{|
+pulls: AddressEntryMapJSON<PullEntry>,
+reviews: AddressEntryMapJSON<ReviewEntry>,
+comments: AddressEntryMapJSON<CommentEntry>,
+commits: AddressEntryMapJSON<CommitEntry>,
+userlikes: AddressEntryMapJSON<UserlikeEntry>,
+references: AddressEntryMapJSON<N.ReferentAddress[]>,
+referencedBy: AddressEntryMapJSON<N.TextContentAddress[]>,

View File

@ -61,6 +61,7 @@ describe("plugins/github/relationalView", () => {
hasEntityMethods("pulls", () => view.pulls(), (x) => view.pull(x));
hasEntityMethods("reviews", () => view.reviews(), (x) => view.review(x));
hasEntityMethods("comments", () => view.comments(), (x) => view.comment(x));
hasEntityMethods("commits", () => view.commits(), (x) => view.commit(x));
hasEntityMethods(
"userlikes",
() => view.userlikes(),
@ -133,6 +134,13 @@ describe("plugins/github/relationalView", () => {
hasEntities("authors", () => entity.authors());
});
const commit = Array.from(view.commits())[0];
describe("Commit", () => {
const entity = commit;
has("url", () => entity.url());
hasEntities("authors", () => entity.authors());
});
const userlike = Array.from(review.authors())[0];
describe("Userlike", () => {
const entity = userlike;
@ -156,6 +164,9 @@ describe("plugins/github/relationalView", () => {
it("works for comment", () => {
expect(view.entity(comment.address())).toEqual(comment);
});
it("works for commit", () => {
expect(view.entity(commit.address())).toEqual(commit);
});
it("works for userlike", () => {
expect(view.entity(userlike.address())).toEqual(userlike);
});
@ -179,10 +190,11 @@ describe("plugins/github/relationalView", () => {
pull: (x: R.Pull) => [x.address(), "PULL"],
review: (x: R.Review) => [x.address(), "REVIEW"],
comment: (x: R.Comment) => [x.address(), "COMMENT"],
commit: (x: R.Commit) => [x.address(), "COMMIT"],
userlike: (x: R.Userlike) => [x.address(), "USERLIKE"],
};
const instances = [repo, issue, pull, review, comment, userlike];
const instances = [repo, issue, pull, review, comment, commit, userlike];
for (const instance of instances) {
const [actualAddress, functionType] = R.match(handlers, instance);
expect(actualAddress.type).toEqual(functionType);

View File

@ -20,6 +20,11 @@ export function description(e: R.Entity) {
},
review: (x) => `review ${withAuthors(x)}of ${description(x.parent())}`,
comment: (x) => `comment ${withAuthors(x)}on ${description(x.parent())}`,
// The commit type is included for completeness's sake and to
// satisfy the typechecker, but won't ever be seen in the frontend
// because the commit has a Git plugin prefix and will therefore by
// handled by the git plugin adapter
commit: (x) => `commit ${x.address().hash}`,
userlike: (x) => `@${x.login()}`,
};
return R.match(handlers, e);