resources/perf.webkit.org/public/v3/models/commit-set.js (427 lines of code) (raw):

'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()); } areAllRootsAvailable(earliestCreationTime) { return this.allRootFiles().every(rootFile => this.customRoots().includes(rootFile) || (!rootFile.deletedAt() && rootFile.createdAt() >= earliestCreationTime)); } 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; } equalsIgnoringRoot(other) { return this._equalsOptionallyIgnoringRoot(other, true); } equals(other) { return this._equalsOptionallyIgnoringRoot(other, false); } _equalsOptionallyIgnoringRoot(other, ignoringRoot) { 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) && !ignoringRoot) 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 revisionIdentifier = values[3]; const order = values[4]; const time = values[5]; 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, revisionIdentifier, 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; }