Git: track the repositories containing each commit (#884)
This modifies the Git `Repository` data structure so that for every commit, we track the `RepoId`s of repos containing that commit. This way we will be able to do things like hyperlink to the right url for that commit. `loadRepository` has been modified to set the initial `repoId`. `mergeRepository` has been updated to ensure that it concatenates the `repoId`s properly. Tests were added for both cases. The example-git snapshot has been updated accordingly. Test plan: `yarn test --full`
This commit is contained in:
parent
b506d40efd
commit
3b962eacea
|
@ -20,7 +20,7 @@ export default function cloneAndLoadRepository(repoId: RepoId): Repository {
|
|||
const tmpdir = tmp.dirSync({unsafeCleanup: true});
|
||||
const git = localGit(tmpdir.name);
|
||||
git(["clone", cloneUrl, ".", "--quiet"]);
|
||||
const result = loadRepository(tmpdir.name, "HEAD");
|
||||
const result = loadRepository(tmpdir.name, "HEAD", repoId);
|
||||
tmpdir.removeCallback();
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,30 @@
|
|||
{
|
||||
"commitToRepoId": {
|
||||
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"69c5aad50eec8f2a0a07c988c3b283a6490eb45b": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"8d287c3bfbf8455ef30187bf5153ffc1b6eef268": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"c08ee3a4edea384d5291ffcbf06724a13ed72325": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"c2b51945e7457546912a8ce158ed9d294558d294": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"c90f6424017f787bbbaf22e4082a01355546f7e3": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"d160cca97611e9dfed642522ad44408d0292e8ea": {
|
||||
"sourcecred/example-git": true
|
||||
},
|
||||
"e8b7a8f19701cd5a25e4a097d513ead60e5f8bcc": {
|
||||
"sourcecred/example-git": true
|
||||
}
|
||||
},
|
||||
"commits": {
|
||||
"3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f": {
|
||||
"hash": "3715ddfb8d4c4fd2a6f6af75488c82f84c92ec2f",
|
||||
|
|
|
@ -9,9 +9,15 @@
|
|||
*/
|
||||
// @flow
|
||||
|
||||
import * as MapUtil from "../../util/map";
|
||||
import type {GitDriver} from "./gitUtils";
|
||||
import type {Repository, Hash, Commit} from "./types";
|
||||
import type {Repository, Commit} from "./types";
|
||||
import {localGit} from "./gitUtils";
|
||||
import {
|
||||
repoIdToString,
|
||||
type RepoId,
|
||||
type RepoIdString,
|
||||
} from "../../core/repoId";
|
||||
|
||||
/**
|
||||
* Load a Git repository from disk into memory. The `rootRef` should be
|
||||
|
@ -21,7 +27,8 @@ import {localGit} from "./gitUtils";
|
|||
*/
|
||||
export function loadRepository(
|
||||
repositoryPath: string,
|
||||
rootRef: string
|
||||
rootRef: string,
|
||||
repoId: RepoId
|
||||
): Repository {
|
||||
const git = localGit(repositoryPath);
|
||||
try {
|
||||
|
@ -32,18 +39,18 @@ export function loadRepository(
|
|||
git(["rev-parse", "--verify", "HEAD"]);
|
||||
} catch (e) {
|
||||
// No data in the repository.
|
||||
return {commits: {}};
|
||||
return {commits: {}, commitToRepoId: {}};
|
||||
}
|
||||
const commits = findCommits(git, rootRef);
|
||||
return {commits: objectMap(commits)};
|
||||
}
|
||||
|
||||
function objectMap<T: {+hash: Hash}>(ts: $ReadOnlyArray<T>): {[Hash]: T} {
|
||||
const result = {};
|
||||
ts.forEach((t) => {
|
||||
result[t.hash] = t;
|
||||
const rawCommits = findCommits(git, rootRef);
|
||||
const commits = MapUtil.toObject(new Map(rawCommits.map((x) => [x.hash, x])));
|
||||
const repoIdString = repoIdToString(repoId);
|
||||
const repoIdStringSet: () => {[RepoIdString]: true} = () => ({
|
||||
[((repoIdString: RepoIdString): any)]: true,
|
||||
});
|
||||
return result;
|
||||
const commitToRepoId = MapUtil.toObject(
|
||||
new Map(rawCommits.map(({hash}) => [hash, repoIdStringSet()]))
|
||||
);
|
||||
return {commits, commitToRepoId};
|
||||
}
|
||||
|
||||
function findCommits(git: GitDriver, rootRef: string): Commit[] {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import tmp from "tmp";
|
||||
|
||||
import {makeRepoId, repoIdToString} from "../../core/repoId";
|
||||
import {createExampleRepo} from "./example/exampleRepo";
|
||||
import {localGit} from "./gitUtils";
|
||||
import {loadRepository} from "./loadRepository";
|
||||
|
@ -25,15 +26,38 @@ describe("plugins/git/loadRepository", () => {
|
|||
// In case of failure, run
|
||||
// src/plugins/git/loadRepositoryTest.sh --updateSnapshot
|
||||
// to update the snapshot, then inspect the resulting changes.
|
||||
expect(loadRepository(repository.path, "HEAD")).toEqual(
|
||||
require("./example/example-git.json")
|
||||
);
|
||||
expect(
|
||||
loadRepository(
|
||||
repository.path,
|
||||
"HEAD",
|
||||
makeRepoId("sourcecred", "example-git")
|
||||
)
|
||||
).toEqual(require("./example/example-git.json"));
|
||||
});
|
||||
|
||||
it("sets the right repoId for every commit", () => {
|
||||
const gitRepository = createExampleRepo(mkdtemp());
|
||||
const repoId = makeRepoId("sourcecred", "example-git");
|
||||
const repository = loadRepository(gitRepository.path, "HEAD", repoId);
|
||||
for (const commitHash of Object.keys(repository.commits)) {
|
||||
expect(Object.keys(repository.commitToRepoId[commitHash])).toEqual([
|
||||
repoIdToString(repoId),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("processes an old commit", () => {
|
||||
const repository = createExampleRepo(mkdtemp());
|
||||
const whole = loadRepository(repository.path, "HEAD");
|
||||
const part = loadRepository(repository.path, repository.commits[1]);
|
||||
const whole = loadRepository(
|
||||
repository.path,
|
||||
"HEAD",
|
||||
makeRepoId("sourcecred", "example-git")
|
||||
);
|
||||
const part = loadRepository(
|
||||
repository.path,
|
||||
repository.commits[1],
|
||||
makeRepoId("sourcecred", "example-git")
|
||||
);
|
||||
|
||||
// Check that `part` is a subset of `whole`...
|
||||
Object.keys(part.commits).forEach((hash) => {
|
||||
|
@ -50,7 +74,11 @@ describe("plugins/git/loadRepository", () => {
|
|||
const repository = createExampleRepo(mkdtemp());
|
||||
const invalidHash = "0".repeat(40);
|
||||
expect(() => {
|
||||
loadRepository(repository.path, invalidHash);
|
||||
loadRepository(
|
||||
repository.path,
|
||||
invalidHash,
|
||||
makeRepoId("sourcecred", "example-git")
|
||||
);
|
||||
}).toThrow("fatal: bad object 0000000000000000000000000000000000000000");
|
||||
});
|
||||
|
||||
|
@ -58,8 +86,15 @@ describe("plugins/git/loadRepository", () => {
|
|||
const repositoryPath = mkdtemp();
|
||||
const git = localGit(repositoryPath);
|
||||
git(["init"]);
|
||||
expect(loadRepository(repositoryPath, "HEAD")).toEqual({
|
||||
expect(
|
||||
loadRepository(
|
||||
repositoryPath,
|
||||
"HEAD",
|
||||
makeRepoId("sourcecred", "example-git")
|
||||
)
|
||||
).toEqual({
|
||||
commits: {},
|
||||
commitToRepoId: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,18 +6,23 @@ import type {Repository} from "./types";
|
|||
export function mergeRepository(
|
||||
repositories: $ReadOnlyArray<Repository>
|
||||
): Repository {
|
||||
const newRepository = {commits: {}};
|
||||
for (const {commits} of repositories) {
|
||||
const newCommits = {};
|
||||
const newCommitToRepoId = {};
|
||||
for (const {commits, commitToRepoId} of repositories) {
|
||||
for (const commitHash of Object.keys(commits)) {
|
||||
const existingCommit = newRepository.commits[commitHash];
|
||||
const existingCommit = newCommits[commitHash];
|
||||
if (
|
||||
existingCommit != null &&
|
||||
!deepEqual(existingCommit, commits[commitHash])
|
||||
) {
|
||||
throw new Error(`Conflict between commits at ${commitHash}`);
|
||||
}
|
||||
newRepository.commits[commitHash] = commits[commitHash];
|
||||
newCommits[commitHash] = commits[commitHash];
|
||||
const newRepos = commitToRepoId[commitHash];
|
||||
const existingRepos = newCommitToRepoId[commitHash] || {};
|
||||
const combinedRepoIdsForCommit = {...newRepos, ...existingRepos};
|
||||
newCommitToRepoId[commitHash] = combinedRepoIdsForCommit;
|
||||
}
|
||||
}
|
||||
return newRepository;
|
||||
return {commits: newCommits, commitToRepoId: newCommitToRepoId};
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
//@flow
|
||||
|
||||
import {makeRepoId, repoIdToString, type RepoIdString} from "../../core/repoId";
|
||||
import type {Repository} from "./types";
|
||||
import {mergeRepository} from "./mergeRepository";
|
||||
|
||||
describe("plugins/git/mergeRepository", () => {
|
||||
describe("mergeRepository", () => {
|
||||
const empty: Repository = Object.freeze({commits: {}});
|
||||
const empty: Repository = Object.freeze({commits: {}, commitToRepoId: {}});
|
||||
const repoId1 = repoIdToString(makeRepoId("repo", "1"));
|
||||
const repoId2 = repoIdToString(makeRepoId("repo", "2"));
|
||||
const repository1: Repository = Object.freeze({
|
||||
commits: {
|
||||
commit1: {
|
||||
|
@ -21,6 +24,10 @@ describe("plugins/git/mergeRepository", () => {
|
|||
parentHashes: ["commit1"],
|
||||
},
|
||||
},
|
||||
commitToRepoId: {
|
||||
commit1: {[((repoId1: RepoIdString): any)]: true},
|
||||
commit2: {[((repoId1: RepoIdString): any)]: true},
|
||||
},
|
||||
});
|
||||
const repository2: Repository = Object.freeze({
|
||||
commits: {
|
||||
|
@ -37,6 +44,10 @@ describe("plugins/git/mergeRepository", () => {
|
|||
parentHashes: ["commit1"],
|
||||
},
|
||||
},
|
||||
commitToRepoId: {
|
||||
commit1: {[((repoId2: RepoIdString): any)]: true},
|
||||
commit3: {[((repoId2: RepoIdString): any)]: true},
|
||||
},
|
||||
});
|
||||
|
||||
it("returns empty repository with no arguments", () => {
|
||||
|
@ -59,6 +70,20 @@ describe("plugins/git/mergeRepository", () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
it("commitToRepoId tracks every repository containing each commit", () => {
|
||||
const merged = mergeRepository([repository1, repository2]);
|
||||
expect(merged.commitToRepoId).toEqual({
|
||||
commit1: {
|
||||
[((repoId2: RepoIdString): any)]: true,
|
||||
[((repoId1: RepoIdString): any)]: true,
|
||||
},
|
||||
commit2: {[((repoId1: RepoIdString): any)]: true},
|
||||
commit3: {[((repoId2: RepoIdString): any)]: true},
|
||||
});
|
||||
});
|
||||
it("merging a repo with itself returns that repo", () => {
|
||||
expect(mergeRepository([repository1, repository1])).toEqual(repository1);
|
||||
});
|
||||
it("throws an error if merging a repository with conflicting commits", () => {
|
||||
const conflictingRepository: Repository = Object.freeze({
|
||||
commits: {
|
||||
|
@ -69,6 +94,11 @@ describe("plugins/git/mergeRepository", () => {
|
|||
parentHashes: ["commit0"],
|
||||
},
|
||||
},
|
||||
commitToRepoId: {
|
||||
commit1: {
|
||||
[((repoId1: RepoIdString): any)]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
mergeRepository([repository1, conflictingRepository])
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import {makeRepoId} from "../../core/repoId";
|
||||
import * as GN from "./nodes";
|
||||
import {description} from "./render";
|
||||
import type {Repository} from "./types";
|
||||
|
@ -19,6 +20,9 @@ describe("plugins/git/render", () => {
|
|||
parentHashes: [],
|
||||
},
|
||||
},
|
||||
commitToRepoId: {
|
||||
[exampleHash]: {[(makeRepoId("sourcecred", "example-git"): any)]: true},
|
||||
},
|
||||
});
|
||||
|
||||
it("commit snapshots as expected", () => {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
// @flow
|
||||
|
||||
import type {RepoIdString} from "../../core/repoId";
|
||||
|
||||
export type Repository = {|
|
||||
+commits: {[Hash]: Commit},
|
||||
// For every commit, track all the RepoIds of repos
|
||||
// containing this commit.
|
||||
+commitToRepoId: {[Hash]: {+[RepoIdString]: true}},
|
||||
|};
|
||||
export type Hash = string;
|
||||
export type Commit = {|
|
||||
|
|
Loading…
Reference in New Issue