Create Git graph (#406)

Summary:
This commit adds logic to create the Git graph, modeled after the GitHub
graph creator in #405. In this commit, we do not include the
corresponding porcelain; a Git `GraphView` will be added subsequently.

Kudos to @decentralion for suggesting in #187 that I write the logic to
detect BECOMES edges against the high-level data structures. Due to that
decision, the logic and tests are copied directly from the V1 code
without change, because the high-level data structures are the same. The
new code is exactly the body of the `GraphCreator` class.

Test Plan:
Verify that the new snapshot is likely equivalent to the V1 snapshot,
using the heuristic that the two graphs have the same numbers of nodes
(59) and edges (84). (I have performed this check.)

wchargin-branch: git-v3-create-graph
This commit is contained in:
William Chargin 2018-06-26 13:54:47 -07:00 committed by GitHub
parent a470f28204
commit 0522894a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 2067 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
// @flow
import {Graph} from "../../core/graph";
import * as GT from "./types";
import * as GN from "./nodes";
import * as GE from "./edges";
export function createGraph(repository: GT.Repository): Graph {
const creator = new GraphCreator();
creator.addRepository(repository);
return creator.graph;
}
class GraphCreator {
graph: Graph;
constructor() {
this.graph = new Graph();
}
addNode(a: GN.StructuredAddress) {
this.graph.addNode(GN.toRaw(a));
}
addRepository(repository: GT.Repository) {
const treeAndNameToSubmoduleUrls = this.treeAndNameToSubmoduleUrls(
repository
);
for (const treeHash of Object.keys(repository.trees)) {
this.addTree(repository.trees[treeHash], treeAndNameToSubmoduleUrls);
}
for (const commitHash of Object.keys(repository.commits)) {
this.addCommit(repository.commits[commitHash]);
}
this.addBecomesEdges(repository);
}
treeAndNameToSubmoduleUrls(repository: GT.Repository) {
const result: {[tree: GT.Hash]: {[name: string]: string[]}} = {};
Object.keys(repository.commits).forEach((commitHash) => {
const {treeHash: rootTreeHash, submoduleUrls} = repository.commits[
commitHash
];
Object.keys(submoduleUrls).forEach((path) => {
const parts = path.split("/");
const [treePath, name] = [
parts.slice(0, parts.length - 1),
parts[parts.length - 1],
];
let tree = repository.trees[rootTreeHash];
for (const pathComponent of treePath) {
tree = repository.trees[tree.entries[pathComponent].hash];
if (tree == null) {
return;
}
}
if (result[tree.hash] == null) {
result[tree.hash] = {};
}
const url = submoduleUrls[path];
if (result[tree.hash][name] == null) {
result[tree.hash][name] = [];
}
result[tree.hash][name].push(url);
});
});
return result;
}
addCommit(commit: GT.Commit) {
const node: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: commit.hash};
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: commit.treeHash};
this.graph.addNode(GN.toRaw(node));
this.graph.addNode(GN.toRaw(tree));
this.graph.addEdge(GE.createEdge.hasTree(node, tree));
for (const parentHash of commit.parentHashes) {
const parent: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: parentHash};
this.graph.addNode(GN.toRaw(parent));
this.graph.addEdge(GE.createEdge.hasParent(node, parent));
}
}
addTree(tree: GT.Tree, treeAndNameToSubmoduleUrls) {
const treeNode: GN.TreeAddress = {type: GN.TREE_TYPE, hash: tree.hash};
this.graph.addNode(GN.toRaw(treeNode));
for (const name of Object.keys(tree.entries)) {
const entry = tree.entries[name];
const entryNode: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: tree.hash,
name: entry.name,
};
this.graph.addNode(GN.toRaw(entryNode));
this.graph.addEdge(GE.createEdge.includes(treeNode, entryNode));
let targets: GN.TreeEntryContentsAddress[] = [];
switch (entry.type) {
case "blob":
targets.push({type: GN.BLOB_TYPE, hash: entry.hash});
break;
case "tree":
targets.push({type: GN.TREE_TYPE, hash: entry.hash});
break;
case "commit":
// One entry for each possible URL.
const urls = treeAndNameToSubmoduleUrls[tree.hash][name];
for (const url of urls) {
targets.push({
type: GN.SUBMODULE_COMMIT_TYPE,
submoduleUrl: url,
commitHash: entry.hash,
});
}
break;
default:
// eslint-disable-next-line no-unused-expressions
(entry.type: empty);
throw new Error(String(entry.type));
}
for (const target of targets) {
this.graph.addNode(GN.toRaw(target));
this.graph.addEdge(GE.createEdge.hasContents(entryNode, target));
}
}
}
addBecomesEdges(repository: GT.Repository) {
for (const {
becomesEdge: {from, to},
} of findBecomesEdges(repository)) {
const was: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: from.tree,
name: from.name,
};
const becomes: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: to.tree,
name: to.name,
};
this.graph.addEdge(GE.createEdge.becomes(was, becomes));
}
}
}
export type BecomesEdge = {|
+from: {|
+tree: GT.Hash,
+name: string,
|},
+to: {|
+tree: GT.Hash,
+name: string,
|},
+path: $ReadOnlyArray<string>,
|};
export function* findBecomesEdgesForCommits(
repository: GT.Repository,
childCommit: GT.Hash,
parentCommit: GT.Hash
): Iterator<BecomesEdge> {
const workUnits = [
{
path: [],
beforeTreeHash: repository.commits[parentCommit].treeHash,
afterTreeHash: repository.commits[childCommit].treeHash,
},
];
while (workUnits.length > 0) {
const {path, beforeTreeHash, afterTreeHash} = workUnits.pop();
const beforeTree = repository.trees[beforeTreeHash];
const afterTree = repository.trees[afterTreeHash];
for (const name of Object.keys(beforeTree.entries)) {
if (!(name in afterTree.entries)) {
continue;
}
const beforeEntry = beforeTree.entries[name];
const afterEntry = afterTree.entries[name];
const subpath = [...path, name];
if (beforeEntry.hash !== afterEntry.hash) {
yield {
from: {tree: beforeTreeHash, name},
to: {tree: afterTreeHash, name},
path: subpath,
};
}
if (beforeEntry.type === "tree" && afterEntry.type === "tree") {
workUnits.push({
path: subpath,
beforeTreeHash: beforeEntry.hash,
afterTreeHash: afterEntry.hash,
});
}
}
}
}
export function* findBecomesEdges(
repository: GT.Repository
): Iterator<{|
+childCommit: GT.Hash,
+parentCommit: GT.Hash,
+becomesEdge: BecomesEdge,
|}> {
for (const childCommit of Object.keys(repository.commits)) {
for (const parentCommit of repository.commits[childCommit].parentHashes) {
for (const becomesEdge of findBecomesEdgesForCommits(
repository,
childCommit,
parentCommit
)) {
yield {childCommit, parentCommit, becomesEdge};
}
}
}
}

