Add BECOMES edges in the Git graph (#203)

Summary:
If a commit causes a tree entry to change hash while keeping the same
name, we now add a BECOMES edge between the corresponding entries.

Test Plan:
Snapshot changes are readable enough to manually verify. Programmatic
tests also added.

wchargin-branch: graph-becomes-edges
This commit is contained in:
William Chargin 2018-05-03 14:16:18 -07:00 committed by GitHub
parent e9ecb8c608
commit 315f66cc4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 3 deletions

View File

@ -1136,6 +1136,64 @@ Object {
"type": "COMMIT",
},
},
"{\\"id\\":\\"{\\\\\\"childCommit\\\\\\":\\\\\\"69c5aad50eec8f2a0a07c988c3b283a6490eb45b\\\\\\",\\\\\\"parentCommit\\\\\\":\\\\\\"e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc\\\\\\",\\\\\\"path\\\\\\":[\\\\\\"src\\\\\\",\\\\\\"quantum_gravity.py\\\\\\"]}\\",\\"pluginName\\":\\"sourcecred/git-beta\\",\\"type\\":\\"BECOMES\\"}": Object {
"dst": Object {
"id": "78fc9c83023386854c6bfdc5761c0e58f68e226f:quantum_gravity.py",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
"payload": Object {
"childCommit": "69c5aad50eec8f2a0a07c988c3b283a6490eb45b",
"parentCommit": "e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc",
"path": Array [
"src",
"quantum_gravity.py",
],
},
"src": Object {
"id": "7b79d579b62994faba3b69fdf8aa442586c32681:quantum_gravity.py",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
},
"{\\"id\\":\\"{\\\\\\"childCommit\\\\\\":\\\\\\"69c5aad50eec8f2a0a07c988c3b283a6490eb45b\\\\\\",\\\\\\"parentCommit\\\\\\":\\\\\\"e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc\\\\\\",\\\\\\"path\\\\\\":[\\\\\\"src\\\\\\"]}\\",\\"pluginName\\":\\"sourcecred/git-beta\\",\\"type\\":\\"BECOMES\\"}": Object {
"dst": Object {
"id": "bbf3b8b3d26a4f884b5c022d46851f593d329192:src",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
"payload": Object {
"childCommit": "69c5aad50eec8f2a0a07c988c3b283a6490eb45b",
"parentCommit": "e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc",
"path": Array [
"src",
],
},
"src": Object {
"id": "819fc546cea489476ce8dc90785e9ba7753d0a8f:src",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
},
"{\\"id\\":\\"{\\\\\\"childCommit\\\\\\":\\\\\\"e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc\\\\\\",\\\\\\"parentCommit\\\\\\":\\\\\\"d160cca97611e9dfed642522ad44408d0292e8ea\\\\\\",\\\\\\"path\\\\\\":[\\\\\\"pygravitydefier\\\\\\"]}\\",\\"pluginName\\":\\"sourcecred/git-beta\\",\\"type\\":\\"BECOMES\\"}": Object {
"dst": Object {
"id": "819fc546cea489476ce8dc90785e9ba7753d0a8f:pygravitydefier",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
"payload": Object {
"childCommit": "e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc",
"parentCommit": "d160cca97611e9dfed642522ad44408d0292e8ea",
"path": Array [
"pygravitydefier",
],
},
"src": Object {
"id": "569e1d383759903134df75230d63c0090196d4cb:pygravitydefier",
"pluginName": "sourcecred/git-beta",
"type": "TREE_ENTRY",
},
},
},
"nodes": Object {
"{\\"id\\":\\"0fb31858c8e3710be77e1dbb8880acf8a5543d82\\",\\"pluginName\\":\\"sourcecred/git-beta\\",\\"type\\":\\"BLOB\\"}": Object {

View File

@ -2,6 +2,7 @@
import type {Edge, Node} from "../../core/graph";
import type {
BecomesEdgePayload,
BlobNodePayload,
Commit,
EdgePayload,
@ -17,6 +18,7 @@ import type {
} from "./types";
import {Graph, edgeID} from "../../core/graph";
import {
BECOMES_EDGE_TYPE,
BLOB_NODE_TYPE,
COMMIT_NODE_TYPE,
HAS_CONTENTS_EDGE_TYPE,
@ -26,6 +28,7 @@ import {
SUBMODULE_COMMIT_NODE_TYPE,
TREE_ENTRY_NODE_TYPE,
TREE_NODE_TYPE,
becomesEdgeId,
hasParentEdgeId,
includesEdgeId,
submoduleCommitId,
@ -45,6 +48,7 @@ class GitGraphCreator {
...Object.keys(repository.trees).map((hash) =>
this.treeGraph(repository.trees[hash], treeAndNameToSubmoduleUrls)
),
this.becomesEdges(repository),
];
return graphs.reduce((g, h) => Graph.mergeConservative(g, h), new Graph());
}
@ -194,6 +198,34 @@ class GitGraphCreator {
});
return result;
}
becomesEdges(repository: Repository): Graph<NodePayload, EdgePayload> {
const result = new Graph();
for (const {
childCommit,
parentCommit,
becomesEdge: {from, to, path},
} of findBecomesEdges(repository)) {
result.addEdge(
({
address: _makeAddress(
BECOMES_EDGE_TYPE,
becomesEdgeId(childCommit, parentCommit, path)
),
src: _makeAddress(
TREE_ENTRY_NODE_TYPE,
treeEntryId(from.tree, from.name)
),
dst: _makeAddress(
TREE_ENTRY_NODE_TYPE,
treeEntryId(to.tree, to.name)
),
payload: {childCommit, parentCommit, path},
}: Edge<BecomesEdgePayload>)
);
}
return result;
}
}
export type BecomesEdge = {|

View File

@ -2,14 +2,18 @@
import cloneDeep from "lodash.clonedeep";
import type {Address} from "../../core/address";
import type {Edge} from "../../core/graph";
import type {BecomesEdge} from "./createGraph";
import type {Hash, Tree} from "./types";
import type {BecomesEdgePayload, Hash, Tree} from "./types";
import {_makeAddress} from "./address";
import {
createGraph,
findBecomesEdges,
findBecomesEdgesForCommits,
} from "./createGraph";
import {
BECOMES_EDGE_TYPE,
BLOB_NODE_TYPE,
COMMIT_NODE_TYPE,
GIT_PLUGIN_NAME,
@ -143,11 +147,73 @@ describe("createGraph", () => {
direction: "OUT",
})
).toHaveLength(1);
expect(graph.neighborhood(entryAddress)).toHaveLength(2);
const becomesCount = graph.neighborhood(entryAddress, {
edgeType: BECOMES_EDGE_TYPE,
}).length;
["OUT", "IN"].forEach((direction) => {
expect(
graph.neighborhood(entryAddress, {
edgeType: BECOMES_EDGE_TYPE,
direction,
}).length
).toBeLessThanOrEqual(1);
});
expect(graph.neighborhood(entryAddress)).toHaveLength(becomesCount + 2);
});
});
});
it('has "becomes" edges with valid paths', () => {
const data = makeData();
const graph = createGraph(data);
const becomings: $ReadOnlyArray<Edge<BecomesEdgePayload>> = graph
.edges({type: BECOMES_EDGE_TYPE})
.map((edge) => ((edge: Edge<any>): Edge<BecomesEdgePayload>));
expect(becomings).not.toHaveLength(0);
becomings.forEach((edge) => {
expect(edge.dst).not.toEqual(edge.src);
const expectedPayload = {
name: edge.payload.path[edge.payload.path.length - 1],
};
expect(graph.node(edge.src)).toEqual(
expect.objectContaining({payload: expectedPayload})
);
expect(graph.node(edge.dst)).toEqual(
expect.objectContaining({payload: expectedPayload})
);
const {payload: {childCommit, parentCommit, path}} = edge;
expect(path).not.toHaveLength(0);
expect(data.commits[childCommit].parentHashes).toEqual(
expect.arrayContaining([parentCommit])
);
function expectedTreeEntryAddress(commit: Hash): Address {
const {tree, name} = path.slice(1).reduce(
({tree, name}, newName) => {
if (!(tree in data.trees)) {
throw new Error(
"Unexpected leaf along " +
JSON.stringify(path) +
" from " +
commit
);
}
return {
tree: data.trees[tree].entries[name].hash,
name: newName,
};
},
{tree: data.commits[commit].treeHash, name: path[0]}
);
return _makeAddress(TREE_ENTRY_NODE_TYPE, treeEntryId(tree, name));
}
const parentEntryAddress = expectedTreeEntryAddress(parentCommit);
const childEntryAddress = expectedTreeEntryAddress(childCommit);
expect(parentEntryAddress).toEqual(edge.src);
expect(childEntryAddress).toEqual(edge.dst);
});
});
describe("has specific paths:", () => {
const headCommitHash = "3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f";
if (makeData().commits[headCommitHash] == null) {

View File

@ -1,5 +1,7 @@
// @flow
import stringify from "json-stable-stringify";
export const GIT_PLUGIN_NAME = "sourcecred/git-beta";
// Logical types
@ -112,7 +114,18 @@ export function includesEdgeId(treeSha: string, name: string): string {
// TreeEntryNode -> TreeEntryNode
export const BECOMES_EDGE_TYPE: "BECOMES" = "BECOMES";
export type BecomesEdgePayload = {||};
export type BecomesEdgePayload = {|
+childCommit: Hash,
+parentCommit: Hash,
+path: $ReadOnlyArray<string>,
|};
export function becomesEdgeId(
childCommit: Hash,
parentCommit: Hash,
path: $ReadOnlyArray<string>
) {
return stringify({childCommit, parentCommit, path});
}
// TreeEntryNode -> BlobNode | TreeNode
export const HAS_CONTENTS_EDGE_TYPE: "HAS_CONTENTS" = "HAS_CONTENTS";