<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[214975] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/214975">214975</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-04-05 16:12:46 -0700 (Wed, 05 Apr 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Introduce the notion of repository groups to triggerables
https://bugs.webkit.org/show_bug.cgi?id=170228

Reviewed by Chris Dumez.

On some triggerable, it's desirable to specify multiple sets of repositories that are accepted.

For example, if a repository X transitioned from Subversion to Git, and if a triggerable accepted X and
some other repository Y, then it's desirable to two sets: (X-Subversion, Y) and (X-Git, Y) since neither
(X-Subversion, X-Git) nor (X-Subversion, X-Git, Y) makes sense as a set.

This patch introduces triggerable_repository_groups table to represent a set of repositories accepted by
a triggerable. It has many to one relationship to build_triggerables and triggerable_repositories in turn
now has many to one relationship to triggerable_repository_groups instead of build_triggerables.

Also make it possible to disable a triggerable e.g. a set of tests and platforms are no longer supported.
We don't want to delete the triggerable completely from the database since it would result in the associated
A/B testing results being purged, which is not desirale.

To migrate an existing database, run the following transaction:
```sql
BEGIN;
ALTER TABLE build_triggerables ADD COLUMN triggerable_disabled boolean NOT NULL DEFAULT FALSE;

CREATE TABLE triggerable_repository_groups (
    repositorygroup_id serial PRIMARY KEY,
    repositorygroup_triggerable integer REFERENCES build_triggerables NOT NULL,
    repositorygroup_name varchar(256) NOT NULL,
    repositorygroup_description varchar(256),
    repositorygroup_accepts_roots boolean NOT NULL DEFAULT FALSE,
    CONSTRAINT repository_group_name_must_be_unique_for_triggerable
        UNIQUE(repositorygroup_triggerable, repositorygroup_name));
INSERT INTO triggerable_repository_groups (repositorygroup_triggerable, repositorygroup_name)
    SELECT triggerable_id, 'default' FROM build_triggerables;

ALTER TABLE triggerable_repositories ADD COLUMN trigrepo_group integer REFERENCES triggerable_repository_groups;
UPDATE triggerable_repositories SET trigrepo_group = repositorygroup_id FROM triggerable_repository_groups
    WHERE trigrepo_triggerable = repositorygroup_triggerable;
ALTER TABLE triggerable_repositories ALTER COLUMN trigrepo_group SET NOT NULL;

ALTER TABLE triggerable_repositories DROP COLUMN trigrepo_triggerable;
ALTER TABLE triggerable_repositories DROP COLUMN trigrepo_sub_roots;
END;
```

* init-database.sql:
* public/admin/triggerables.php: Use a custom column to make forms to add and configure repository groups.
(insert_triggerable_repositories): Added.
(generate_repository_list): Added.
(generate_repository_form): Added.
(generate_repository_checkboxes): Now generates checkboxes for a repository group instead of a triggerable.

* public/include/manifest-generator.php:
(fetch_triggerables): Fixed the bug that we were not filtering results with query in /api/triggerable.
Rewrote it to include an array of repository groups, which in turn contains an array of repositories along
with its name and a description, and a boolean indicating whether it accepts a custom root file or not.
The boolean will be used when we're adding the support for perf try bots. We will keep acceptedRepositories
since it's still used by detect-changes.js.

* public/v3/models/manifest.js:
(Manifest._didFetchManifest): Resolve repositoriy, test, and platform IDs to their respective objects.

* public/v3/models/triggerable.js:
(Triggerable):
(Triggerable.prototype.isDisabled): Added.
(Triggerable.prototype.repositoryGroups): Added.
(Triggerable.prototype.acceptsTest): Added.
(TriggerableRepositoryGroup): Added.
(TriggerableRepositoryGroup.prototype.description): Added.
(TriggerableRepositoryGroup.prototype.acceptsCustomRoots): Added.
(TriggerableRepositoryGroup.prototype.repositories): Added.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage.prototype._didFetchTask): Don't use a disabled triggerable.

* server-tests/api-manifest-tests.js: Updated a test case to test repository groups.

* tools/js/database.js:
(tableToPrefixMap): Added triggerable_repository_groups.