View File

@ -0,0 +1,246 @@
// @flow
import cloneDeep from "lodash.clonedeep";
import {
type BecomesEdge,
createGraph,
findBecomesEdges,
findBecomesEdgesForCommits,
} from "./createGraph";
import type {Hash, Tree} from "./types";
const makeData = () => cloneDeep(require("./demoData/example-git"));
describe("plugins/git/createGraph", () => {
describe("createGraph", () => {
it("processes a simple repository", () => {
expect(createGraph(makeData())).toMatchSnapshot();
});
});
describe("findBecomesEdgesForCommits", () => {
function fromTrees(
beforeTree: Hash,
afterTree: Hash,
trees: {[Hash]: Tree}
): BecomesEdge[] {
const repo = {
commits: {
commit1: {
hash: "commit1",
parentHashes: [],
treeHash: beforeTree,
submoduleUrls: {},
},
commit2: {
hash: "commit2",
parentHashes: ["commit1"],
treeHash: afterTree,
submoduleUrls: {},
},
},
trees,
};
return Array.from(findBecomesEdgesForCommits(repo, "commit2", "commit1"));
}
it("works on the example repository", () => {
const data = makeData();
const childCommitHash = "69c5aad50eec8f2a0a07c988c3b283a6490eb45b";
expect(data.commits[childCommitHash]).toEqual(expect.anything());
expect(data.commits[childCommitHash].parentHashes).toHaveLength(1);
const parentCommitHash = data.commits[childCommitHash].parentHashes[0];
expect(
Array.from(
findBecomesEdgesForCommits(data, childCommitHash, parentCommitHash)
)
).toMatchSnapshot();
});
it("works on empty trees", () => {
expect(
fromTrees("tree1", "tree2", {
tree1: {
hash: "tree1",
entries: {},
},
tree2: {
hash: "tree2",
entries: {},
},
})
).toEqual([]);
});
it("finds differences and non-differences at the root", () => {
expect(
fromTrees("tree1", "tree2", {
tree1: {
hash: "tree1",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "blue",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "twelve",
},
},
},
tree2: {
hash: "tree2",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "yellow",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "twelve",
},
},
},
})
).toEqual([
{
from: {
tree: "tree1",
name: "color.txt",
},
to: {
tree: "tree2",
name: "color.txt",
},
path: ["color.txt"],
},
]);
});
it("handles cases where files of the same name appear in different trees", () => {
const result = fromTrees("tree1", "tree2", {
tree1: {
hash: "tree1",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "blue",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "twelve",
},
mirror_universe: {
type: "tree",
name: "mirror_universe",
hash: "eert1",
},
},
},
eert1: {
hash: "eert1",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "eulb",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "evlewt",
},
},
},
tree2: {
hash: "tree2",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "yellow",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "twelve",
},
mirror_universe: {
type: "tree",
name: "mirror_universe",
hash: "eert2",
},
},
},
eert2: {
hash: "eert1",
entries: {
"color.txt": {
type: "blob",
name: "color.txt",
hash: "eulb",
},
"number.txt": {
type: "blob",
name: "number.txt",
hash: "neetneves",
},
},
},
});
const expected = [
{
from: {
tree: "tree1",
name: "color.txt",
},
to: {
tree: "tree2",
name: "color.txt",
},
path: ["color.txt"],
},
{
from: {
tree: "eert1",
name: "number.txt",
},
to: {
tree: "eert2",
name: "number.txt",
},
path: ["mirror_universe", "number.txt"],
},
{
from: {
tree: "tree1",
name: "mirror_universe",
},
to: {
tree: "tree2",
name: "mirror_universe",
},
path: ["mirror_universe"],
},
];
expect(result).toEqual(expect.arrayContaining(expected));
expect(expected).toEqual(
expect.arrayContaining((result.slice(): $ReadOnlyArray<mixed>).slice())
);
});
});
describe("findBecomesEdges", () => {
it("works on the example repository", () => {
const data = makeData();
expect(Array.from(findBecomesEdges(data))).toMatchSnapshot();
});
});
});