| '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); } |
| commitsWithTestability() { return this.commits().filter((commit) => !!commit.testability()); } |
| commits() { return Array.from(this._repositoryToCommitMap.values()); } |
| |
| 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, {id: 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); |
| } |
| |
| commitsWithTestability() { return this.commits().filter((commit) => !!commit.testability()); } |
| commits() { return Array.from(this._commitByRepository.values()); } |
| |
| _fetchCommitLogAndOwnedCommits(repository, revision) |
| { |
| return CommitLog.fetchForSingleRevision(repository, revision, true).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; |
| } |