blob: 29a98256d0f3132f063a701a7c52b11c02af6966 [file] [log] [blame]
'use strict';
class CommitSet extends DataModelObject {
constructor(id, object)
{
super(id);
this._repositories = [];
this._repositoryToCommitMap = new Map;
this._repositoryToPatchMap = new Map;
this._repositoryToRootMap = new Map;
this._repositoryToCommitOwnerMap = new Map;
this._repositoryRequiresBuildMap = new Map;
this._ownerRepositoryToOwnedRepositoriesMap = new Map;
this._latestCommitTime = null;
this._customRoots = [];
this._allRootFiles = [];
if (!object)
return;
this._updateFromObject(object);
}
updateSingleton(object)
{
this._repositoryToCommitMap.clear();
this._repositoryToPatchMap.clear();
this._repositoryToRootMap.clear();
this._repositoryToCommitOwnerMap.clear();
this._repositoryRequiresBuildMap.clear();
this._ownerRepositoryToOwnedRepositoriesMap.clear();
this._repositories = [];
this._updateFromObject(object);
}
_updateFromObject(object)
{
const rootFiles = new Set;
for (const item of object.revisionItems) {
const commit = item.commit;
console.assert(commit instanceof CommitLog);
console.assert(!item.patch || item.patch instanceof UploadedFile);
console.assert(!item.rootFile || item.rootFile instanceof UploadedFile);
console.assert(!item.commitOwner || item.commitOwner instanceof CommitLog);
const repository = commit.repository();
this._repositoryToCommitMap.set(repository, commit);
this._repositoryToPatchMap.set(repository, item.patch);
if (item.commitOwner) {
this._repositoryToCommitOwnerMap.set(repository, item.commitOwner);
const ownerRepository = item.commitOwner.repository();
if (!this._ownerRepositoryToOwnedRepositoriesMap.get(ownerRepository))
this._ownerRepositoryToOwnedRepositoriesMap.set(ownerRepository, [repository]);
else
this._ownerRepositoryToOwnedRepositoriesMap.get(ownerRepository).push(repository);
}
this._repositoryRequiresBuildMap.set(repository, item.requiresBuild);
this._repositoryToRootMap.set(repository, item.rootFile);
if (item.rootFile)
rootFiles.add(item.rootFile);
this._repositories.push(commit.repository());
}
this._customRoots = object.customRoots;
this._allRootFiles = Array.from(rootFiles).concat(object.customRoots);
}
repositories() { return this._repositories; }
customRoots() { return this._customRoots; }
allRootFiles() { return this._allRootFiles; }
commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
ownerCommitForRepository(repository) { return this._repositoryToCommitOwnerMap.get(repository); }
topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this._repositories.filter((repository) => !this.ownerRevisionForRepository(repository))); }
ownedRepositoriesForOwnerRepository(repository) { return this._ownerRepositoryToOwnedRepositoriesMap.get(repository); }
revisionForRepository(repository)
{
var commit = this._repositoryToCommitMap.get(repository);
return commit ? commit.revision() : null;
}
ownerRevisionForRepository(repository)
{
const commit = this._repositoryToCommitOwnerMap.get(repository);
return commit ? commit.revision() : null;
}
patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
rootForRepository(repository) { return this._repositoryToRootMap.get(repository); }
requiresBuildForRepository(repository) { return this._repositoryRequiresBuildMap.get(repository) || false; }
// FIXME: This should return a Date object.
latestCommitTime()
{
if (this._latestCommitTime == null) {
var maxTime = 0;
for (const [repository, commit] of this._repositoryToCommitMap)
maxTime = Math.max(maxTime, +commit.time());
this._latestCommitTime = maxTime;
}
return this._latestCommitTime;
}
equals(other)
{
if (this._repositories.length != other._repositories.length)
return false;
for (const [repository, commit] of this._repositoryToCommitMap) {
if (commit != other._repositoryToCommitMap.get(repository))
return false;
if (this.patchForRepository(repository) != other.patchForRepository(repository))
return false;
if (this.rootForRepository(repository) != other.rootForRepository(repository))
return false;
if (this.ownerCommitForRepository(repository) != other.ownerCommitForRepository(repository))
return false;
if (this.requiresBuildForRepository(repository) != other.requiresBuildForRepository(repository))
return false;
}
return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
}
hasSameRepositories(commitSet)
{
return commitSet.repositories().length === this._repositoryToCommitMap.size
&& commitSet.repositories().every((repository) => this._repositoryToCommitMap.has(repository));
}
static areCustomRootsEqual(customRoots1, customRoots2)
{
if (customRoots1.length != customRoots2.length)
return false;
const set2 = new Set(customRoots2);
for (let file of customRoots1) {
if (!set2.has(file))
return false;
}
return true;
}
static containsMultipleCommitsForRepository(commitSets, repository)
{
console.assert(repository instanceof Repository);
if (commitSets.length < 2)
return false;
const firstCommit = commitSets[0].commitForRepository(repository);
for (let set of commitSets) {
const anotherCommit = set.commitForRepository(repository);
if (!firstCommit != !anotherCommit || (firstCommit && firstCommit.revision() != anotherCommit.revision()))
return true;
}
return false;
}
containsRootOrPatchOrOwnedCommit()
{
if (this.allRootFiles().length)
return true;
for (const repository of this.repositories()) {
if (this.ownerCommitForRepository(repository))
return true;
if (this.ownedRepositoriesForOwnerRepository(repository))
return true;
if (this.patchForRepository(repository))
return true;
}
return false;
}
static createNameWithoutCollision(name, existingNameSet)
{
console.assert(existingNameSet instanceof Set);
if (!existingNameSet.has(name))
return name;
const nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
let number = 1;
if (nameWithNumberMatch) {
name = nameWithNumberMatch[1];
number = parseInt(nameWithNumberMatch[2]);
}
let newName;
do {
number++;
newName = `${name} (${number})`;
} while (existingNameSet.has(newName));
return newName;
}
static diff(firstCommitSet, secondCommitSet)
{
console.assert(!firstCommitSet.equals(secondCommitSet));
const allRepositories = new Set([...firstCommitSet.repositories(), ...secondCommitSet.repositories()]);
const sortedRepositories = Repository.sortByNamePreferringOnesWithURL([...allRepositories]);
const nameParts = [];
const missingCommit = {label: () => 'none'};
const missingPatch = {filename: () => 'none'};
const makeNameGenerator = () => {
const existingNameSet = new Set;
return (name) => {
const newName = CommitSet.createNameWithoutCollision(name, existingNameSet);
existingNameSet.add(newName);
return newName;
}
};
for (const repository of sortedRepositories) {
const firstCommit = firstCommitSet.commitForRepository(repository) || missingCommit;
const secondCommit = secondCommitSet.commitForRepository(repository) || missingCommit;
const firstPatch = firstCommitSet.patchForRepository(repository) || missingPatch;
const secondPatch = secondCommitSet.patchForRepository(repository) || missingPatch;
const nameGenerator = makeNameGenerator();
if (firstCommit == secondCommit && firstPatch == secondPatch)
continue;
if (firstCommit != secondCommit && firstPatch == secondPatch)
nameParts.push(`${repository.name()}: ${secondCommit.diff(firstCommit).label}`);
// FIXME: It would be nice if we can abbreviate the name when it's too long.
const nameForFirstPatch = nameGenerator(firstPatch.filename());
const nameForSecondPath = nameGenerator(secondPatch.filename());
if (firstCommit == secondCommit && firstPatch != secondPatch)
nameParts.push(`${repository.name()}: ${nameForFirstPatch} - ${nameForSecondPath}`);
if (firstCommit != secondCommit && firstPatch != secondPatch)
nameParts.push(`${repository.name()}: ${firstCommit.label()} with ${nameForFirstPatch} - ${secondCommit.label()} with ${nameForSecondPath}`);
}
if (firstCommitSet.allRootFiles().length || secondCommitSet.allRootFiles().length) {
const firstRootFileSet = new Set(firstCommitSet.allRootFiles());
const secondRootFileSet = new Set(secondCommitSet.allRootFiles());
const uniqueInFirstCommitSet = firstCommitSet.allRootFiles().filter((rootFile) => !secondRootFileSet.has(rootFile));
const uniqueInSecondCommitSet = secondCommitSet.allRootFiles().filter((rootFile) => !firstRootFileSet.has(rootFile));
const nameGenerator = makeNameGenerator();
const firstDescription = uniqueInFirstCommitSet.map((rootFile) => nameGenerator(rootFile.filename())).join(', ');
const secondDescription = uniqueInSecondCommitSet.map((rootFile) => nameGenerator(rootFile.filename())).join(', ');
nameParts.push(`Roots: ${firstDescription || 'none'} - ${secondDescription || 'none'}`);
}
return nameParts.join(' ');
}
static revisionSetsFromCommitSets(commitSets)
{
return commitSets.map((commitSet) => {
console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
const revisionSet = {};
for (let repository of commitSet.repositories()) {
const patchFile = commitSet.patchForRepository(repository);
revisionSet[repository.id()] = {
revision: commitSet.revisionForRepository(repository),
ownerRevision: commitSet.ownerRevisionForRepository(repository),
patch: patchFile ? patchFile.id() : null,
};
}
const customRoots = commitSet.customRoots();
if (customRoots && customRoots.length)
revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
return revisionSet;
});
}
}
class MeasurementCommitSet extends CommitSet {
constructor(id, revisionList)
{
super(id, null);
for (const values of revisionList) {
// [<commit-id>, <repository-id>, <revision>, <order>, <time>]
const commitId = values[0];
const repositoryId = values[1];
const revision = values[2];
const order = values[3];
const time = values[4];
const repository = Repository.findById(repositoryId);
if (!repository)
continue;
// FIXME: Add a flag to remember the fact this commit log is incomplete.
const commit = CommitLog.ensureSingleton(commitId, {repository, revision, order, time});
this._repositoryToCommitMap.set(repository, commit);
this._repositories.push(repository);
}
}
// Use CommitSet's static maps because MeasurementCommitSet and CommitSet are logically of the same type.
// FIXME: Ideally, DataModel should take care of this but traversing prototype chain is expensive.
namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
static namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
static ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
static ensureSingleton(measurementId, revisionList)
{
const commitSetId = measurementId + '-commitset';
return CommitSet.findById(commitSetId) || (new MeasurementCommitSet(commitSetId, revisionList));
}
}
class CustomCommitSet {
constructor()
{
this._revisionListByRepository = new Map;
this._customRoots = [];
}
setRevisionForRepository(repository, revision, patch = null, ownerRevision = null)
{
console.assert(repository instanceof Repository);
console.assert(!patch || patch instanceof UploadedFile);
this._revisionListByRepository.set(repository, {revision, patch, ownerRevision});
}
equals(other)
{
console.assert(other instanceof CustomCommitSet);
if (this._revisionListByRepository.size != other._revisionListByRepository.size)
return false;
for (const [repository, thisRevision] of this._revisionListByRepository) {
const otherRevision = other._revisionListByRepository.get(repository);
if (!thisRevision != !otherRevision)
return false;
if (thisRevision && (thisRevision.revision != otherRevision.revision
|| thisRevision.patch != otherRevision.patch
|| thisRevision.ownerRevision != otherRevision.ownerRevision))
return false;
}
return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
}
repositories() { return Array.from(this._revisionListByRepository.keys()); }
topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerRevisionForRepository(repository))); }
revisionForRepository(repository)
{
const entry = this._revisionListByRepository.get(repository);
if (!entry)
return null;
return entry.revision;
}
patchForRepository(repository)
{
const entry = this._revisionListByRepository.get(repository);
if (!entry)
return null;
return entry.patch;
}
ownerRevisionForRepository(repository)
{
const entry = this._revisionListByRepository.get(repository);
if (!entry)
return null;
return entry.ownerRevision;
}
customRoots() { return this._customRoots; }
addCustomRoot(uploadedFile)
{
console.assert(uploadedFile instanceof UploadedFile);
this._customRoots.push(uploadedFile);
}
}
class IntermediateCommitSet {
constructor(commitSet)
{
console.assert(commitSet instanceof CommitSet);
this._commitByRepository = new Map;
this._ownerToOwnedRepositories = new Map;
this._fetchingPromiseByRepository = new Map;
for (const repository of commitSet.repositories())
this.setCommitForRepository(repository, commitSet.commitForRepository(repository), commitSet.ownerCommitForRepository(repository));
}
fetchCommitLogs()
{
const fetchingPromises = [];
for (const [repository, commit] of this._commitByRepository)
fetchingPromises.push(this._fetchCommitLogAndOwnedCommits(repository, commit.revision()));
return Promise.all(fetchingPromises);
}
_fetchCommitLogAndOwnedCommits(repository, revision)
{
return CommitLog.fetchForSingleRevision(repository, revision).then((commits) => {
console.assert(commits.length === 1);
const commit = commits[0];
if (!commit.ownsCommits())
return commit;
return commit.fetchOwnedCommits().then(() => commit);
});
}
updateRevisionForOwnerRepository(repository, revision)
{
const fetchingPromise = this._fetchCommitLogAndOwnedCommits(repository, revision);
this._fetchingPromiseByRepository.set(repository, fetchingPromise);
return fetchingPromise.then((commit) => {
const currentFetchingPromise = this._fetchingPromiseByRepository.get(repository);
if (currentFetchingPromise !== fetchingPromise)
return;
this._fetchingPromiseByRepository.set(repository, null);
this.setCommitForRepository(repository, commit);
});
}
setCommitForRepository(repository, commit, ownerCommit = null)
{
console.assert(repository instanceof Repository);
console.assert(commit instanceof CommitLog);
this._commitByRepository.set(repository, commit);
if (!ownerCommit)
ownerCommit = commit.ownerCommit();
if (ownerCommit) {
const ownerRepository = ownerCommit.repository();
if (!this._ownerToOwnedRepositories.has(ownerRepository))
this._ownerToOwnedRepositories.set(ownerRepository, new Set);
const repositorySet = this._ownerToOwnedRepositories.get(ownerRepository);
repositorySet.add(repository);
}
}
removeCommitForRepository(repository)
{
console.assert(repository instanceof Repository);
this._fetchingPromiseByRepository.set(repository, null);
const ownerCommit = this.ownerCommitForRepository(repository);
if (ownerCommit) {
const repositorySet = this._ownerToOwnedRepositories.get(ownerCommit.repository());
console.assert(repositorySet.has(repository));
repositorySet.delete(repository);
} else if (this._ownerToOwnedRepositories.has(repository)) {
const ownedRepositories = this._ownerToOwnedRepositories.get(repository);
for (const ownedRepository of ownedRepositories)
this._commitByRepository.delete(ownedRepository);
this._ownerToOwnedRepositories.delete(repository);
}
this._commitByRepository.delete(repository);
}
ownsCommitsForRepository(repository) { return this.commitForRepository(repository).ownsCommits(); }
repositories() { return Array.from(this._commitByRepository.keys()); }
highestLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerCommitForRepository(repository))); }
commitForRepository(repository) { return this._commitByRepository.get(repository); }
ownedRepositoriesForOwnerRepository(repository) { return this._ownerToOwnedRepositories.get(repository); }
ownerCommitForRepository(repository)
{
const commit = this._commitByRepository.get(repository);
if (!commit)
return null;
return commit.ownerCommit();
}
}
if (typeof module != 'undefined') {
module.exports.CommitSet = CommitSet;
module.exports.MeasurementCommitSet = MeasurementCommitSet;
module.exports.CustomCommitSet = CustomCommitSet;
module.exports.IntermediateCommitSet = IntermediateCommitSet;
}