* tools/js/v3-models.js: Imported TriggerableRepositoryGroup from triggerable.js.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicadmintriggerablesphp">trunk/Websites/perf.webkit.org/public/admin/triggerables.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludemanifestgeneratorphp">trunk/Websites/perf.webkit.org/public/include/manifest-generator.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmanifestjs">trunk/Websites/perf.webkit.org/public/v3/models/manifest.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstriggerablejs">trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsapimanifesttestsjs">trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsdatabasejs">trunk/Websites/perf.webkit.org/tools/js/database.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsv3modelsjs">trunk/Websites/perf.webkit.org/tools/js/v3-models.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -1,3 +1,87 @@
</span><ins>+2017-04-05  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Introduce the notion of repository groups to triggerables
+        https://bugs.webkit.org/show_bug.cgi?id=170228
+
+        Reviewed by Chris Dumez.
+
+        On some triggerable, it's desirable to specify multiple sets of repositories that are accepted.
+
+        For example, if a repository X transitioned from Subversion to Git, and if a triggerable accepted X and
+        some other repository Y, then it's desirable to two sets: (X-Subversion, Y) and (X-Git, Y) since neither
+        (X-Subversion, X-Git) nor (X-Subversion, X-Git, Y) makes sense as a set.
+
+        This patch introduces triggerable_repository_groups table to represent a set of repositories accepted by
+        a triggerable. It has many to one relationship to build_triggerables and triggerable_repositories in turn
+        now has many to one relationship to triggerable_repository_groups instead of build_triggerables.
+
+        Also make it possible to disable a triggerable e.g. a set of tests and platforms are no longer supported.
+        We don't want to delete the triggerable completely from the database since it would result in the associated
+        A/B testing results being purged, which is not desirale.
+
+        To migrate an existing database, run the following transaction:
+        ```sql
+        BEGIN;
+        ALTER TABLE build_triggerables ADD COLUMN triggerable_disabled boolean NOT NULL DEFAULT FALSE;
+
+        CREATE TABLE triggerable_repository_groups (
+            repositorygroup_id serial PRIMARY KEY,
+            repositorygroup_triggerable integer REFERENCES build_triggerables NOT NULL,
+            repositorygroup_name varchar(256) NOT NULL,
+            repositorygroup_description varchar(256),
+            repositorygroup_accepts_roots boolean NOT NULL DEFAULT FALSE,
+            CONSTRAINT repository_group_name_must_be_unique_for_triggerable
+                UNIQUE(repositorygroup_triggerable, repositorygroup_name));
+        INSERT INTO triggerable_repository_groups (repositorygroup_triggerable, repositorygroup_name)
+            SELECT triggerable_id, 'default' FROM build_triggerables;
+
+        ALTER TABLE triggerable_repositories ADD COLUMN trigrepo_group integer REFERENCES triggerable_repository_groups;
+        UPDATE triggerable_repositories SET trigrepo_group = repositorygroup_id FROM triggerable_repository_groups
+            WHERE trigrepo_triggerable = repositorygroup_triggerable;
+        ALTER TABLE triggerable_repositories ALTER COLUMN trigrepo_group SET NOT NULL;
+
+        ALTER TABLE triggerable_repositories DROP COLUMN trigrepo_triggerable;
+        ALTER TABLE triggerable_repositories DROP COLUMN trigrepo_sub_roots;
+        END;
+        ```
+
+        * init-database.sql:
+        * public/admin/triggerables.php: Use a custom column to make forms to add and configure repository groups.
+        (insert_triggerable_repositories): Added.
+        (generate_repository_list): Added.
+        (generate_repository_form): Added.
+        (generate_repository_checkboxes): Now generates checkboxes for a repository group instead of a triggerable.
+
+        * public/include/manifest-generator.php:
+        (fetch_triggerables): Fixed the bug that we were not filtering results with query in /api/triggerable.
+        Rewrote it to include an array of repository groups, which in turn contains an array of repositories along
+        with its name and a description, and a boolean indicating whether it accepts a custom root file or not.
+        The boolean will be used when we're adding the support for perf try bots. We will keep acceptedRepositories
+        since it's still used by detect-changes.js.
+
+        * public/v3/models/manifest.js:
+        (Manifest._didFetchManifest): Resolve repositoriy, test, and platform IDs to their respective objects.
+
+        * public/v3/models/triggerable.js:
+        (Triggerable):
+        (Triggerable.prototype.isDisabled): Added.
+        (Triggerable.prototype.repositoryGroups): Added.
+        (Triggerable.prototype.acceptsTest): Added.
+        (TriggerableRepositoryGroup): Added.
+        (TriggerableRepositoryGroup.prototype.description): Added.
+        (TriggerableRepositoryGroup.prototype.acceptsCustomRoots): Added.
+        (TriggerableRepositoryGroup.prototype.repositories): Added.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage.prototype._didFetchTask): Don't use a disabled triggerable.
+
+        * server-tests/api-manifest-tests.js: Updated a test case to test repository groups.
+
+        * tools/js/database.js:
+        (tableToPrefixMap): Added triggerable_repository_groups.
+
+        * tools/js/v3-models.js: Imported TriggerableRepositoryGroup from triggerable.js.
+
</ins><span class="cx"> 2017-03-31  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Build fix. For OS versions, we can end up with non-alphanumeric revision.
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -23,6 +23,7 @@
</span><span class="cx"> DROP TYPE IF EXISTS analysis_task_result_type CASCADE;
</span><span class="cx"> DROP TABLE IF EXISTS build_triggerables CASCADE;
</span><span class="cx"> DROP TABLE IF EXISTS triggerable_configurations CASCADE;
</span><ins>+DROP TABLE IF EXISTS triggerable_repository_groups CASCADE;
</ins><span class="cx"> DROP TABLE IF EXISTS triggerable_repositories CASCADE;
</span><span class="cx"> DROP TABLE IF EXISTS uploaded_files CASCADE;
</span><span class="cx"> DROP TABLE IF EXISTS bugs CASCADE;
</span><span class="lines">@@ -229,12 +230,20 @@
</span><span class="cx"> 
</span><span class="cx"> CREATE TABLE build_triggerables (
</span><span class="cx">     triggerable_id serial PRIMARY KEY,
</span><del>-    triggerable_name varchar(64) NOT NULL UNIQUE);
</del><ins>+    triggerable_name varchar(64) NOT NULL UNIQUE,
+    triggerable_disabled boolean NOT NULL DEFAULT FALSE);
</ins><span class="cx"> 
</span><ins>+CREATE TABLE triggerable_repository_groups (
+    repositorygroup_id serial PRIMARY KEY,
+    repositorygroup_triggerable integer REFERENCES build_triggerables NOT NULL,
+    repositorygroup_name varchar(256) NOT NULL,
+    repositorygroup_description varchar(256),
+    repositorygroup_accepts_roots boolean NOT NULL DEFAULT FALSE,
+    CONSTRAINT repository_group_name_must_be_unique_for_triggerable UNIQUE(repositorygroup_triggerable, repositorygroup_name));
+
</ins><span class="cx"> CREATE TABLE triggerable_repositories (
</span><del>-    trigrepo_triggerable integer REFERENCES build_triggerables NOT NULL,
</del><span class="cx">     trigrepo_repository integer REFERENCES repositories NOT NULL,
</span><del>-    trigrepo_sub_roots boolean NOT NULL DEFAULT FALSE);
</del><ins>+    trigrepo_group integer REFERENCES triggerable_repository_groups NOT NULL);
</ins><span class="cx"> 
</span><span class="cx"> CREATE TABLE triggerable_configurations (
</span><span class="cx">     trigconfig_test integer REFERENCES tests NOT NULL,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicadmintriggerablesphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/admin/triggerables.php (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/admin/triggerables.php        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/public/admin/triggerables.php        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -13,25 +13,53 @@
</span><span class="cx">     } else if ($action == 'update') {
</span><span class="cx">         if (update_field('build_triggerables', 'triggerable', 'name'))
</span><span class="cx">             regenerate_manifest();
</span><ins>+        else if (update_field('build_triggerables', 'triggerable', 'disabled', Database::to_database_boolean(array_get($_POST, 'disabled'))))
+            regenerate_manifest();
</ins><span class="cx">         else
</span><span class="cx">             notice('Invalid parameters.');
</span><ins>+    } else if ($action == 'update-group-name') {
+        if (update_field('triggerable_repository_groups', 'repositorygroup', 'name'))
+            regenerate_manifest();
+    } else if ($action == 'update-group-description') {
+        if (update_field('triggerable_repository_groups', 'repositorygroup', 'description'))
+            regenerate_manifest();
+    } else if ($action == 'update-group-accept-roots') {
+        if (update_field('triggerable_repository_groups', 'repositorygroup', 'accepts_roots',
+            Database::to_database_boolean(array_get($_POST, 'accepts'))))
+            regenerate_manifest();
+    } else if ($action == 'update-repository') {
+        $association = array_get($_POST, 'association');
+        $triggerable_id = array_get($_POST, 'triggerable');
+        $repository_id = array_get($_POST, 'repository');
+
+        $should_delete = FALSE;
+        $accepted = $association == 'accepted';
+        $required = $association == 'required';
+        if ($accepted || $required) {
+            $db-&gt;begin_transaction();
+            $select = array('repository' =&gt; $repository_id, 'triggerable' =&gt; $triggerable_id);
+            $update = array('repository' =&gt; $repository_id, 'triggerable' =&gt; $triggerable_id, 'required' =&gt; Database::to_database_boolean($required));
+            if (!$db-&gt;update_row('triggerable_repositories', 'trigrepo', $select, $update, 'repository')) {
+                notice(&quot;Failed to update the association of repository $repository_id with triggerable $triggerable_id.&quot;);
+                $db-&gt;rollback_transaction();
+            } else
+                $db-&gt;commit_transaction();
+        } else if ($association == 'not-accepted') {
+            $db-&gt;begin_transaction();
+            $result = $db-&gt;query_and_get_affected_rows(&quot;DELETE FROM triggerable_repositories WHERE trigrepo_triggerable = $1 AND trigrepo_repository = $2&quot;,
+                array($triggerable_id, $repository_id));
+            if ($result &gt; 1) {
+                notice(&quot;Failed to update the association of repository $repository_id with triggerable $triggerable_id.&quot;);
+                $db-&gt;rollback_transaction();
+            } else
+                $db-&gt;commit_transaction();
+        }
</ins><span class="cx">     } else if ($action == 'update-repositories') {
</span><del>-        $triggerable_id = intval($_POST['id']);
</del><ins>+        $group_id = intval($_POST['group']);
</ins><span class="cx"> 
</span><span class="cx">         $db-&gt;begin_transaction();
</span><del>-        $db-&gt;query_and_get_affected_rows(&quot;DELETE FROM triggerable_repositories WHERE trigrepo_triggerable = $1&quot;, array($triggerable_id));
-
-        $repositories = array_get($_POST, 'repositories');
-        $suceeded = TRUE;
-        if ($repositories) {
-            foreach ($repositories as $repository_id) {
-                if (!$db-&gt;insert_row('triggerable_repositories', 'trigrepo', array('triggerable' =&gt; $triggerable_id, 'repository' =&gt; $repository_id), NULL)) {
-                    $suceeded = FALSE;
-                    notice(&quot;Failed to associate repository $repository_id with triggerable $triggerable_id.&quot;);
-                    break;
-                }
-            }
-        }
</del><ins>+        $db-&gt;query_and_get_affected_rows(&quot;DELETE FROM triggerable_repositories WHERE trigrepo_group = $1&quot;, array($group_id));
+        $suceeded = insert_triggerable_repositories($db, $group_id, array_get($_POST, 'repositories'));
</ins><span class="cx">         if ($suceeded) {
</span><span class="cx">             $db-&gt;commit_transaction();
</span><span class="cx">             notice('Updated the association.');
</span><span class="lines">@@ -38,48 +66,138 @@
</span><span class="cx">             regenerate_manifest();
</span><span class="cx">         } else
</span><span class="cx">             $db-&gt;rollback_transaction();
</span><ins>+    } else if ($action == 'add-repository-group') {
+        $triggerable_id = intval($_POST['triggerable']);
+        $name = $_POST['name'];
+
+        $db-&gt;begin_transaction();
+        $group_id = $db-&gt;insert_row('triggerable_repository_groups', 'repositorygroup', array('name' =&gt; $name, 'triggerable' =&gt; $triggerable_id));
+        if (!$group_id)
+            notice('Failed to insert the specified repository group.');
+        else if (!insert_triggerable_repositories($db, $group_id, array_get($_POST, 'repositories')))
+            $db-&gt;rollback_transaction();
+        else {
+            $db-&gt;commit_transaction();
+            notice('Updated the association.');
+            regenerate_manifest();
+        }
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     $repository_rows = $db-&gt;fetch_table('repositories', 'repository_name');
</span><del>-    $repository_names = array();
</del><span class="cx"> 
</span><del>-
</del><span class="cx">     $page = new AdministrativePage($db, 'build_triggerables', 'triggerable', array(
</span><span class="cx">         'name' =&gt; array('editing_mode' =&gt; 'string'),
</span><del>-        'repositories' =&gt; array('custom' =&gt; function ($triggerable_row) use (&amp;$repository_rows) {
-            return array(generate_repository_checkboxes($triggerable_row['triggerable_id'], $repository_rows));
-        }),
</del><ins>+        'disabled' =&gt; array('editing_mode' =&gt; 'boolean', 'post_insertion' =&gt; TRUE),
+        'repositories' =&gt; array(
+            'label' =&gt; 'Repository Groups',
+            'subcolumns'=&gt; array('ID', 'Name', 'Description', 'Accepts Roots', 'Repositories'),
+            'custom' =&gt; function ($triggerable_row) use (&amp;$db, &amp;$repository_rows) {
+                return generate_repository_list($db, $triggerable_row['triggerable_id'], $repository_rows);
+            }),
</ins><span class="cx">     ));
</span><span class="cx"> 
</span><del>-    function generate_repository_checkboxes($triggerable_id, $repository_rows) {
-        global $db;
</del><ins>+    $page-&gt;render_table('name');
+    $page-&gt;render_form_to_add();
+}
</ins><span class="cx"> 
</span><del>-        $repository_rows = $db-&gt;query_and_fetch_all('SELECT * FROM repositories LEFT OUTER JOIN triggerable_repositories
-            ON trigrepo_repository = repository_id AND trigrepo_triggerable = $1 ORDER BY repository_name', array($triggerable_id));
</del><ins>+function insert_triggerable_repositories($db, $group_id, $repositories)
+{
+    if (!$repositories)
+        return TRUE;
+    foreach ($repositories as $repository_id) {
+        if (!$db-&gt;insert_row('triggerable_repositories', 'trigrepo', array('group' =&gt; $group_id, 'repository' =&gt; $repository_id), NULL)) {
+            notice(&quot;Failed to associate repository $repository_id with repository group $group_id.&quot;);
+            return FALSE;
+        }
+    }
+    return TRUE;
+}
</ins><span class="cx"> 
</span><del>-        $form = &lt;&lt;&lt; END
-&lt;form method=&quot;POST&quot;&gt;
-&lt;input type=&quot;hidden&quot; name=&quot;id&quot; value=&quot;$triggerable_id&quot;&gt;
-&lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;update-repositories&quot;&gt;
</del><ins>+
+function generate_repository_list($db, $triggerable_id, $repository_rows) {
+    $group_forms = array();
+
+    $repository_groups = $db-&gt;select_rows('triggerable_repository_groups', 'repositorygroup', array('triggerable' =&gt; $triggerable_id), 'name');
+    foreach ($repository_groups as $group_row) {
+        $group_id = $group_row['repositorygroup_id'];
+        $group_name = $group_row['repositorygroup_name'];
+        $group_description = $group_row['repositorygroup_description'];
+        $checked_if_accepts_roots = Database::is_true($group_row['repositorygroup_accepts_roots']) ? 'checked' : '';
+
+        $group_name_form = &lt;&lt;&lt; END
+            &lt;form method=&quot;POST&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;update-group-name&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value=&quot;$group_id&quot;&gt;
+            &lt;input type=&quot;text&quot; name=&quot;name&quot; value=&quot;$group_name&quot;&gt;
+            &lt;/form&gt;
</ins><span class="cx"> END;
</span><span class="cx"> 
</span><del>-        foreach ($repository_rows as $row) {
-            $checked = $row['trigrepo_triggerable'] ? ' checked' : '';
-            $form .= &lt;&lt;&lt; END
-&lt;label&gt;&lt;input type=&quot;checkbox&quot; name=&quot;repositories[]&quot; value=&quot;{$row['repository_id']}&quot;$checked&gt;{$row['repository_name']}&lt;/label&gt;
</del><ins>+        $group_description_form = &lt;&lt;&lt; END
+            &lt;form method=&quot;POST&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;update-group-description&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value=&quot;$group_id&quot;&gt;
+            &lt;input name=&quot;description&quot; value=&quot;$group_description&quot;&gt;
+            &lt;/form&gt;
</ins><span class="cx"> END;
</span><del>-        }
</del><span class="cx"> 
</span><del>-        return $form . &lt;&lt;&lt; END
-&lt;button&gt;Save&lt;/button&gt;
-&lt;/form&gt;
</del><ins>+        $group_accepts_roots = &lt;&lt;&lt; END
+            &lt;form method=&quot;POST&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;update-group-accept-roots&quot;&gt;
+            &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value=&quot;$group_id&quot;&gt;
+            &lt;input type=&quot;checkbox&quot; name=&quot;accepts&quot; $checked_if_accepts_roots&gt;
+            &lt;button type=&quot;submit&quot;&gt;Save&lt;/button&gt;
+            &lt;/form&gt;
</ins><span class="cx"> END;
</span><ins>+
+        array_push($group_forms, array($group_id, $group_name_form, $group_description_form, $group_accepts_roots, generate_repository_form($db, $repository_rows, $group_id)));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    $page-&gt;render_table('name');
-    $page-&gt;render_form_to_add();
</del><ins>+    $new_group_checkboxes = generate_repository_checkboxes($db, $repository_rows);
+    $new_group_form = &lt;&lt;&lt; END
+        &lt;form method=&quot;POST&quot;&gt;
+        &lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;add-repository-group&quot;&gt;
+        &lt;input type=&quot;hidden&quot; name=&quot;triggerable&quot; value=&quot;$triggerable_id&quot;&gt;
+        &lt;input type=&quot;text&quot; name=&quot;name&quot; value=&quot;&quot; required&gt;&lt;br&gt;
+        $new_group_checkboxes
+        &lt;br&gt;&lt;button type=&quot;submit&quot;&gt;Add&lt;/button&gt;&lt;/form&gt;
+END;
+
+    array_push($group_forms, $new_group_form);
+
+    return $group_forms;
</ins><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function generate_repository_form($db, $repository_rows, $group_id)
+{
+    $checkboxes = generate_repository_checkboxes($db, $repository_rows, $group_id);
+    return &lt;&lt;&lt; END
+        &lt;form method=&quot;POST&quot;&gt;
+        &lt;input type=&quot;hidden&quot; name=&quot;action&quot; value=&quot;update-repositories&quot;&gt;
+        &lt;input type=&quot;hidden&quot; name=&quot;group&quot; value=&quot;$group_id&quot;&gt;
+        $checkboxes
+        &lt;br&gt;&lt;button type=&quot;submit&quot;&gt;Save&lt;/button&gt;&lt;/form&gt;
+END;
+}
+
+function generate_repository_checkboxes($db, $repository_rows, $group_id = NULL)
+{
+    $repositories_in_group = array();
+    if ($group_id) {
+        $group_repository_rows = $db-&gt;select_rows('triggerable_repositories', 'trigrepo', array('group' =&gt; $group_id));
+        foreach ($group_repository_rows as $row)
+            $repositories_in_group[$row['trigrepo_repository']] = TRUE;
+    }
+
+    $form = '';
+    foreach ($repository_rows as $row) {
+        $id = $row['repository_id'];
+        $name = $row['repository_name'];
+        $checked = array_key_exists($id, $repositories_in_group) ? 'checked' : '';
+        $form .= &quot;&lt;label&gt;&lt;input type=\&quot;checkbox\&quot; name=\&quot;repositories[]\&quot; value=\&quot;$id\&quot; $checked&gt;$name&lt;/label&gt;&quot;;
+    }
+    return $form;
+}
+
</ins><span class="cx"> require('../include/admin-footer.php');
</span><span class="cx"> 
</span><span class="cx"> ?&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludemanifestgeneratorphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/manifest-generator.php (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/manifest-generator.php        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/public/include/manifest-generator.php        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -189,32 +189,64 @@
</span><span class="cx"> 
</span><span class="cx">     static function fetch_triggerables($db, $query)
</span><span class="cx">     {
</span><del>-        $triggerables = $db-&gt;fetch_table('build_triggerables');
</del><ins>+        $triggerables = $db-&gt;select_rows('build_triggerables', 'triggerable', $query);
</ins><span class="cx">         if (!$triggerables)
</span><span class="cx">             return array();
</span><span class="cx"> 
</span><span class="cx">         $id_to_triggerable = array();
</span><del>-        foreach ($triggerables as $row) {
</del><ins>+        $triggerable_id_to_repository_set = array();
+        foreach ($triggerables as &amp;$row) {
</ins><span class="cx">             $id = $row['triggerable_id'];
</span><span class="cx">             $id_to_triggerable[$id] = array(
</span><del>-                'id' =&gt; $id,
</del><span class="cx">                 'name' =&gt; $row['triggerable_name'],
</span><ins>+                'isDisabled' =&gt; Database::is_true($row['triggerable_disabled']),
</ins><span class="cx">                 'acceptedRepositories' =&gt; array(),
</span><ins>+                'repositoryGroups' =&gt; array(),
</ins><span class="cx">                 'configurations' =&gt; array());
</span><ins>+            $triggerable_id_to_repository_set[$id] = array();
</ins><span class="cx">         }
</span><span class="cx"> 
</span><del>-        $repository_map = $db-&gt;fetch_table('triggerable_repositories');
-        if ($repository_map) {
-            foreach ($repository_map as $row) {
-                $triggerable = &amp;$id_to_triggerable[$row['trigrepo_triggerable']];
-                array_push($triggerable['acceptedRepositories'], $row['trigrepo_repository']);
</del><ins>+        $repository_groups = $db-&gt;fetch_table('triggerable_repository_groups', 'repositorygroup_name');
+        $group_repositories = $db-&gt;fetch_table('triggerable_repositories');
+        if ($repository_groups &amp;&amp; $group_repositories) {
+            $repository_set_by_group = array();
+            foreach ($group_repositories as &amp;$repository_row) {
+                $group_id = $repository_row['trigrepo_group'];
+                array_ensure_item_has_array($repository_set_by_group, $group_id);
+                array_push($repository_set_by_group[$group_id], $repository_row['trigrepo_repository']);
</ins><span class="cx">             }
</span><ins>+            foreach ($repository_groups as &amp;$group_row) {
+                $triggerable_id = $group_row['repositorygroup_triggerable'];
+                if (!array_key_exists($triggerable_id, $id_to_triggerable))
+                    continue;
+                $triggerable = &amp;$id_to_triggerable[$triggerable_id];
+                $group_id = $group_row['repositorygroup_id'];
+                $repository_list = array_get($repository_set_by_group, $group_id, array());
+                array_push($triggerable['repositoryGroups'], array(
+                    'id' =&gt; $group_row['repositorygroup_id'],
+                    'name' =&gt; $group_row['repositorygroup_name'],
+                    'description' =&gt; $group_row['repositorygroup_description'],
+                    'acceptsCustomRoots' =&gt; Database::is_true($group_row['repositorygroup_accepts_roots']),
+                    'repositories' =&gt; $repository_list));
+                // V2 UI compatibility.
+                foreach ($repository_list as $repository_id) {
+                    $set = &amp;$triggerable_id_to_repository_set[$triggerable_id];
+                    if (array_key_exists($repository_id, $set))
+                        continue;
+                    $set[$repository_id] = true;
+                    array_push($triggerable['acceptedRepositories'], $repository_id);
+                }
+
+            }
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         $configuration_map = $db-&gt;fetch_table('triggerable_configurations');
</span><span class="cx">         if ($configuration_map) {
</span><del>-            foreach ($configuration_map as $row) {
-                $triggerable = &amp;$id_to_triggerable[$row['trigconfig_triggerable']];
</del><ins>+            foreach ($configuration_map as &amp;$row) {
+                $triggerable_id = $row['trigconfig_triggerable'];
+                if (!array_key_exists($triggerable_id, $id_to_triggerable))
+                    continue;
+                $triggerable = &amp;$id_to_triggerable[$triggerable_id];
</ins><span class="cx">                 array_push($triggerable['configurations'], array($row['trigconfig_test'], $row['trigconfig_platform']));
</span><span class="cx">             }
</span><span class="cx">         }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmanifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/manifest.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/manifest.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/public/v3/models/manifest.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -47,6 +47,14 @@
</span><span class="cx">             raw.acceptedRepositories = raw.acceptedRepositories.map((repositoryId) =&gt; {
</span><span class="cx">                 return Repository.findById(repositoryId);
</span><span class="cx">             });
</span><ins>+            raw.repositoryGroups = raw.repositoryGroups.map((group) =&gt; {
+                group.repositories = group.repositories.map((repositoryId) =&gt; Repository.findById(repositoryId));
+                return TriggerableRepositoryGroup.ensureSingleton(group.id, group);
+            });
+            raw.configurations = raw.configurations.map((configuration) =&gt; {
+                const [testId, platformId] = configuration;
+                return {test: Test.findById(testId), platform: Platform.findById(platformId)};
+            });
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('Manifest', '_didFetchManifest');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstriggerablejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -4,20 +4,27 @@
</span><span class="cx">     {
</span><span class="cx">         super(id, object);
</span><span class="cx">         this._name = object.name;
</span><ins>+        this._isDisabled = !!object.isDisabled;
</ins><span class="cx">         this._acceptedRepositories = object.acceptedRepositories;
</span><ins>+        this._repositoryGroups = object.repositoryGroups;
</ins><span class="cx">         this._configurationList = object.configurations;
</span><ins>+        this._acceptedTests = new Set;
</ins><span class="cx"> 
</span><del>-        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
</del><ins>+        const configurationMap = this.ensureNamedStaticMap('testConfigurations');
</ins><span class="cx">         for (const config of object.configurations) {
</span><del>-            const [testId, platformId] = config;
-            const key = `${testId}-${platformId}`;
</del><ins>+            const key = `${config.test.id()}-${config.platform.id()}`;
+            this._acceptedTests.add(config.test);
</ins><span class="cx">             console.assert(!(key in configurationMap));
</span><span class="cx">             configurationMap[key] = this;
</span><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    isDisabled() { return this._isDisabled; }
</ins><span class="cx">     acceptedRepositories() { return this._acceptedRepositories; }
</span><ins>+    repositoryGroups() { return this._repositoryGroups; }
</ins><span class="cx"> 
</span><ins>+    acceptsTest(test) { return this._acceptedTests.has(test); }
+
</ins><span class="cx">     static findByTestConfiguration(test, platform)
</span><span class="cx">     {
</span><span class="cx">         let configurationMap = this.ensureNamedStaticMap('testConfigurations');
</span><span class="lines">@@ -30,8 +37,25 @@
</span><span class="cx">         }
</span><span class="cx">         return null;
</span><span class="cx">     }
</span><ins>+}
</ins><span class="cx"> 
</span><ins>+class TriggerableRepositoryGroup extends LabeledObject {
+
+    constructor(id, object)
+    {
+        super(id, object);
+        this._description = object.description;
+        this._acceptsCustomRoots = !!object.acceptsCustomRoots;
+        this._repositories = object.repositories;
+    }
+
+    description() { return this._description || this.name(); }
+    acceptsCustomRoots() { return this._acceptsCustomRoots; }
+    repositories() { return this._repositories; }
</ins><span class="cx"> }
</span><span class="cx"> 
</span><del>-if (typeof module != 'undefined')
</del><ins>+if (typeof module != 'undefined') {
</ins><span class="cx">     module.exports.Triggerable = Triggerable;
</span><ins>+    module.exports.TriggerableRepositoryGroup = TriggerableRepositoryGroup;
+}
+
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -413,7 +413,8 @@
</span><span class="cx">         const platform = task.platform();
</span><span class="cx">         const metric = task.metric();
</span><span class="cx">         const lastModified = platform.lastModified(metric);
</span><del>-        this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
</del><ins>+        const triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
+        this._triggerable = triggerable &amp;&amp; !triggerable.isDisabled() ? triggerable : null;
</ins><span class="cx">         this._metric = metric;
</span><span class="cx"> 
</span><span class="cx">         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsapimanifesttestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -268,11 +268,13 @@
</span><span class="cx">             db.insert('repositories', {id: 101, name: 'WebKit', owner: 9, url: 'https://trac.webkit.org/$1'}),
</span><span class="cx">             db.insert('build_triggerables', {id: 200, name: 'build.webkit.org'}),
</span><span class="cx">             db.insert('build_triggerables', {id: 201, name: 'ios-build.webkit.org'}),
</span><ins>+            db.insert('build_triggerables', {id: 202, name: 'mac-build.webkit.org', disabled: true}),
</ins><span class="cx">             db.insert('tests', {id: 1, name: 'SomeTest'}),
</span><span class="cx">             db.insert('tests', {id: 2, name: 'SomeOtherTest'}),
</span><span class="cx">             db.insert('tests', {id: 3, name: 'ChildTest', parent: 1}),
</span><span class="cx">             db.insert('platforms', {id: 23, name: 'iOS 9 iPhone 5s'}),
</span><span class="cx">             db.insert('platforms', {id: 46, name: 'Trunk Mavericks'}),
</span><ins>+            db.insert('platforms', {id: 104, name: 'Trunk Sierra MacBookPro11,2'}),
</ins><span class="cx">             db.insert('test_metrics', {id: 5, test: 1, name: 'Time'}),
</span><span class="cx">             db.insert('test_metrics', {id: 8, test: 2, name: 'FrameRate'}),
</span><span class="cx">             db.insert('test_metrics', {id: 9, test: 3, name: 'Time'}),
</span><span class="lines">@@ -282,45 +284,60 @@
</span><span class="cx">             db.insert('test_configurations', {id: 104, metric: 5, platform: 23, type: 'current'}),
</span><span class="cx">             db.insert('test_configurations', {id: 105, metric: 8, platform: 23, type: 'current'}),
</span><span class="cx">             db.insert('test_configurations', {id: 106, metric: 9, platform: 23, type: 'current'}),
</span><del>-            db.insert('triggerable_repositories', {triggerable: 200, repository: 11}),
-            db.insert('triggerable_repositories', {triggerable: 201, repository: 11}),
</del><ins>+            db.insert('test_configurations', {id: 107, metric: 5, platform: 104, type: 'current'}),
+            db.insert('test_configurations', {id: 108, metric: 8, platform: 104, type: 'current'}),
+            db.insert('test_configurations', {id: 109, metric: 9, platform: 104, type: 'current'}),
+            db.insert('triggerable_repository_groups', {id: 300, triggerable: 200, name: 'default'}),
+            db.insert('triggerable_repository_groups', {id: 301, triggerable: 201, name: 'default'}),
+            db.insert('triggerable_repository_groups', {id: 302, triggerable: 202, name: 'system-and-webkit'}),
+            db.insert('triggerable_repository_groups', {id: 312, triggerable: 202, name: 'system-and-roots', accepts_roots: true}),
+            db.insert('triggerable_repositories', {group: 300, repository: 11}),
+            db.insert('triggerable_repositories', {group: 301, repository: 11}),
+            db.insert('triggerable_repositories', {group: 302, repository: 11}),
+            db.insert('triggerable_repositories', {group: 302, repository: 9}),
+            db.insert('triggerable_repositories', {group: 312, repository: 9}),
</ins><span class="cx">             db.insert('triggerable_configurations', {triggerable: 200, test: 1, platform: 46}),
</span><span class="cx">             db.insert('triggerable_configurations', {triggerable: 200, test: 2, platform: 46}),
</span><span class="cx">             db.insert('triggerable_configurations', {triggerable: 201, test: 1, platform: 23}),
</span><span class="cx">             db.insert('triggerable_configurations', {triggerable: 201, test: 2, platform: 23}),
</span><ins>+            db.insert('triggerable_configurations', {triggerable: 202, test: 1, platform: 104}),
+            db.insert('triggerable_configurations', {triggerable: 202, test: 2, platform: 104}),
</ins><span class="cx">         ]).then(() =&gt; {
</span><span class="cx">             return Manifest.fetch();
</span><span class="cx">         }).then(() =&gt; {
</span><del>-            let webkit = Repository.findById(11);
</del><ins>+            const webkit = Repository.findById(11);
</ins><span class="cx">             assert.equal(webkit.name(), 'WebKit');
</span><span class="cx">             assert.equal(webkit.urlForRevision(123), 'https://trac.webkit.org/123');
</span><span class="cx"> 
</span><del>-            let osWebkit1 = Repository.findById(101);
</del><ins>+            const osWebkit1 = Repository.findById(101);
</ins><span class="cx">             assert.equal(osWebkit1.name(), 'WebKit');
</span><span class="cx">             assert.equal(osWebkit1.owner(), 9);
</span><span class="cx">             assert.equal(osWebkit1.urlForRevision(123), 'https://trac.webkit.org/123');
</span><span class="cx"> 
</span><del>-            let osx = Repository.findById(9);
</del><ins>+            const osx = Repository.findById(9);
</ins><span class="cx">             assert.equal(osx.name(), 'OS X');
</span><span class="cx"> 
</span><del>-            let someTest = Test.findById(1);
</del><ins>+            const someTest = Test.findById(1);
</ins><span class="cx">             assert.equal(someTest.name(), 'SomeTest');
</span><span class="cx"> 
</span><del>-            let someOtherTest = Test.findById(2);
</del><ins>+            const someOtherTest = Test.findById(2);
</ins><span class="cx">             assert.equal(someOtherTest.name(), 'SomeOtherTest');
</span><span class="cx"> 
</span><del>-            let childTest = Test.findById(3);
</del><ins>+            const childTest = Test.findById(3);
</ins><span class="cx">             assert.equal(childTest.name(), 'ChildTest');
</span><span class="cx"> 
</span><del>-            let ios9iphone5s = Platform.findById(23);
</del><ins>+            const ios9iphone5s = Platform.findById(23);
</ins><span class="cx">             assert.equal(ios9iphone5s.name(), 'iOS 9 iPhone 5s');
</span><span class="cx"> 
</span><del>-            let mavericks = Platform.findById(46);
</del><ins>+            const mavericks = Platform.findById(46);
</ins><span class="cx">             assert.equal(mavericks.name(), 'Trunk Mavericks');
</span><span class="cx"> 
</span><del>-            assert.equal(Triggerable.all().length, 2);
</del><ins>+            const sierra = Platform.findById(104);
+            assert.equal(sierra.name(), 'Trunk Sierra MacBookPro11,2');
</ins><span class="cx"> 
</span><del>-            let osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
</del><ins>+            assert.equal(Triggerable.all().length, 3);
+
+            const osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
</ins><span class="cx">             assert.equal(osxTriggerable.name(), 'build.webkit.org');
</span><span class="cx">             assert.deepEqual(osxTriggerable.acceptedRepositories(), [webkit]);
</span><span class="cx"> 
</span><span class="lines">@@ -327,7 +344,7 @@
</span><span class="cx">             assert.equal(Triggerable.findByTestConfiguration(someOtherTest, mavericks), osxTriggerable);
</span><span class="cx">             assert.equal(Triggerable.findByTestConfiguration(childTest, mavericks), osxTriggerable);
</span><span class="cx"> 
</span><del>-            let iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
</del><ins>+            const iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
</ins><span class="cx">             assert.notEqual(iosTriggerable, osxTriggerable);
</span><span class="cx">             assert.equal(iosTriggerable.name(), 'ios-build.webkit.org');
</span><span class="cx">             assert.deepEqual(iosTriggerable.acceptedRepositories(), [webkit]);
</span><span class="lines">@@ -334,6 +351,24 @@
</span><span class="cx"> 
</span><span class="cx">             assert.equal(Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s), iosTriggerable);
</span><span class="cx">             assert.equal(Triggerable.findByTestConfiguration(childTest, ios9iphone5s), iosTriggerable);
</span><ins>+
+            const macTriggerable = Triggerable.findByTestConfiguration(someTest, sierra);
+            assert.equal(macTriggerable.name(), 'mac-build.webkit.org');
+            assert.deepEqual(Repository.sortByName(macTriggerable.acceptedRepositories()), [osx, webkit]);
+            assert(macTriggerable.acceptsTest(someTest));
+
+            const groups = macTriggerable.repositoryGroups();
+            assert.deepEqual(groups.length, 2);
+            TriggerableRepositoryGroup.sortByName(groups);
+
+            assert.equal(groups[0].name(), 'system-and-roots');
+            assert.equal(groups[0].acceptsCustomRoots(), true);
+            assert.deepEqual(Repository.sortByName(groups[0].repositories()), [osx]);
+
+            assert.equal(groups[1].name(), 'system-and-webkit');
+            assert.equal(groups[1].acceptsCustomRoots(), false);
+            assert.deepEqual(Repository.sortByName(groups[1].repositories()), [osx, webkit]);
+
</ins><span class="cx">         });
</span><span class="cx">     });
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsdatabasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/database.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/database.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/tools/js/database.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -145,6 +145,7 @@
</span><span class="cx">     'tests': 'test',
</span><span class="cx">     'tracker_repositories': 'tracrepo',
</span><span class="cx">     'triggerable_configurations': 'trigconfig',
</span><ins>+    'triggerable_repository_groups': 'repositorygroup',
</ins><span class="cx">     'triggerable_repositories': 'trigrepo',
</span><span class="cx">     'platforms': 'platform',
</span><span class="cx">     'reports': 'report',
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsv3modelsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (214974 => 214975)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2017-04-05 22:55:25 UTC (rev 214974)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2017-04-05 23:12:46 UTC (rev 214975)
</span><span class="lines">@@ -28,6 +28,7 @@
</span><span class="cx"> importFromV3('models/test-group.js', 'TestGroup');
</span><span class="cx"> importFromV3('models/time-series.js', 'TimeSeries');
</span><span class="cx"> importFromV3('models/triggerable.js', 'Triggerable');
</span><ins>+importFromV3('models/triggerable.js', 'TriggerableRepositoryGroup');
</ins><span class="cx"> importFromV3('models/uploaded-file.js', 'UploadedFile');
</span><span class="cx"> 
</span><span class="cx"> importFromV3('privileged-api.js', 'PrivilegedAPI');
</span></span></pre>
</div>
</div>

</body>
</html>