Add `mergeRepository` for merging Git repositories (#871)

This commit adds a utility method, `mergeRepository`, which can merge
multiple Git repository data structures. `loadGitData` has been updated
to create a merged repository and then subsequently generate a graph
from it.

Test plan:
New unit tests were added. `yarn test --full` passes. Loading a project
and viewing its git data in the cred explorer works.
This commit is contained in:
Dandelion Mané 2018-09-20 12:57:57 -07:00 committed by GitHub
parent 8442255db3
commit 12aa5b7439
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 7 deletions

View File

@ -3,10 +3,10 @@
import fs from "fs-extra";
import path from "path";
import {Graph} from "../../core/graph";
import type {Repo} from "../../core/repo";
import cloneAndLoadRepository from "./cloneAndLoadRepository";
import {createMinimalGraph} from "./createMinimalGraph";
import type {Repo} from "../../core/repo";
import {mergeRepository} from "./mergeRepository";
export type Options = {|
+repos: $ReadOnlyArray<Repo>,
@ -15,11 +15,9 @@ export type Options = {|
|};
export function loadGitData(options: Options): Promise<void> {
const graphs = options.repos.map((repo) => {
const repository = cloneAndLoadRepository(repo);
return createMinimalGraph(repository);
});
const graph = Graph.merge(graphs);
const repositories = options.repos.map((r) => cloneAndLoadRepository(r));
const repository = mergeRepository(repositories);
const graph = createMinimalGraph(repository);
const blob = JSON.stringify(graph);
const outputFilename = path.join(options.outputDirectory, "graph.json");
return fs.writeFile(outputFilename, blob);

View File

@ -0,0 +1,30 @@
// @flow
import deepEqual from "lodash.isequal";
import type {Repository} from "./types";
export function mergeRepository(
repositories: $ReadOnlyArray<Repository>
): Repository {
const newRepository = {commits: {}, trees: {}};
for (const {trees, commits} of repositories) {
for (const treeHash of Object.keys(trees)) {
const existingTree = newRepository.trees[treeHash];
if (existingTree != null && !deepEqual(existingTree, trees[treeHash])) {
throw new Error(`Conflict between trees at ${treeHash}`);
}
newRepository.trees[treeHash] = trees[treeHash];
}
for (const commitHash of Object.keys(commits)) {
const existingCommit = newRepository.commits[commitHash];
if (
existingCommit != null &&
!deepEqual(existingCommit, commits[commitHash])
) {
throw new Error(`Conflict between commits at ${commitHash}`);
}
newRepository.commits[commitHash] = commits[commitHash];
}
}
return newRepository;
}

View File

@ -0,0 +1,122 @@
//@flow
import type {Repository} from "./types";
import {mergeRepository} from "./mergeRepository";
describe("plugins/git/mergeRepository", () => {
describe("mergeRepository", () => {
const empty: Repository = Object.freeze({commits: {}, trees: {}});
const repository1: Repository = Object.freeze({
commits: {
commit1: {
hash: "commit1",
parentHashes: [],
treeHash: "tree1",
},
commit2: {
hash: "commit2",
parentHashes: ["commit1"],
treeHash: "tree2",
},
},
trees: {
tree1: {
hash: "tree1",
entries: {},
},
tree2: {
hash: "tree2",
entries: {},
},
},
});
const repository2: Repository = Object.freeze({
commits: {
commit1: {
hash: "commit1",
parentHashes: [],
treeHash: "tree1",
},
commit3: {
hash: "commit3",
parentHashes: ["commit1"],
treeHash: "tree3",
},
},
trees: {
tree1: {
hash: "tree1",
entries: {},
},
tree3: {
hash: "tree3",
entries: {},
},
},
});
it("returns empty repository with no arguments", () => {
expect(mergeRepository([])).toEqual(empty);
});
it("returns copy of the input repository when n=1", () => {
const merged = mergeRepository([repository1]);
expect(merged).toEqual(repository1);
expect(merged).not.toBe(repository1);
});
it("treats empty repository as identity", () => {
const merged = mergeRepository([empty, repository1, empty]);
expect(merged).toEqual(repository1);
});
it("merged contains every hash and tree for every constituent repository", () => {
const merged = mergeRepository([repository1, repository2]);
for (const {trees, commits} of [repository1, repository2]) {
for (const treeHash in trees) {
expect(merged.trees[treeHash]).toEqual(trees[treeHash]);
}
for (const commitHash in commits) {
expect(merged.commits[commitHash]).toEqual(commits[commitHash]);
}
}
});
it("throws an error if merging a repository with conflicting commits", () => {
const conflictingRepository: Repository = Object.freeze({
commits: {
commit1: {
hash: "commit1",
parentHashes: [],
treeHash: "tree2",
},
},
trees: {
tree2: {
hash: "tree2",
entries: {},
},
},
});
expect(() =>
mergeRepository([repository1, conflictingRepository])
).toThrowError("Conflict between commits");
});
it("throws an error if merging a repository with conflicting trees", () => {
const conflictingRepository: Repository = Object.freeze({
commits: {
commit1: {
hash: "commit1",
parentHashes: [],
treeHash: "tree1",
},
},
trees: {
tree1: {
hash: "tree1",
entries: {blob: {type: "blob", name: "blob", hash: "blob"}},
},
},
});
expect(() =>
mergeRepository([repository1, conflictingRepository])
).toThrowError("Conflict between trees");
});
});
});