8112d56d22cdff808b99e7a9239871d722f143cd
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / models / commit-set.js
1 'use strict';
2
3 class CommitSet extends DataModelObject {
4
5     constructor(id, object)
6     {
7         super(id);
8         this._repositories = [];
9         this._repositoryToCommitMap = new Map;
10         this._repositoryToPatchMap = new Map;
11         this._repositoryToRootMap = new Map;
12         this._repositoryToCommitOwnerMap = new Map;
13         this._repositoryRequiresBuildMap = new Map;
14         this._ownerRepositoryToOwnedRepositoriesMap = new Map;
15         this._latestCommitTime = null;
16         this._customRoots = [];
17         this._allRootFiles = [];
18
19         if (!object)
20             return;
21
22         this._updateFromObject(object);
23     }
24
25     updateSingleton(object)
26     {
27         this._repositoryToCommitMap.clear();
28         this._repositoryToPatchMap.clear();
29         this._repositoryToRootMap.clear();
30         this._repositoryToCommitOwnerMap.clear();
31         this._repositoryRequiresBuildMap.clear();
32         this._ownerRepositoryToOwnedRepositoriesMap.clear();
33         this._repositories = [];
34         this._updateFromObject(object);
35     }
36
37     _updateFromObject(object)
38     {
39         const rootFiles = new Set;
40         for (const item of object.revisionItems) {
41             const commit = item.commit;
42             console.assert(commit instanceof CommitLog);
43             console.assert(!item.patch || item.patch instanceof UploadedFile);
44             console.assert(!item.rootFile || item.rootFile instanceof UploadedFile);
45             console.assert(!item.commitOwner || item.commitOwner instanceof CommitLog);
46             const repository = commit.repository();
47             this._repositoryToCommitMap.set(repository, commit);
48             this._repositoryToPatchMap.set(repository, item.patch);
49             if (item.commitOwner) {
50                 this._repositoryToCommitOwnerMap.set(repository, item.commitOwner);
51                 const ownerRepository = item.commitOwner.repository();
52                 if (!this._ownerRepositoryToOwnedRepositoriesMap.get(ownerRepository))
53                     this._ownerRepositoryToOwnedRepositoriesMap.set(ownerRepository, [repository]);
54                 else
55                     this._ownerRepositoryToOwnedRepositoriesMap.get(ownerRepository).push(repository);
56             }
57             this._repositoryRequiresBuildMap.set(repository, item.requiresBuild);
58             this._repositoryToRootMap.set(repository, item.rootFile);
59             if (item.rootFile)
60                 rootFiles.add(item.rootFile);
61             this._repositories.push(commit.repository());
62         }
63         this._customRoots = object.customRoots;
64         this._allRootFiles = Array.from(rootFiles).concat(object.customRoots);
65     }
66
67     repositories() { return this._repositories; }
68     customRoots() { return this._customRoots; }
69     allRootFiles() { return this._allRootFiles; }
70     commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
71     ownerCommitForRepository(repository) { return this._repositoryToCommitOwnerMap.get(repository); }
72     topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this._repositories.filter((repository) => !this.ownerRevisionForRepository(repository))); }
73     ownedRepositoriesForOwnerRepository(repository) { return this._ownerRepositoryToOwnedRepositoriesMap.get(repository); }
74
75     revisionForRepository(repository)
76     {
77         var commit = this._repositoryToCommitMap.get(repository);
78         return commit ? commit.revision() : null;
79     }
80
81     ownerRevisionForRepository(repository)
82     {
83         const commit = this._repositoryToCommitOwnerMap.get(repository);
84         return commit ? commit.revision() : null;
85     }
86
87     patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
88     rootForRepository(repository) { return this._repositoryToRootMap.get(repository); }
89     requiresBuildForRepository(repository) { return this._repositoryRequiresBuildMap.get(repository) || false; }
90
91     // FIXME: This should return a Date object.
92     latestCommitTime()
93     {
94         if (this._latestCommitTime == null) {
95             var maxTime = 0;
96             for (const [repository, commit] of this._repositoryToCommitMap)
97                 maxTime = Math.max(maxTime, +commit.time());
98             this._latestCommitTime = maxTime;
99         }
100         return this._latestCommitTime;
101     }
102
103     equals(other)
104     {
105         if (this._repositories.length != other._repositories.length)
106             return false;
107         for (const [repository, commit] of this._repositoryToCommitMap) {
108             if (commit != other._repositoryToCommitMap.get(repository))
109                 return false;
110             if (this.patchForRepository(repository) != other.patchForRepository(repository))
111                 return false;
112             if (this.rootForRepository(repository) != other.rootForRepository(repository))
113                 return false;
114             if (this.ownerCommitForRepository(repository) != other.ownerCommitForRepository(repository))
115                 return false;
116             if (this.requiresBuildForRepository(repository) != other.requiresBuildForRepository(repository))
117                 return false;
118         }
119         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
120     }
121
122     hasSameRepositories(commitSet)
123     {
124         return commitSet.repositories().length === this._repositoryToCommitMap.size
125             && commitSet.repositories().every((repository) => this._repositoryToCommitMap.has(repository));
126     }
127
128     static areCustomRootsEqual(customRoots1, customRoots2)
129     {
130         if (customRoots1.length != customRoots2.length)
131             return false;
132         const set2 = new Set(customRoots2);
133         for (let file of customRoots1) {
134             if (!set2.has(file))
135                 return false;
136         }
137         return true;
138     }
139
140     static containsMultipleCommitsForRepository(commitSets, repository)
141     {
142         console.assert(repository instanceof Repository);
143         if (commitSets.length < 2)
144             return false;
145         const firstCommit = commitSets[0].commitForRepository(repository);
146         for (let set of commitSets) {
147             const anotherCommit = set.commitForRepository(repository);
148             if (!firstCommit != !anotherCommit || (firstCommit && firstCommit.revision() != anotherCommit.revision()))
149                 return true;
150         }
151         return false;
152     }
153
154     containsRootOrPatchOrOwnedCommit()
155     {
156         if (this.allRootFiles().length)
157             return true;
158
159         for (const repository of this.repositories()) {
160             if (this.ownerCommitForRepository(repository))
161                 return true;
162             if (this.ownedRepositoriesForOwnerRepository(repository))
163                 return true;
164             if (this.patchForRepository(repository))
165                 return true;
166         }
167         return false;
168     }
169
170     static createNameWithoutCollision(name, existingNameSet)
171     {
172         console.assert(existingNameSet instanceof Set);
173         if (!existingNameSet.has(name))
174             return name;
175         const nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
176         let number = 1;
177         if (nameWithNumberMatch) {
178             name = nameWithNumberMatch[1];
179             number = parseInt(nameWithNumberMatch[2]);
180         }
181
182         let newName;
183         do {
184             number++;
185             newName = `${name} (${number})`;
186         } while (existingNameSet.has(newName));
187
188         return newName;
189     }
190
191     static diff(firstCommitSet, secondCommitSet)
192     {
193         console.assert(!firstCommitSet.equals(secondCommitSet));
194         const allRepositories = new Set([...firstCommitSet.repositories(), ...secondCommitSet.repositories()]);
195         const sortedRepositories = Repository.sortByNamePreferringOnesWithURL([...allRepositories]);
196         const nameParts = [];
197         const missingCommit = {label: () => 'none'};
198         const missingPatch = {filename: () => 'none'};
199         const makeNameGenerator = () => {
200             const existingNameSet = new Set;
201             return (name) => {
202                 const newName = CommitSet.createNameWithoutCollision(name, existingNameSet);
203                 existingNameSet.add(newName);
204                 return newName;
205             }
206         };
207
208         for (const repository of sortedRepositories) {
209             const firstCommit = firstCommitSet.commitForRepository(repository) || missingCommit;
210             const secondCommit = secondCommitSet.commitForRepository(repository) || missingCommit;
211             const firstPatch = firstCommitSet.patchForRepository(repository) || missingPatch;
212             const secondPatch = secondCommitSet.patchForRepository(repository) || missingPatch;
213             const nameGenerator = makeNameGenerator();
214
215             if (firstCommit == secondCommit && firstPatch == secondPatch)
216                 continue;
217
218             if (firstCommit != secondCommit && firstPatch == secondPatch)
219                 nameParts.push(`${repository.name()}: ${secondCommit.diff(firstCommit).label}`);
220
221             // FIXME: It would be nice if we can abbreviate the name when it's too long.
222             const nameForFirstPatch = nameGenerator(firstPatch.filename());
223             const nameForSecondPath = nameGenerator(secondPatch.filename());
224
225             if (firstCommit == secondCommit && firstPatch != secondPatch)
226                 nameParts.push(`${repository.name()}: ${nameForFirstPatch} - ${nameForSecondPath}`);
227
228             if (firstCommit != secondCommit && firstPatch != secondPatch)
229                 nameParts.push(`${repository.name()}: ${firstCommit.label()} with ${nameForFirstPatch} - ${secondCommit.label()} with ${nameForSecondPath}`);
230         }
231
232         if (firstCommitSet.allRootFiles().length || secondCommitSet.allRootFiles().length) {
233             const firstRootFileSet = new Set(firstCommitSet.allRootFiles());
234             const secondRootFileSet = new Set(secondCommitSet.allRootFiles());
235             const uniqueInFirstCommitSet = firstCommitSet.allRootFiles().filter((rootFile) => !secondRootFileSet.has(rootFile));
236             const uniqueInSecondCommitSet = secondCommitSet.allRootFiles().filter((rootFile) => !firstRootFileSet.has(rootFile));
237             const nameGenerator = makeNameGenerator();
238             const firstDescription = uniqueInFirstCommitSet.map((rootFile) => nameGenerator(rootFile.filename())).join(', ');
239             const secondDescription = uniqueInSecondCommitSet.map((rootFile) => nameGenerator(rootFile.filename())).join(', ');
240             nameParts.push(`Roots: ${firstDescription || 'none'} - ${secondDescription || 'none'}`);
241         }
242
243         return nameParts.join(' ');
244     }
245 }
246
247 class MeasurementCommitSet extends CommitSet {
248
249     constructor(id, revisionList)
250     {
251         super(id, null);
252         for (const values of revisionList) {
253             // [<commit-id>, <repository-id>, <revision>, <order>, <time>]
254             const commitId = values[0];
255             const repositoryId = values[1];
256             const revision = values[2];
257             const order = values[3];
258             const time = values[4];
259             const repository = Repository.findById(repositoryId);
260             if (!repository)
261                 continue;
262
263             // FIXME: Add a flag to remember the fact this commit log is incomplete.
264             const commit = CommitLog.ensureSingleton(commitId, {repository, revision, order, time});
265             this._repositoryToCommitMap.set(repository, commit);
266             this._repositories.push(repository);
267         }
268     }
269
270     // Use CommitSet's static maps because MeasurementCommitSet and CommitSet are logically of the same type.
271     // FIXME: Ideally, DataModel should take care of this but traversing prototype chain is expensive.
272     namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
273     ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
274     static namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
275     static ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
276
277     static ensureSingleton(measurementId, revisionList)
278     {
279         const commitSetId = measurementId + '-commitset';
280         return CommitSet.findById(commitSetId) || (new MeasurementCommitSet(commitSetId, revisionList));
281     }
282 }
283
284 class CustomCommitSet {
285
286     constructor()
287     {
288         this._revisionListByRepository = new Map;
289         this._customRoots = [];
290     }
291
292     setRevisionForRepository(repository, revision, patch = null, ownerRevision = null)
293     {
294         console.assert(repository instanceof Repository);
295         console.assert(!patch || patch instanceof UploadedFile);
296         this._revisionListByRepository.set(repository, {revision, patch, ownerRevision});
297     }
298
299     equals(other)
300     {
301         console.assert(other instanceof CustomCommitSet);
302         if (this._revisionListByRepository.size != other._revisionListByRepository.size)
303             return false;
304
305         for (const [repository, thisRevision] of this._revisionListByRepository) {
306             const otherRevision = other._revisionListByRepository.get(repository);
307             if (!thisRevision != !otherRevision)
308                 return false;
309             if (thisRevision && (thisRevision.revision != otherRevision.revision
310                 || thisRevision.patch != otherRevision.patch
311                 || thisRevision.ownerRevision != otherRevision.ownerRevision))
312                 return false;
313         }
314         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
315     }
316
317     repositories() { return Array.from(this._revisionListByRepository.keys()); }
318     topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerRevisionForRepository(repository))); }
319     revisionForRepository(repository)
320     {
321         const entry = this._revisionListByRepository.get(repository);
322         if (!entry)
323             return null;
324         return entry.revision;
325     }
326     patchForRepository(repository)
327     {
328         const entry = this._revisionListByRepository.get(repository);
329         if (!entry)
330             return null;
331         return entry.patch;
332     }
333     ownerRevisionForRepository(repository)
334     {
335         const entry = this._revisionListByRepository.get(repository);
336         if (!entry)
337             return null;
338         return entry.ownerRevision;
339     }
340     customRoots() { return this._customRoots; }
341
342     addCustomRoot(uploadedFile)
343     {
344         console.assert(uploadedFile instanceof UploadedFile);
345         this._customRoots.push(uploadedFile);
346     }
347 }
348
349 class IntermediateCommitSet {
350
351     constructor(commitSet)
352     {
353         console.assert(commitSet instanceof CommitSet);
354         this._commitByRepository = new Map;
355         this._ownerToOwnedRepositories = new Map;
356         this._fetchingPromiseByRepository = new Map;
357
358         for (const repository of commitSet.repositories())
359             this.setCommitForRepository(repository, commitSet.commitForRepository(repository), commitSet.ownerCommitForRepository(repository));
360     }
361
362     fetchCommitLogs()
363     {
364         const fetchingPromises = [];
365         for (const [repository, commit] of this._commitByRepository)
366             fetchingPromises.push(this._fetchCommitLogAndOwnedCommits(repository, commit.revision()));
367         return Promise.all(fetchingPromises);
368     }
369
370     _fetchCommitLogAndOwnedCommits(repository, revision)
371     {
372         return CommitLog.fetchForSingleRevision(repository, revision).then((commits) => {
373             console.assert(commits.length === 1);
374             const commit = commits[0];
375             if (!commit.ownsCommits())
376                 return commit;
377             return commit.fetchOwnedCommits().then(() => commit);
378         });
379     }
380
381     updateRevisionForOwnerRepository(repository, revision)
382     {
383         const fetchingPromise = this._fetchCommitLogAndOwnedCommits(repository, revision);
384         this._fetchingPromiseByRepository.set(repository, fetchingPromise);
385         return fetchingPromise.then((commit) => {
386             const currentFetchingPromise = this._fetchingPromiseByRepository.get(repository);
387             if (currentFetchingPromise !== fetchingPromise)
388                 return;
389             this._fetchingPromiseByRepository.set(repository, null);
390             this.setCommitForRepository(repository, commit);
391         });
392     }
393
394     setCommitForRepository(repository, commit, ownerCommit = null)
395     {
396         console.assert(repository instanceof Repository);
397         console.assert(commit instanceof CommitLog);
398         this._commitByRepository.set(repository, commit);
399         if (!ownerCommit)
400             ownerCommit = commit.ownerCommit();
401         if (ownerCommit) {
402             const ownerRepository = ownerCommit.repository();
403             if (!this._ownerToOwnedRepositories.has(ownerRepository))
404                 this._ownerToOwnedRepositories.set(ownerRepository, new Set);
405             const repositorySet = this._ownerToOwnedRepositories.get(ownerRepository);
406             repositorySet.add(repository);
407         }
408     }
409
410     removeCommitForRepository(repository)
411     {
412         console.assert(repository instanceof Repository);
413         this._fetchingPromiseByRepository.set(repository, null);
414         const ownerCommit = this.ownerCommitForRepository(repository);
415         if (ownerCommit) {
416             const repositorySet = this._ownerToOwnedRepositories.get(ownerCommit.repository());
417             console.assert(repositorySet.has(repository));
418             repositorySet.delete(repository);
419         } else if (this._ownerToOwnedRepositories.has(repository)) {
420             const ownedRepositories = this._ownerToOwnedRepositories.get(repository);
421             for (const ownedRepository of ownedRepositories)
422                 this._commitByRepository.delete(ownedRepository);
423             this._ownerToOwnedRepositories.delete(repository);
424         }
425         this._commitByRepository.delete(repository);
426     }
427
428     ownsCommitsForRepository(repository) { return this.commitForRepository(repository).ownsCommits(); }
429
430     repositories() { return Array.from(this._commitByRepository.keys()); }
431     highestLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerCommitForRepository(repository))); }
432     commitForRepository(repository) { return this._commitByRepository.get(repository); }
433     ownedRepositoriesForOwnerRepository(repository) { return this._ownerToOwnedRepositories.get(repository); }
434
435     ownerCommitForRepository(repository)
436     {
437         const commit = this._commitByRepository.get(repository);
438         if (!commit)
439             return null;
440         return commit.ownerCommit();
441     }
442 }
443
444 if (typeof module != 'undefined') {
445     module.exports.CommitSet = CommitSet;
446     module.exports.MeasurementCommitSet = MeasurementCommitSet;
447     module.exports.CustomCommitSet = CustomCommitSet;
448     module.exports.IntermediateCommitSet = IntermediateCommitSet;
449 }