Add UI for A/B testing on owned commits.
[WebKit.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     commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
75
76     revisionForRepository(repository)
77     {
78         var commit = this._repositoryToCommitMap.get(repository);
79         return commit ? commit.revision() : null;
80     }
81
82     ownerRevisionForRepository(repository)
83     {
84         const commit = this._repositoryToCommitOwnerMap.get(repository);
85         return commit ? commit.revision() : null;
86     }
87
88     patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
89     rootForRepository(repository) { return this._repositoryToRootMap.get(repository); }
90     requiresBuildForRepository(repository) { return this._repositoryRequiresBuildMap.get(repository); }
91
92     // FIXME: This should return a Date object.
93     latestCommitTime()
94     {
95         if (this._latestCommitTime == null) {
96             var maxTime = 0;
97             for (const [repository, commit] of this._repositoryToCommitMap)
98                 maxTime = Math.max(maxTime, +commit.time());
99             this._latestCommitTime = maxTime;
100         }
101         return this._latestCommitTime;
102     }
103
104     equals(other)
105     {
106         if (this._repositories.length != other._repositories.length)
107             return false;
108         for (const [repository, commit] of this._repositoryToCommitMap) {
109             if (commit != other._repositoryToCommitMap.get(repository))
110                 return false;
111             if (this._repositoryToPatchMap.get(repository) != other._repositoryToPatchMap.get(repository))
112                 return false;
113             if (this._repositoryToRootMap.get(repository) != other._repositoryToRootMap.get(repository))
114                 return false;
115             if (this._repositoryToCommitOwnerMap.get(repository) != other._repositoryToCommitMap.get(repository))
116                 return false;
117             if (this._repositoryRequiresBuildMap.get(repository) != other._repositoryRequiresBuildMap.get(repository))
118                 return false;
119         }
120         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
121     }
122
123     static areCustomRootsEqual(customRoots1, customRoots2)
124     {
125         if (customRoots1.length != customRoots2.length)
126             return false;
127         const set2 = new Set(customRoots2);
128         for (let file of customRoots1) {
129             if (!set2.has(file))
130                 return false;
131         }
132         return true;
133     }
134
135     static containsMultipleCommitsForRepository(commitSets, repository)
136     {
137         console.assert(repository instanceof Repository);
138         if (commitSets.length < 2)
139             return false;
140         const firstCommit = commitSets[0].commitForRepository(repository);
141         for (let set of commitSets) {
142             const anotherCommit = set.commitForRepository(repository);
143             if (!firstCommit != !anotherCommit || (firstCommit && firstCommit.revision() != anotherCommit.revision()))
144                 return true;
145         }
146         return false;
147     }
148 }
149
150 class MeasurementCommitSet extends CommitSet {
151
152     constructor(id, revisionList)
153     {
154         super(id, null);
155         for (var values of revisionList) {
156             // [<commit-id>, <repository-id>, <revision>, <time>]
157             var commitId = values[0];
158             var repositoryId = values[1];
159             var revision = values[2];
160             var time = values[3];
161             var repository = Repository.findById(repositoryId);
162             if (!repository)
163                 continue;
164
165             // FIXME: Add a flag to remember the fact this commit log is incomplete.
166             const commit = CommitLog.ensureSingleton(commitId, {repository: repository, revision: revision, time: time});
167             this._repositoryToCommitMap.set(repository, commit);
168             this._repositories.push(repository);
169         }
170     }
171
172     // Use CommitSet's static maps because MeasurementCommitSet and CommitSet are logically of the same type.
173     // FIXME: Ideally, DataModel should take care of this but traversing prototype chain is expensive.
174     namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
175     ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
176     static namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
177     static ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
178
179     static ensureSingleton(measurementId, revisionList)
180     {
181         const commitSetId = measurementId + '-commitset';
182         return CommitSet.findById(commitSetId) || (new MeasurementCommitSet(commitSetId, revisionList));
183     }
184 }
185
186 class CustomCommitSet {
187
188     constructor()
189     {
190         this._revisionListByRepository = new Map;
191         this._customRoots = [];
192     }
193
194     setRevisionForRepository(repository, revision, patch = null, ownerRevision = null)
195     {
196         console.assert(repository instanceof Repository);
197         console.assert(!patch || patch instanceof UploadedFile);
198         this._revisionListByRepository.set(repository, {revision, patch, ownerRevision});
199     }
200
201     equals(other)
202     {
203         console.assert(other instanceof CustomCommitSet);
204         if (this._revisionListByRepository.size != other._revisionListByRepository.size)
205             return false;
206
207         for (const [repository, thisRevision] of this._revisionListByRepository) {
208             const otherRevision = other._revisionListByRepository.get(repository);
209             if (!thisRevision != !otherRevision)
210                 return false;
211             if (thisRevision && (thisRevision.revision != otherRevision.revision
212                 || thisRevision.patch != otherRevision.patch
213                 || thisRevision.ownerRevision != otherRevision.ownerRevision))
214                 return false;
215         }
216         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
217     }
218
219     repositories() { return Array.from(this._revisionListByRepository.keys()); }
220     topLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerRevisionForRepository(repository))); }
221     revisionForRepository(repository)
222     {
223         const entry = this._revisionListByRepository.get(repository);
224         if (!entry)
225             return null;
226         return entry.revision;
227     }
228     patchForRepository(repository)
229     {
230         const entry = this._revisionListByRepository.get(repository);
231         if (!entry)
232             return null;
233         return entry.patch;
234     }
235     ownerRevisionForRepository(repository)
236     {
237         const entry = this._revisionListByRepository.get(repository);
238         if (!entry)
239             return null;
240         return entry.ownerRevision;
241     }
242     customRoots() { return this._customRoots; }
243
244     addCustomRoot(uploadedFile)
245     {
246         console.assert(uploadedFile instanceof UploadedFile);
247         this._customRoots.push(uploadedFile);
248     }
249 }
250
251 class IntermediateCommitSet {
252
253     constructor(commitSet)
254     {
255         console.assert(commitSet instanceof CommitSet);
256         this._commitByRepository = new Map;
257         this._ownerToOwnedRepositories = new Map;
258         this._fetchingPromiseByRepository = new Map;
259
260         for (const repository of commitSet.repositories())
261             this.setCommitForRepository(repository, commitSet.commitForRepository(repository), commitSet.ownerCommitForRepository(repository));
262     }
263
264     fetchCommitLogs()
265     {
266         const fetchingPromises = [];
267         for (const [repository, commit] of this._commitByRepository)
268             fetchingPromises.push(this._fetchCommitLogAndOwnedCommits(repository, commit.revision()));
269         return Promise.all(fetchingPromises);
270     }
271
272     _fetchCommitLogAndOwnedCommits(repository, revision)
273     {
274         return CommitLog.fetchForSingleRevision(repository, revision).then((commits) => {
275             console.assert(commits.length === 1);
276             const commit = commits[0];
277             if (!commit.ownsCommits())
278                 return commit;
279             return commit.fetchOwnedCommits().then(() => commit);
280         });
281     }
282
283     updateRevisionForOwnerRepository(repository, revision)
284     {
285         const fetchingPromise = this._fetchCommitLogAndOwnedCommits(repository, revision);
286         this._fetchingPromiseByRepository.set(repository, fetchingPromise);
287         return fetchingPromise.then((commit) => {
288             const currentFetchingPromise = this._fetchingPromiseByRepository.get(repository);
289             if (currentFetchingPromise !== fetchingPromise)
290                 return;
291             this._fetchingPromiseByRepository.set(repository, null);
292             this.setCommitForRepository(repository, commit);
293         });
294     }
295
296     setCommitForRepository(repository, commit, ownerCommit = null)
297     {
298         console.assert(repository instanceof Repository);
299         console.assert(commit instanceof CommitLog);
300         this._commitByRepository.set(repository, commit);
301         if (!ownerCommit)
302             ownerCommit = commit.ownerCommit();
303         if (ownerCommit) {
304             const ownerRepository = ownerCommit.repository();
305             if (!this._ownerToOwnedRepositories.has(ownerRepository))
306                 this._ownerToOwnedRepositories.set(ownerRepository, new Set);
307             const repositorySet = this._ownerToOwnedRepositories.get(ownerRepository);
308             repositorySet.add(repository);
309         }
310     }
311
312     removeCommitForRepository(repository)
313     {
314         console.assert(repository instanceof Repository);
315         this._fetchingPromiseByRepository.set(repository, null);
316         const ownerCommit = this.ownerCommitForRepository(repository);
317         if (ownerCommit) {
318             const repositorySet = this._ownerToOwnedRepositories.get(ownerCommit.repository());
319             console.assert(repositorySet.has(repository));
320             repositorySet.delete(repository);
321         } else if (this._ownerToOwnedRepositories.has(repository)) {
322             const ownedRepositories = this._ownerToOwnedRepositories.get(repository);
323             for (const ownedRepository of ownedRepositories)
324                 this._commitByRepository.delete(ownedRepository);
325             this._ownerToOwnedRepositories.delete(repository);
326         }
327         this._commitByRepository.delete(repository);
328     }
329
330     ownsCommitsForRepository(repository) { return this.commitForRepository(repository).ownsCommits(); }
331
332     repositories() { return Array.from(this._commitByRepository.keys()); }
333     highestLevelRepositories() { return Repository.sortByNamePreferringOnesWithURL(this.repositories().filter((repository) => !this.ownerCommitForRepository(repository))); }
334     commitForRepository(repository) { return this._commitByRepository.get(repository); }
335     ownedRepositoriesForOwnerRepository(repository) { return this._ownerToOwnedRepositories.get(repository); }
336
337     ownerCommitForRepository(repository)
338     {
339         const commit = this._commitByRepository.get(repository);
340         if (!commit)
341             return null;
342         return commit.ownerCommit();
343     }
344 }
345
346 if (typeof module != 'undefined') {
347     module.exports.CommitSet = CommitSet;
348     module.exports.MeasurementCommitSet = MeasurementCommitSet;
349     module.exports.CustomCommitSet = CustomCommitSet;
350     module.exports.IntermediateCommitSet = IntermediateCommitSet;
351 }