Add edge type definitions for V3 Git plugin (#404)

Summary:
This is modeled after the GitHub edge module format. In particular, the
whole length encoding garbage is directly copied. As in that module, we
decline to test the error paths.

Test Plan:
Unit tests added; run `yarn travis`. Snapshots are readable.

wchargin-branch: git-v3-edges
This commit is contained in:
William Chargin 2018-06-20 15:49:50 -07:00 committed by GitHub
parent 7c1b3ca835
commit 448fb3e1a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 476 additions and 0 deletions

View File

@ -0,0 +1,140 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/git/edges createEdge works for "becomes" 1`] = `
Object {
"addressParts": Array [
"sourcecred",
"git",
"BECOMES",
"3",
"TREE_ENTRY",
"de07d6d2b2977734cf39d2b9aff4135eefce3eb7",
"old_science.txt",
"3",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
"dstParts": Array [
"sourcecred",
"git",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
"srcParts": Array [
"sourcecred",
"git",
"TREE_ENTRY",
"de07d6d2b2977734cf39d2b9aff4135eefce3eb7",
"old_science.txt",
],
}
`;
exports[`plugins/git/edges createEdge works for "hasContents" 1`] = `
Object {
"addressParts": Array [
"sourcecred",
"git",
"HAS_CONTENTS",
"3",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
"dstParts": Array [
"sourcecred",
"git",
"BLOB",
"f1f2514ca6d7a6a1a0511957021b1995bf9ace1c",
],
"srcParts": Array [
"sourcecred",
"git",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
}
`;
exports[`plugins/git/edges createEdge works for "hasParent" 1`] = `
Object {
"addressParts": Array [
"sourcecred",
"git",
"HAS_PARENT",
"2",
"COMMIT",
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
"2",
"COMMIT",
"69c5aad50eec8f2a0a07c988c3b283a6490eb45b",
],
"dstParts": Array [
"sourcecred",
"git",
"COMMIT",
"69c5aad50eec8f2a0a07c988c3b283a6490eb45b",
],
"srcParts": Array [
"sourcecred",
"git",
"COMMIT",
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
],
}
`;
exports[`plugins/git/edges createEdge works for "hasTree" 1`] = `
Object {
"addressParts": Array [
"sourcecred",
"git",
"HAS_TREE",
"2",
"COMMIT",
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
],
"dstParts": Array [
"sourcecred",
"git",
"TREE",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
],
"srcParts": Array [
"sourcecred",
"git",
"COMMIT",
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
],
}
`;
exports[`plugins/git/edges createEdge works for "includes" 1`] = `
Object {
"addressParts": Array [
"sourcecred",
"git",
"INCLUDES",
"3",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
"dstParts": Array [
"sourcecred",
"git",
"TREE_ENTRY",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
"science.txt",
],
"srcParts": Array [
"sourcecred",
"git",
"TREE",
"7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
],
}
`;

244
src/v3/plugins/git/edges.js Normal file
View File

@ -0,0 +1,244 @@
// @flow
import {
type Edge,
type EdgeAddressT,
EdgeAddress,
NodeAddress,
} from "../../core/graph";
import * as GitNode from "./nodes";
export opaque type RawAddress: EdgeAddressT = EdgeAddressT;
export const HAS_TREE_TYPE: "HAS_TREE" = "HAS_TREE";
export const HAS_PARENT_TYPE: "HAS_PARENT" = "HAS_PARENT";
export const INCLUDES_TYPE: "INCLUDES" = "INCLUDES";
export const BECOMES_TYPE: "BECOMES" = "BECOMES";
export const HAS_CONTENTS_TYPE: "HAS_CONTENTS" = "HAS_CONTENTS";
const GIT_PREFIX = EdgeAddress.fromParts(["sourcecred", "git"]);
function gitEdgeAddress(...parts: string[]): RawAddress {
return EdgeAddress.append(GIT_PREFIX, ...parts);
}
export const _Prefix = Object.freeze({
base: GIT_PREFIX,
hasTree: gitEdgeAddress(HAS_TREE_TYPE),
hasParent: gitEdgeAddress(HAS_PARENT_TYPE),
includes: gitEdgeAddress(INCLUDES_TYPE),
becomes: gitEdgeAddress(BECOMES_TYPE),
hasContents: gitEdgeAddress(HAS_CONTENTS_TYPE),
});
export type HasTreeAddress = {|
type: typeof HAS_TREE_TYPE,
commit: GitNode.CommitAddress,
|};
export type HasParentAddress = {|
type: typeof HAS_PARENT_TYPE,
child: GitNode.CommitAddress,
parent: GitNode.CommitAddress,
|};
export type IncludesAddress = {|
type: typeof INCLUDES_TYPE,
treeEntry: GitNode.TreeEntryAddress,
|};
export type BecomesAddress = {|
type: typeof BECOMES_TYPE,
was: GitNode.TreeEntryAddress,
becomes: GitNode.TreeEntryAddress,
|};
export type HasContentsAddress = {|
type: typeof HAS_CONTENTS_TYPE,
treeEntry: GitNode.TreeEntryAddress,
|};
export type StructuredAddress =
| HasTreeAddress
| HasParentAddress
| IncludesAddress
| BecomesAddress
| HasContentsAddress;
export const createEdge = Object.freeze({
hasTree: (
commit: GitNode.CommitAddress,
tree: GitNode.TreeAddress
): Edge => ({
address: toRaw({type: HAS_TREE_TYPE, commit}),
src: GitNode.toRaw(commit),
dst: GitNode.toRaw(tree),
}),
hasParent: (
child: GitNode.CommitAddress,
parent: GitNode.CommitAddress
): Edge => ({
address: toRaw({type: HAS_PARENT_TYPE, child, parent}),
src: GitNode.toRaw(child),
dst: GitNode.toRaw(parent),
}),
includes: (
tree: GitNode.TreeAddress,
treeEntry: GitNode.TreeEntryAddress
): Edge => ({
address: toRaw({type: INCLUDES_TYPE, treeEntry}),
src: GitNode.toRaw(tree),
dst: GitNode.toRaw(treeEntry),
}),
becomes: (
was: GitNode.TreeEntryAddress,
becomes: GitNode.TreeEntryAddress
): Edge => ({
address: toRaw({type: BECOMES_TYPE, was, becomes}),
src: GitNode.toRaw(was),
dst: GitNode.toRaw(becomes),
}),
hasContents: (
treeEntry: GitNode.TreeEntryAddress,
contents: GitNode.TreeEntryContentsAddress
): Edge => ({
address: toRaw({type: HAS_CONTENTS_TYPE, treeEntry}),
src: GitNode.toRaw(treeEntry),
dst: GitNode.toRaw(contents),
}),
});
const NODE_PREFIX_LENGTH = NodeAddress.toParts(GitNode._gitAddress()).length;
function lengthEncode(x: GitNode.RawAddress): $ReadOnlyArray<string> {
const baseParts = NodeAddress.toParts(x).slice(NODE_PREFIX_LENGTH);
return [String(baseParts.length), ...baseParts];
}
function lengthDecode(
x: $ReadOnlyArray<string>,
fail: () => Error
): {|+parts: $ReadOnlyArray<string>, +rest: $ReadOnlyArray<string>|} {
if (x.length === 0) {
// Not length-encoded.
throw fail();
}
const [lengthString, ...allParts] = x;
const length = parseInt(lengthString, 10);
if (isNaN(length)) {
throw fail();
}
if (length > allParts.length) {
// Not enough elements.
throw fail();
}
return {parts: allParts.slice(0, length), rest: allParts.slice(length)};
}
function multiLengthDecode(x: $ReadOnlyArray<string>, fail: () => Error) {
let remaining = x;
let partses = [];
while (remaining.length > 0) {
const {parts, rest} = lengthDecode(remaining, fail);
partses.push(parts);
remaining = rest;
}
return partses;
}
export function fromRaw(x: RawAddress): StructuredAddress {
function fail() {
return new Error(`Bad address: ${EdgeAddress.toString(x)}`);
}
if (!EdgeAddress.hasPrefix(x, GIT_PREFIX)) {
throw fail();
}
const [_unused_sc, _unused_git, _type, ...rest] = EdgeAddress.toParts(x);
const type: $ElementType<StructuredAddress, "type"> = (_type: any);
switch (type) {
case "HAS_TREE": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 1) throw fail();
const [commitParts] = parts;
const commit: GitNode.CommitAddress = (GitNode.fromRaw(
GitNode._gitAddress(...commitParts)
): any);
return {type: HAS_TREE_TYPE, commit};
}
case "HAS_PARENT": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 2) throw fail();
const [childParts, parentParts] = parts;
const child: GitNode.CommitAddress = (GitNode.fromRaw(
GitNode._gitAddress(...childParts)
): any);
const parent: GitNode.CommitAddress = (GitNode.fromRaw(
GitNode._gitAddress(...parentParts)
): any);
return {type: HAS_PARENT_TYPE, child, parent};
}
case "INCLUDES": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 1) throw fail();
const [treeEntryParts] = parts;
const treeEntry: GitNode.TreeEntryAddress = (GitNode.fromRaw(
GitNode._gitAddress(...treeEntryParts)
): any);
return {type: INCLUDES_TYPE, treeEntry};
}
case "BECOMES": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 2) throw fail();
const [wasParts, becomesParts] = parts;
const was: GitNode.TreeEntryAddress = (GitNode.fromRaw(
GitNode._gitAddress(...wasParts)
): any);
const becomes: GitNode.TreeEntryAddress = (GitNode.fromRaw(
GitNode._gitAddress(...becomesParts)
): any);
return {type: BECOMES_TYPE, was, becomes};
}
case "HAS_CONTENTS": {
const parts = multiLengthDecode(rest, fail);
if (parts.length !== 1) throw fail();
const [treeEntryParts] = parts;
const treeEntry: GitNode.TreeEntryAddress = (GitNode.fromRaw(
GitNode._gitAddress(...treeEntryParts)
): any);
return {type: HAS_CONTENTS_TYPE, treeEntry};
}
default:
// eslint-disable-next-line no-unused-expressions
(type: empty);
throw fail();
}
}
export function toRaw(x: StructuredAddress): RawAddress {
switch (x.type) {
case HAS_TREE_TYPE:
return EdgeAddress.append(
_Prefix.hasTree,
...lengthEncode(GitNode.toRaw(x.commit))
);
case HAS_PARENT_TYPE:
return EdgeAddress.append(
_Prefix.hasParent,
...lengthEncode(GitNode.toRaw(x.child)),
...lengthEncode(GitNode.toRaw(x.parent))
);
case INCLUDES_TYPE:
return EdgeAddress.append(
_Prefix.includes,
...lengthEncode(GitNode.toRaw(x.treeEntry))
);
case BECOMES_TYPE:
return EdgeAddress.append(
_Prefix.becomes,
...lengthEncode(GitNode.toRaw(x.was)),
...lengthEncode(GitNode.toRaw(x.becomes))
);
case HAS_CONTENTS_TYPE:
return EdgeAddress.append(
_Prefix.hasContents,
...lengthEncode(GitNode.toRaw(x.treeEntry))
);
default:
// eslint-disable-next-line no-unused-expressions
(x.type: empty);
throw new Error(x.type);
}
}

View File

@ -0,0 +1,85 @@
// @flow
import {type EdgeAddressT, edgeToParts} from "../../core/graph";
import {createEdge, fromRaw, toRaw} from "./edges";
import * as GE from "./edges";
import * as GN from "./nodes";
describe("plugins/git/edges", () => {
const nodeExamples = {
blob: (): GN.BlobAddress => ({
type: GN.BLOB_TYPE,
hash: "f1f2514ca6d7a6a1a0511957021b1995bf9ace1c",
}),
commit: (): GN.CommitAddress => ({
type: GN.COMMIT_TYPE,
hash: "3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
}),
parentCommit: (): GN.CommitAddress => ({
type: GN.COMMIT_TYPE,
hash: "69c5aad50eec8f2a0a07c988c3b283a6490eb45b",
}),
submoduleCommit: (): GN.SubmoduleCommitAddress => ({
type: GN.SUBMODULE_COMMIT_TYPE,
submoduleUrl: "https://github.com/sourcecred/example-git-submodule.git",
commitHash: "29ef158bc982733e2ba429fcf73e2f7562244188",
}),
tree: (): GN.TreeAddress => ({
type: GN.TREE_TYPE,
hash: "7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
}),
treeEntry: (): GN.TreeEntryAddress => ({
type: GN.TREE_ENTRY_TYPE,
treeHash: "7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
name: "science.txt",
}),
oldTreeEntry: (): GN.TreeEntryAddress => ({
type: GN.TREE_ENTRY_TYPE,
treeHash: "de07d6d2b2977734cf39d2b9aff4135eefce3eb7",
name: "old_science.txt",
}),
};
const edgeExamples = {
hasTree: () =>
createEdge.hasTree(nodeExamples.commit(), nodeExamples.tree()),
hasParent: () =>
createEdge.hasParent(nodeExamples.commit(), nodeExamples.parentCommit()),
includes: () =>
createEdge.includes(nodeExamples.tree(), nodeExamples.treeEntry()),
becomes: () =>
createEdge.becomes(nodeExamples.oldTreeEntry(), nodeExamples.treeEntry()),
hasContents: () =>
createEdge.hasContents(nodeExamples.treeEntry(), nodeExamples.blob()),
};
describe("createEdge", () => {
Object.keys(edgeExamples).forEach((example) => {
it(`works for ${JSON.stringify(example)}`, () => {
const instance = edgeExamples[example]();
expect(edgeToParts(instance)).toMatchSnapshot();
});
});
});
describe("`toRaw` after `fromRaw` is identity", () => {
Object.keys(edgeExamples).forEach((example) => {
it(example, () => {
const baseAddress: EdgeAddressT = edgeExamples[example]().address;
const instance: GE.RawAddress = (baseAddress: any);
expect(toRaw(fromRaw(instance))).toEqual(instance);
});
});
});
describe("`fromRaw` after `toRaw` is identity", () => {
Object.keys(edgeExamples).forEach((example) => {
it(example, () => {
const baseAddress: EdgeAddressT = edgeExamples[example]().address;
const instance: GE.RawAddress = (baseAddress: any);
const structured: GE.StructuredAddress = fromRaw(instance);
expect(fromRaw(toRaw(structured))).toEqual(structured);
});
});
});
});

View File

@ -48,6 +48,13 @@ export type TreeEntryAddress = {|
+name: string,
|};
// A tree entry has contents with one of the following types of
// addresses.
export type TreeEntryContentsAddress =
| BlobAddress
| TreeAddress
| SubmoduleCommitAddress;
export type StructuredAddress =
| BlobAddress
| CommitAddress