Implement a Git graph view (#415)
Summary: Similar in structure to the GitHub graph view. Test Plan: Unit tests added, with full coverage. wchargin-branch: git-graph-view
This commit is contained in:
parent
0522894a8d
commit
dd83d7b4ab
|
@ -8,6 +8,7 @@ import {
|
||||||
findBecomesEdges,
|
findBecomesEdges,
|
||||||
findBecomesEdgesForCommits,
|
findBecomesEdgesForCommits,
|
||||||
} from "./createGraph";
|
} from "./createGraph";
|
||||||
|
import {GraphView} from "./graphView";
|
||||||
import type {Hash, Tree} from "./types";
|
import type {Hash, Tree} from "./types";
|
||||||
|
|
||||||
const makeData = () => cloneDeep(require("./demoData/example-git"));
|
const makeData = () => cloneDeep(require("./demoData/example-git"));
|
||||||
|
@ -17,6 +18,11 @@ describe("plugins/git/createGraph", () => {
|
||||||
it("processes a simple repository", () => {
|
it("processes a simple repository", () => {
|
||||||
expect(createGraph(makeData())).toMatchSnapshot();
|
expect(createGraph(makeData())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("satisfies the GraphView invariants", () => {
|
||||||
|
const graph = createGraph(makeData());
|
||||||
|
expect(() => new GraphView(graph)).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findBecomesEdgesForCommits", () => {
|
describe("findBecomesEdgesForCommits", () => {
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
type EdgeAddressT,
|
||||||
|
type NeighborsOptions,
|
||||||
|
type NodeAddressT,
|
||||||
|
Direction,
|
||||||
|
Graph,
|
||||||
|
NodeAddress,
|
||||||
|
edgeToString,
|
||||||
|
} from "../../core/graph";
|
||||||
|
|
||||||
|
import * as GN from "./nodes";
|
||||||
|
import * as GE from "./edges";
|
||||||
|
|
||||||
|
export class GraphView {
|
||||||
|
_graph: Graph;
|
||||||
|
|
||||||
|
constructor(graph: Graph): void {
|
||||||
|
this._graph = graph;
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
}
|
||||||
|
|
||||||
|
*_neighbors<T: GN.StructuredAddress>(
|
||||||
|
node: GN.StructuredAddress,
|
||||||
|
options: NeighborsOptions
|
||||||
|
): Iterator<T> {
|
||||||
|
if (!NodeAddress.hasPrefix(options.nodePrefix, GN._Prefix.base)) {
|
||||||
|
throw new Error(`_neighbors must filter to Git nodes`);
|
||||||
|
}
|
||||||
|
const rawNode: GN.RawAddress = GN.toRaw(node);
|
||||||
|
for (const neighbor of this._graph.neighbors(rawNode, options)) {
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
yield ((GN.fromRaw(
|
||||||
|
(((neighbor.node: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): any): T);
|
||||||
|
}
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
}
|
||||||
|
|
||||||
|
graph(): Graph {
|
||||||
|
const result = this._graph;
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
commits(): Iterator<GN.CommitAddress> {
|
||||||
|
const result = this._commits();
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*_commits(): Iterator<GN.CommitAddress> {
|
||||||
|
for (const node of this._graph.nodes({prefix: GN._Prefix.commit})) {
|
||||||
|
const rawAddress: GN.RawAddress = ((node: NodeAddressT): any);
|
||||||
|
const commit: GN.CommitAddress = (GN.fromRaw(rawAddress): any);
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
yield commit;
|
||||||
|
}
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
}
|
||||||
|
|
||||||
|
tree(commit: GN.CommitAddress): GN.TreeAddress {
|
||||||
|
const result: GN.TreeAddress = Array.from(
|
||||||
|
this._neighbors(commit, {
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: GN._Prefix.tree,
|
||||||
|
edgePrefix: GE._Prefix.hasTree,
|
||||||
|
})
|
||||||
|
)[0];
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
parents(commit: GN.CommitAddress): Iterator<GN.CommitAddress> {
|
||||||
|
const result: Iterator<GN.CommitAddress> = this._neighbors(commit, {
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: GN._Prefix.commit,
|
||||||
|
edgePrefix: GE._Prefix.hasParent,
|
||||||
|
});
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(tree: GN.TreeAddress): Iterator<GN.TreeEntryAddress> {
|
||||||
|
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(tree, {
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: GN._Prefix.treeEntry,
|
||||||
|
edgePrefix: GE._Prefix.includes,
|
||||||
|
});
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
contents(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryContentsAddress> {
|
||||||
|
const result: Iterator<GN.TreeEntryContentsAddress> = this._neighbors(
|
||||||
|
entry,
|
||||||
|
{
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: GN._Prefix.base, // multiple kinds
|
||||||
|
edgePrefix: GE._Prefix.hasContents,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
evolvesTo(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryAddress> {
|
||||||
|
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(entry, {
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: GN._Prefix.treeEntry,
|
||||||
|
edgePrefix: GE._Prefix.becomes,
|
||||||
|
});
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
evolvesFrom(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryAddress> {
|
||||||
|
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(entry, {
|
||||||
|
direction: Direction.IN,
|
||||||
|
nodePrefix: GN._Prefix.treeEntry,
|
||||||
|
edgePrefix: GE._Prefix.becomes,
|
||||||
|
});
|
||||||
|
this._maybeCheckInvariants();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_maybeCheckInvariants() {
|
||||||
|
if (process.env.NODE_ENV === "test") {
|
||||||
|
// TODO(perf): If this method becomes really slow, we can disable
|
||||||
|
// it on specific tests wherein we construct large graphs.
|
||||||
|
this.checkInvariants();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInvariants() {
|
||||||
|
// All Git nodes and edges must have valid Git addresses.
|
||||||
|
for (const node of this._graph.nodes({prefix: GN._Prefix.base})) {
|
||||||
|
GN.fromRaw((((node: NodeAddressT): any): GN.RawAddress));
|
||||||
|
}
|
||||||
|
// (Edges are checked down below.)
|
||||||
|
|
||||||
|
// All Git edges must have `src` and `dst` of specific types.
|
||||||
|
type EdgeInvariant = {|
|
||||||
|
+prefix: GE.RawAddress,
|
||||||
|
+homs: $ReadOnlyArray<{|
|
||||||
|
+srcPrefix: NodeAddressT,
|
||||||
|
+dstPrefix: NodeAddressT,
|
||||||
|
|}>,
|
||||||
|
|};
|
||||||
|
const edgeInvariants = {
|
||||||
|
[GE.HAS_TREE_TYPE]: {
|
||||||
|
prefix: GE._Prefix.hasTree,
|
||||||
|
homs: [{srcPrefix: GN._Prefix.commit, dstPrefix: GN._Prefix.tree}],
|
||||||
|
},
|
||||||
|
[GE.HAS_PARENT_TYPE]: {
|
||||||
|
prefix: GE._Prefix.hasParent,
|
||||||
|
homs: [{srcPrefix: GN._Prefix.commit, dstPrefix: GN._Prefix.commit}],
|
||||||
|
},
|
||||||
|
[GE.INCLUDES_TYPE]: {
|
||||||
|
prefix: GE._Prefix.includes,
|
||||||
|
homs: [{srcPrefix: GN._Prefix.tree, dstPrefix: GN._Prefix.treeEntry}],
|
||||||
|
},
|
||||||
|
[GE.BECOMES_TYPE]: {
|
||||||
|
prefix: GE._Prefix.becomes,
|
||||||
|
homs: [
|
||||||
|
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.treeEntry},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[GE.HAS_CONTENTS_TYPE]: {
|
||||||
|
prefix: GE._Prefix.hasContents,
|
||||||
|
homs: [
|
||||||
|
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.blob},
|
||||||
|
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.tree},
|
||||||
|
{
|
||||||
|
srcPrefix: GN._Prefix.treeEntry,
|
||||||
|
dstPrefix: GN._Prefix.submoduleCommit,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const edge of this._graph.edges({
|
||||||
|
addressPrefix: GE._Prefix.base,
|
||||||
|
srcPrefix: NodeAddress.empty,
|
||||||
|
dstPrefix: NodeAddress.empty,
|
||||||
|
})) {
|
||||||
|
const address = GE.fromRaw(
|
||||||
|
(((edge.address: EdgeAddressT): any): GE.RawAddress)
|
||||||
|
);
|
||||||
|
const invariant: EdgeInvariant = edgeInvariants[address.type];
|
||||||
|
if (invariant == null) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing invariant definition for: ${String(address.type)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!invariant.homs.some(
|
||||||
|
({srcPrefix, dstPrefix}) =>
|
||||||
|
NodeAddress.hasPrefix(edge.src, srcPrefix) &&
|
||||||
|
NodeAddress.hasPrefix(edge.dst, dstPrefix)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(`invariant violation: bad hom: ${edgeToString(edge)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each commits must have a unique and properly named HAS_TREE edge.
|
||||||
|
for (const rawNode of this._graph.nodes({prefix: GN._Prefix.commit})) {
|
||||||
|
const treeNeighbors = Array.from(
|
||||||
|
this._graph.neighbors(rawNode, {
|
||||||
|
direction: Direction.OUT,
|
||||||
|
nodePrefix: NodeAddress.empty,
|
||||||
|
edgePrefix: GE._Prefix.hasTree,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (treeNeighbors.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
"invariant violation: commit should have 1 tree, " +
|
||||||
|
`but has ${treeNeighbors.length}: ${NodeAddress.toString(rawNode)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const rawEdge = treeNeighbors[0].edge;
|
||||||
|
const edge: GE.HasTreeAddress = (GE.fromRaw(
|
||||||
|
(((rawEdge.address: EdgeAddressT): any): GE.RawAddress)
|
||||||
|
): any);
|
||||||
|
const node: GN.CommitAddress = ((GN.fromRaw(
|
||||||
|
(((rawNode: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
if (node.hash !== edge.commit.hash) {
|
||||||
|
throw new Error(
|
||||||
|
`invariant violation: bad HAS_TREE edge: ${edgeToString(rawEdge)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All HAS_PARENT edges must map between between the correct commits.
|
||||||
|
for (const edge of this._graph.edges({
|
||||||
|
addressPrefix: GE._Prefix.hasParent,
|
||||||
|
srcPrefix: NodeAddress.empty,
|
||||||
|
dstPrefix: NodeAddress.empty,
|
||||||
|
})) {
|
||||||
|
const src: GN.CommitAddress = ((GN.fromRaw(
|
||||||
|
(((edge.src: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const dst: GN.CommitAddress = ((GN.fromRaw(
|
||||||
|
(((edge.dst: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const expectedEdge = GE.createEdge.hasParent(src, dst);
|
||||||
|
if (edge.address !== expectedEdge.address) {
|
||||||
|
throw new Error(
|
||||||
|
`invariant violation: bad HAS_PARENT edge: ${edgeToString(edge)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each tree entry must have a unique and properly named INCLUDES edge.
|
||||||
|
for (const rawNode of this._graph.nodes({prefix: GN._Prefix.treeEntry})) {
|
||||||
|
const treeNeighbors = Array.from(
|
||||||
|
this._graph.neighbors(rawNode, {
|
||||||
|
direction: Direction.IN,
|
||||||
|
nodePrefix: NodeAddress.empty,
|
||||||
|
edgePrefix: GE._Prefix.includes,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (treeNeighbors.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
"invariant violation: tree entry should have 1 inclusion, " +
|
||||||
|
`but has ${treeNeighbors.length}: ${NodeAddress.toString(rawNode)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const edge = treeNeighbors[0].edge;
|
||||||
|
const tree: GN.TreeAddress = ((GN.fromRaw(
|
||||||
|
(((edge.src: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const treeEntry: GN.TreeEntryAddress = ((GN.fromRaw(
|
||||||
|
(((edge.dst: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const expectedEdge = GE.createEdge.includes(tree, treeEntry);
|
||||||
|
if (edge.address !== expectedEdge.address) {
|
||||||
|
throw new Error(
|
||||||
|
`invariant violation: bad INCLUDES edge: ${edgeToString(edge)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All BECOMES edges must map between between the correct tree entries.
|
||||||
|
for (const edge of this._graph.edges({
|
||||||
|
addressPrefix: GE._Prefix.becomes,
|
||||||
|
srcPrefix: NodeAddress.empty,
|
||||||
|
dstPrefix: NodeAddress.empty,
|
||||||
|
})) {
|
||||||
|
const src: GN.TreeEntryAddress = ((GN.fromRaw(
|
||||||
|
(((edge.src: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const dst: GN.TreeEntryAddress = ((GN.fromRaw(
|
||||||
|
(((edge.dst: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const expectedEdge = GE.createEdge.becomes(src, dst);
|
||||||
|
if (edge.address !== expectedEdge.address) {
|
||||||
|
throw new Error(
|
||||||
|
`invariant violation: bad BECOMES edge: ${edgeToString(edge)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All HAS_CONTENTS edges must be properly named.
|
||||||
|
for (const edge of this._graph.edges({
|
||||||
|
addressPrefix: GE._Prefix.hasContents,
|
||||||
|
srcPrefix: NodeAddress.empty,
|
||||||
|
dstPrefix: NodeAddress.empty,
|
||||||
|
})) {
|
||||||
|
const src: GN.TreeEntryAddress = ((GN.fromRaw(
|
||||||
|
(((edge.src: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const dst: GN.TreeEntryContentsAddress = ((GN.fromRaw(
|
||||||
|
(((edge.dst: NodeAddressT): any): GN.RawAddress)
|
||||||
|
): GN.StructuredAddress): any);
|
||||||
|
const expectedEdge = GE.createEdge.hasContents(src, dst);
|
||||||
|
if (edge.address !== expectedEdge.address) {
|
||||||
|
throw new Error(
|
||||||
|
`invariant violation: bad HAS_CONTENTS edge: ${edgeToString(edge)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
|
||||||
|
import {EdgeAddress, Graph, NodeAddress, edgeToString} from "../../core/graph";
|
||||||
|
import {createGraph} from "./createGraph";
|
||||||
|
import {GraphView} from "./graphView";
|
||||||
|
import type {Repository} from "./types";
|
||||||
|
|
||||||
|
import * as GE from "./edges";
|
||||||
|
import * as GN from "./nodes";
|
||||||
|
|
||||||
|
const makeData = (): Repository => cloneDeep(require("./demoData/example-git"));
|
||||||
|
const makeGraph = () => createGraph(makeData());
|
||||||
|
const makeView = () => new GraphView(makeGraph());
|
||||||
|
|
||||||
|
describe("plugins/git/graphView", () => {
|
||||||
|
const view = makeView();
|
||||||
|
function expectEqualMultisets(x: Iterable<mixed>, y: Iterable<mixed>) {
|
||||||
|
const ax = Array.from(x);
|
||||||
|
const ay = Array.from(y);
|
||||||
|
expect(ax).toEqual(expect.arrayContaining(ay));
|
||||||
|
expect(ay).toEqual(expect.arrayContaining(ax));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GraphView", () => {
|
||||||
|
it("#graph returns the provided graph", () => {
|
||||||
|
const g1 = new Graph();
|
||||||
|
const g2 = makeGraph();
|
||||||
|
expect(new GraphView(g1).graph()).toBe(g1);
|
||||||
|
expect(new GraphView(g2).graph()).toBe(g2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#commits yields all commits", () => {
|
||||||
|
const expectedHashes = Object.keys(makeData().commits);
|
||||||
|
const actualHashes = Array.from(view.commits()).map((a) => a.hash);
|
||||||
|
expectEqualMultisets(actualHashes, expectedHashes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#tree yields the correct tree for each commit", () => {
|
||||||
|
const commits = makeData().commits;
|
||||||
|
for (const commitHash of Object.keys(commits)) {
|
||||||
|
const commit = commits[commitHash];
|
||||||
|
const node: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: commitHash};
|
||||||
|
expect(view.tree(node).hash).toEqual(commit.treeHash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#parents yields the correct parents for each commit", () => {
|
||||||
|
const commits = makeData().commits;
|
||||||
|
expect(Object.keys(commits)).not.toEqual([]);
|
||||||
|
for (const commitHash of Object.keys(commits)) {
|
||||||
|
const commit = commits[commitHash];
|
||||||
|
const node: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: commitHash};
|
||||||
|
const expectedParents = commit.parentHashes.slice();
|
||||||
|
const actualParents = Array.from(view.parents(node)).map((a) => a.hash);
|
||||||
|
expectEqualMultisets(actualParents, expectedParents);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#entries yields the correct entries for each tree", () => {
|
||||||
|
const trees = makeData().trees;
|
||||||
|
expect(Object.keys(trees)).not.toEqual([]);
|
||||||
|
for (const treeHash of Object.keys(trees)) {
|
||||||
|
const tree = trees[treeHash];
|
||||||
|
const node: GN.TreeAddress = {type: GN.TREE_TYPE, hash: treeHash};
|
||||||
|
const actualEntries = Array.from(view.entries(node));
|
||||||
|
const expectedEntries = Object.keys(tree.entries).map((name) => ({
|
||||||
|
type: "TREE_ENTRY",
|
||||||
|
treeHash,
|
||||||
|
name,
|
||||||
|
}));
|
||||||
|
expectEqualMultisets(actualEntries, expectedEntries);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#contents yields the correct contents", () => {
|
||||||
|
const entryByName = (name): GN.TreeEntryAddress => ({
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
it("for blobs", () => {
|
||||||
|
const actual = Array.from(view.contents(entryByName("science.txt")));
|
||||||
|
const expected: $ReadOnlyArray<GN.BlobAddress> = [
|
||||||
|
{
|
||||||
|
type: GN.BLOB_TYPE,
|
||||||
|
hash: "f1f2514ca6d7a6a1a0511957021b1995bf9ace1c",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
it("for trees", () => {
|
||||||
|
const actual = Array.from(view.contents(entryByName("src")));
|
||||||
|
const expected: $ReadOnlyArray<GN.TreeAddress> = [
|
||||||
|
{
|
||||||
|
type: GN.TREE_TYPE,
|
||||||
|
hash: "78fc9c83023386854c6bfdc5761c0e58f68e226f",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
it("for submodule commits", () => {
|
||||||
|
const actual = Array.from(
|
||||||
|
view.contents(entryByName("pygravitydefier"))
|
||||||
|
);
|
||||||
|
const expected: $ReadOnlyArray<GN.SubmoduleCommitAddress> = [
|
||||||
|
{
|
||||||
|
type: GN.SUBMODULE_COMMIT_TYPE,
|
||||||
|
submoduleUrl:
|
||||||
|
"https://github.com/sourcecred/example-git-submodule.git",
|
||||||
|
commitHash: "29ef158bc982733e2ba429fcf73e2f7562244188",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#evolvesTo yields the correct entries", () => {
|
||||||
|
const v0: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "569e1d383759903134df75230d63c0090196d4cb",
|
||||||
|
name: "pygravitydefier",
|
||||||
|
};
|
||||||
|
const v1: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "819fc546cea489476ce8dc90785e9ba7753d0a8f",
|
||||||
|
name: "pygravitydefier",
|
||||||
|
};
|
||||||
|
expect(Array.from(view.evolvesTo(v0))).toEqual([v1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#evolvesFrom yields the correct entries", () => {
|
||||||
|
const v0: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "569e1d383759903134df75230d63c0090196d4cb",
|
||||||
|
name: "pygravitydefier",
|
||||||
|
};
|
||||||
|
const v1: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "819fc546cea489476ce8dc90785e9ba7753d0a8f",
|
||||||
|
name: "pygravitydefier",
|
||||||
|
};
|
||||||
|
expect(Array.from(view.evolvesFrom(v1))).toEqual([v0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("invariants", () => {
|
||||||
|
it("check for malformed nodes", () => {
|
||||||
|
const node = GN._gitAddress("wat");
|
||||||
|
const g = new Graph().addNode(node);
|
||||||
|
const expected = "Bad address: " + NodeAddress.toString(node);
|
||||||
|
expect(() => new GraphView(g)).toThrow(expected);
|
||||||
|
});
|
||||||
|
it("check for malformed edges", () => {
|
||||||
|
const c1: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
|
||||||
|
const c2: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c2"};
|
||||||
|
const edge = {
|
||||||
|
address: EdgeAddress.append(GE._Prefix.base, "wat"),
|
||||||
|
src: GN.toRaw(c1),
|
||||||
|
dst: GN.toRaw(c2),
|
||||||
|
};
|
||||||
|
const g = new Graph()
|
||||||
|
.addNode(GN.toRaw(c1))
|
||||||
|
.addNode(GN.toRaw(c2))
|
||||||
|
.addEdge(edge);
|
||||||
|
const expected = "Bad address: " + EdgeAddress.toString(edge.address);
|
||||||
|
expect(() => new GraphView(g)).toThrow(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("check HAS_TREE edges", () => {
|
||||||
|
const commit: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
|
||||||
|
const otherCommit: GN.CommitAddress = {
|
||||||
|
type: GN.COMMIT_TYPE,
|
||||||
|
hash: "c2",
|
||||||
|
};
|
||||||
|
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
|
||||||
|
const otherTree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t2"};
|
||||||
|
const edge = GE.createEdge.hasTree(commit, tree);
|
||||||
|
const otherEdge = GE.createEdge.hasTree(otherCommit, otherTree);
|
||||||
|
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
|
||||||
|
const baseGraph = () =>
|
||||||
|
new Graph()
|
||||||
|
.addNode(foreignNode)
|
||||||
|
.addNode(GN.toRaw(commit))
|
||||||
|
.addNode(GN.toRaw(tree))
|
||||||
|
.addNode(GN.toRaw(otherTree));
|
||||||
|
it("for proper src", () => {
|
||||||
|
const badEdge = {...edge, src: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for proper dst", () => {
|
||||||
|
const badEdge = {...edge, dst: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for existence", () => {
|
||||||
|
const g = baseGraph();
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: commit should have 1 tree, but has 0: " +
|
||||||
|
NodeAddress.toString(GN.toRaw(commit))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for correctness", () => {
|
||||||
|
const badEdge = {...otherEdge, src: edge.src};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad HAS_TREE edge: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("check HAS_PARENT edges", () => {
|
||||||
|
const c1: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
|
||||||
|
const c2: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c2"};
|
||||||
|
const c3: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c3"};
|
||||||
|
const e2 = GE.createEdge.hasParent(c1, c2);
|
||||||
|
const e3 = GE.createEdge.hasParent(c1, c3);
|
||||||
|
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
|
||||||
|
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
|
||||||
|
const baseGraph = () =>
|
||||||
|
new Graph()
|
||||||
|
.addNode(foreignNode)
|
||||||
|
.addNode(GN.toRaw(c1))
|
||||||
|
.addNode(GN.toRaw(c2))
|
||||||
|
.addNode(GN.toRaw(c3))
|
||||||
|
.addNode(GN.toRaw(tree))
|
||||||
|
.addEdge(GE.createEdge.hasTree(c1, tree))
|
||||||
|
.addEdge(GE.createEdge.hasTree(c2, tree))
|
||||||
|
.addEdge(GE.createEdge.hasTree(c3, tree));
|
||||||
|
it("for proper src", () => {
|
||||||
|
const badEdge = {...e2, src: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for proper dst", () => {
|
||||||
|
const badEdge = {...e2, dst: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for correctness", () => {
|
||||||
|
const badEdge = {...e2, src: GN.toRaw(c2), dst: GN.toRaw(c3)};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad HAS_PARENT edge: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("allowing multiple parents", () => {
|
||||||
|
const g = baseGraph()
|
||||||
|
.addEdge(e2)
|
||||||
|
.addEdge(e3);
|
||||||
|
expect(() => new GraphView(g)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("check INCLUDES edges", () => {
|
||||||
|
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
|
||||||
|
const entry: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: tree.hash,
|
||||||
|
name: "tree_entry.txt",
|
||||||
|
};
|
||||||
|
const anotherEntry: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: tree.hash,
|
||||||
|
name: "wat.txt",
|
||||||
|
};
|
||||||
|
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
|
||||||
|
const edge = GE.createEdge.includes(tree, entry);
|
||||||
|
const baseGraph = () =>
|
||||||
|
new Graph()
|
||||||
|
.addNode(foreignNode)
|
||||||
|
.addNode(GN.toRaw(tree))
|
||||||
|
.addNode(GN.toRaw(entry));
|
||||||
|
it("for proper src", () => {
|
||||||
|
const badEdge = {...edge, src: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for proper dst", () => {
|
||||||
|
const badEdge = {...edge, dst: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for existence", () => {
|
||||||
|
const g = baseGraph();
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: " +
|
||||||
|
"tree entry should have 1 inclusion, but has 0: " +
|
||||||
|
NodeAddress.toString(GN.toRaw(entry))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for correctness", () => {
|
||||||
|
const badEdge = {
|
||||||
|
...edge,
|
||||||
|
address: GE.createEdge.includes(tree, anotherEntry).address,
|
||||||
|
};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad INCLUDES edge: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("check BECOMES edges", () => {
|
||||||
|
const t1: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
|
||||||
|
const t2: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
|
||||||
|
const te1: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "t1",
|
||||||
|
name: "foo",
|
||||||
|
};
|
||||||
|
const te2: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "t2",
|
||||||
|
name: "foo",
|
||||||
|
};
|
||||||
|
const te3: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "t2",
|
||||||
|
name: "bar",
|
||||||
|
};
|
||||||
|
const e2 = GE.createEdge.becomes(te1, te2);
|
||||||
|
const e3 = GE.createEdge.becomes(te1, te3);
|
||||||
|
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
|
||||||
|
const baseGraph = () =>
|
||||||
|
new Graph()
|
||||||
|
.addNode(foreignNode)
|
||||||
|
.addNode(GN.toRaw(t1))
|
||||||
|
.addNode(GN.toRaw(t2))
|
||||||
|
.addNode(GN.toRaw(te1))
|
||||||
|
.addNode(GN.toRaw(te2))
|
||||||
|
.addNode(GN.toRaw(te3))
|
||||||
|
.addEdge(GE.createEdge.includes(t1, te1))
|
||||||
|
.addEdge(GE.createEdge.includes(t2, te2))
|
||||||
|
.addEdge(GE.createEdge.includes(t2, te3));
|
||||||
|
it("for proper src", () => {
|
||||||
|
const badEdge = {...e2, src: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for proper dst", () => {
|
||||||
|
const badEdge = {...e2, dst: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for correctness", () => {
|
||||||
|
const badEdge = {...e2, src: GN.toRaw(te2), dst: GN.toRaw(te3)};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad BECOMES edge: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("allowing multiple edges from a single source", () => {
|
||||||
|
const g = baseGraph()
|
||||||
|
.addEdge(e2)
|
||||||
|
.addEdge(e3);
|
||||||
|
expect(() => new GraphView(g)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checks HAS_CONTENTS edges", () => {
|
||||||
|
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "ceda12"};
|
||||||
|
const te1: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "ceda12",
|
||||||
|
name: "foo",
|
||||||
|
};
|
||||||
|
const te2: GN.TreeEntryAddress = {
|
||||||
|
type: GN.TREE_ENTRY_TYPE,
|
||||||
|
treeHash: "ceda12",
|
||||||
|
name: "bar",
|
||||||
|
};
|
||||||
|
const blob: GN.BlobAddress = {type: GN.BLOB_TYPE, hash: "fish"};
|
||||||
|
const e1 = GE.createEdge.hasContents(te1, blob);
|
||||||
|
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
|
||||||
|
const baseGraph = () =>
|
||||||
|
new Graph()
|
||||||
|
.addNode(foreignNode)
|
||||||
|
.addNode(GN.toRaw(tree))
|
||||||
|
.addNode(GN.toRaw(te1))
|
||||||
|
.addNode(GN.toRaw(te2))
|
||||||
|
.addNode(GN.toRaw(blob))
|
||||||
|
.addEdge(GE.createEdge.includes(tree, te1))
|
||||||
|
.addEdge(GE.createEdge.includes(tree, te2));
|
||||||
|
it("for proper src", () => {
|
||||||
|
const badEdge = {...e1, src: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for proper dst", () => {
|
||||||
|
const badEdge = {...e1, dst: foreignNode};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad hom: " + edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("for correctness", () => {
|
||||||
|
const badEdge = {...e1, src: GN.toRaw(te2)};
|
||||||
|
const g = baseGraph().addEdge(badEdge);
|
||||||
|
expect(() => new GraphView(g)).toThrow(
|
||||||
|
"invariant violation: bad HAS_CONTENTS edge: " +
|
||||||
|
edgeToString(badEdge)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue