mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-26 11:15:17 +00:00
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:
parent
7c1b3ca835
commit
448fb3e1a8
140
src/v3/plugins/git/__snapshots__/edges.test.js.snap
Normal file
140
src/v3/plugins/git/__snapshots__/edges.test.js.snap
Normal 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
244
src/v3/plugins/git/edges.js
Normal 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);
|
||||
}
|
||||
}
|
85
src/v3/plugins/git/edges.test.js
Normal file
85
src/v3/plugins/git/edges.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user