Extend create-analysis-test API to be able to create with confirming test group.
[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     static revisionSetsFromCommitSets(commitSets)
247     {
248         return commitSets.map((commitSet) => {
249             console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
250             const revisionSet = {};
251             for (let repository of commitSet.repositories()) {
252                 const patchFile = commitSet.patchForRepository(repository);
253                 revisionSet[repository.id()] = {
254                     revision: commitSet.revisionForRepository(repository),
255                     ownerRevision: commitSet.ownerRevisionForRepository(repository),
256                     patch: patchFile ? patchFile.id() : null,
257                 };
258             }
259             const customRoots = commitSet.customRoots();
260             if (customRoots && customRoots.length)
261                 revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
262             return revisionSet;
263         });
264     }
265 }
266
267 class MeasurementCommitSet extends CommitSet {
268
269     constructor(id, revisionList)
270     {
271         super(id, null);
272         for (const values of revisionList) {
273             // [<commit-id>, <repository-id>, <revision>, <order>, <time>]
274             const commitId = values[0];
275             const repositoryId = values[1];
276             const revision = values[2];
277             const order = values[3];
278             const time = values[4];
279             const repository = Repository.findById(repositoryId);
280             if (!repository)
281                 continue;
282
283             // FIXME: Add a flag to remember the fact this commit log is incomplete.
284             const commit = CommitLog.ensureSingleton(commitId, {repository, revision, order, time});
285             this._repositoryToCommitMap.set(repository, commit);
286             this._repositories.push(repository);
287         }
288     }
289
290     // Use CommitSet's static maps because MeasurementCommitSet and CommitSet are logically of the same type.
291     // FIXME: Ideally, DataModel should take care of this but traversing prototype chain is expensive.
292     namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
293     ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
294     static namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
295     static ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
296
297     static ensureSingleton(measurementId, revisionList)
298     {
299         const commitSetId = measurementId + '-commitset';
300         return CommitSet.findById(commitSetId) || (new MeasurementCommitSet(commitSetId, revisionList));
301     }
302 }
303
304 class CustomCommitSet {
305
306     constructor()
307     {
308         this._revisionListByRepository = new Map;
309         this._customRoots = [];
310     }
311
312     setRevisionForRepository(repository, revision, patch = null, ownerRevision = null)
313     {
314         console.assert(repository instanceof Repository);
315         console.assert(!patch || patch instanceof UploadedFile);
316         this._revisionListByRepository.set(repository, {revision, patch, ownerRevision});
317     }
318
319     equals(other)
320     {
321         console.assert(other instanceof CustomCommitSet);
322         if (this._revisionListByRepository.size != other._revisionListByRepository.size)
323             return false;
324
325         for (const [repository, thisRevision] of this._revisionListByRepository) {
326             const otherRevision = other._revisionListByRepository.get(repository);
327             if (!thisRevision != !otherRevision)
328                 return false;
329             if (thisRevision && (thisRevision.revision != otherRevision.revision
330                 || thisRevision.patch != otherRevision.patch
331                 || thisRevision.ownerRevision != otherRevision.ownerRevision))
332                 return false;
333         }
334         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
335     }
336
337     repositories() { return Array.from(this._revisionListByRepository.keys()); }
338     topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerRevisionForRepository(repository))); }
339     revisionForRepository(repository)
340     {
341         const entry = this._revisionListByRepository.get(repository);
342         if (!entry)
343             return null;
344         return entry.revision;
345     }
346     patchForRepository(repository)
347     {
348         const entry = this._revisionListByRepository.get(repository);
349         if (!entry)
350             return null;
351         return entry.patch;
352     }
353     ownerRevisionForRepository(repository)
354     {
355         const entry = this._revisionListByRepository.get(repository);
356         if (!entry)
357             return null;
358         return entry.ownerRevision;
359     }
360     customRoots() { return this._customRoots; }
361
362     addCustomRoot(uploadedFile)
363     {
364         console.assert(uploadedFile instanceof UploadedFile);
365         this._customRoots.push(uploadedFile);
366     }
367 }
368
369 class IntermediateCommitSet {
370
371     constructor(commitSet)
372     {
373         console.assert(commitSet instanceof CommitSet);
374         this._commitByRepository = new Map;
375         this._ownerToOwnedRepositories = new Map;
376         this._fetchingPromiseByRepository = new Map;
377
378         for (const repository of commitSet.repositories())
379             this.setCommitForRepository(repository, commitSet.commitForRepository(repository), commitSet.ownerCommitForRepository(repository));
380     }
381
382     fetchCommitLogs()
383     {
384         const fetchingPromises = [];
385         for (const [repository, commit] of this._commitByRepository)
386             fetchingPromises.push(this._fetchCommitLogAndOwnedCommits(repository, commit.revision()));
387         return Promise.all(fetchingPromises);
388     }
389
390     _fetchCommitLogAndOwnedCommits(repository, revision)
391     {
392         return CommitLog.fetchForSingleRevision(repository, revision).then((commits) => {
393             console.assert(commits.length === 1);
394             const commit = commits[0];
395             if (!commit.ownsCommits())
396                 return commit;
397             return commit.fetchOwnedCommits().then(() => commit);
398         });
399     }
400
401     updateRevisionForOwnerRepository(repository, revision)
402     {
403         const fetchingPromise = this._fetchCommitLogAndOwnedCommits(repository, revision);
404         this._fetchingPromiseByRepository.set(repository, fetchingPromise);
405         return fetchingPromise.then((commit) => {
406             const currentFetchingPromise = this._fetchingPromiseByRepository.get(repository);
407             if (currentFetchingPromise !== fetchingPromise)
408                 return;
409             this._fetchingPromiseByRepository.set(repository, null);
410             this.setCommitForRepository(repository, commit);
411         });
412     }
413
414     setCommitForRepository(repository, commit, ownerCommit = null)
415     {
416         console.assert(repository instanceof Repository);
417         console.assert(commit instanceof CommitLog);
418         this._commitByRepository.set(repository, commit);
419         if (!ownerCommit)
420             ownerCommit = commit.ownerCommit();
421         if (ownerCommit) {
422             const ownerRepository = ownerCommit.repository();
423             if (!this._ownerToOwnedRepositories.has(ownerRepository))
424                 this._ownerToOwnedRepositories.set(ownerRepository, new Set);
425             const repositorySet = this._ownerToOwnedRepositories.get(ownerRepository);
426             repositorySet.add(repository);
427         }
428     }
429
430     removeCommitForRepository(repository)
431     {
432         console.assert(repository instanceof Repository);
433         this._fetchingPromiseByRepository.set(repository, null);
434         const ownerCommit = this.ownerCommitForRepository(repository);
435         if (ownerCommit) {
436             const repositorySet = this._ownerToOwnedRepositories.get(ownerCommit.repository());
437             console.assert(repositorySet.has(repository));
438             repositorySet.delete(repository);
439         } else if (this._ownerToOwnedRepositories.has(repository)) {
440             const ownedRepositories = this._ownerToOwnedRepositories.get(repository);
441             for (const ownedRepository of ownedRepositories)
442                 this._commitByRepository.delete(ownedRepository);
443             this._ownerToOwnedRepositories.delete(repository);
444         }
445         this._commitByRepository.delete(repository);
446     }
447
448     ownsCommitsForRepository(repository) { return this.commitForRepository(repository).ownsCommits(); }
449
450     repositories() { return Array.from(this._commitByRepository.keys()); }
451     highestLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerCommitForRepository(repository))); }
452     commitForRepository(repository) { return this._commitByRepository.get(repository); }
453     ownedRepositoriesForOwnerRepository(repository) { return this._ownerToOwnedRepositories.get(repository); }
454
455     ownerCommitForRepository(repository)
456     {
457         const commit = this._commitByRepository.get(repository);
458         if (!commit)
459             return null;
460         return commit.ownerCommit();
461     }
462 }
463
464 if (typeof module != 'undefined') {
465     module.exports.CommitSet = CommitSet;
466     module.exports.MeasurementCommitSet = MeasurementCommitSet;
467     module.exports.CustomCommitSet = CustomCommitSet;
468     module.exports.IntermediateCommitSet = IntermediateCommitSet;
469 }