Introduce the notion of repository groups to triggerables
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Apr 2017 23:12:46 +0000 (23:12 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Apr 2017 23:12:46 +0000 (23:12 +0000)
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.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@214975 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/admin/triggerables.php
Websites/perf.webkit.org/public/include/manifest-generator.php
Websites/perf.webkit.org/public/v3/models/manifest.js
Websites/perf.webkit.org/public/v3/models/triggerable.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/server-tests/api-manifest-tests.js
Websites/perf.webkit.org/tools/js/database.js
Websites/perf.webkit.org/tools/js/v3-models.js

index 3517766..6d0edd3 100644 (file)
@@ -1,3 +1,87 @@
+2017-04-05  Ryosuke Niwa  <rniwa@webkit.org>
+
+        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.
+
 2017-03-31  Ryosuke Niwa  <rniwa@webkit.org>
 
         Build fix. For OS versions, we can end up with non-alphanumeric revision.
index 77e3f0c..05851e1 100644 (file)
@@ -23,6 +23,7 @@ DROP TABLE IF EXISTS analysis_strategies CASCADE;
 DROP TYPE IF EXISTS analysis_task_result_type CASCADE;
 DROP TABLE IF EXISTS build_triggerables CASCADE;
 DROP TABLE IF EXISTS triggerable_configurations CASCADE;
+DROP TABLE IF EXISTS triggerable_repository_groups CASCADE;
 DROP TABLE IF EXISTS triggerable_repositories CASCADE;
 DROP TABLE IF EXISTS uploaded_files CASCADE;
 DROP TABLE IF EXISTS bugs CASCADE;
@@ -229,12 +230,20 @@ CREATE TABLE bugs (
 
 CREATE TABLE build_triggerables (
     triggerable_id serial PRIMARY KEY,
-    triggerable_name varchar(64) NOT NULL UNIQUE);
+    triggerable_name varchar(64) NOT NULL UNIQUE,
+    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));
 
 CREATE TABLE triggerable_repositories (
-    trigrepo_triggerable integer REFERENCES build_triggerables NOT NULL,
     trigrepo_repository integer REFERENCES repositories NOT NULL,
-    trigrepo_sub_roots boolean NOT NULL DEFAULT FALSE);
+    trigrepo_group integer REFERENCES triggerable_repository_groups NOT NULL);
 
 CREATE TABLE triggerable_configurations (
     trigconfig_test integer REFERENCES tests NOT NULL,
index 9c62fdb..a8a26b1 100644 (file)
@@ -13,71 +13,189 @@ if ($db) {
     } else if ($action == 'update') {
         if (update_field('build_triggerables', 'triggerable', 'name'))
             regenerate_manifest();
+        else if (update_field('build_triggerables', 'triggerable', 'disabled', Database::to_database_boolean(array_get($_POST, 'disabled'))))
+            regenerate_manifest();
         else
             notice('Invalid parameters.');
+    } 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->begin_transaction();
+            $select = array('repository' => $repository_id, 'triggerable' => $triggerable_id);
+            $update = array('repository' => $repository_id, 'triggerable' => $triggerable_id, 'required' => Database::to_database_boolean($required));
+            if (!$db->update_row('triggerable_repositories', 'trigrepo', $select, $update, 'repository')) {
+                notice("Failed to update the association of repository $repository_id with triggerable $triggerable_id.");
+                $db->rollback_transaction();
+            } else
+                $db->commit_transaction();
+        } else if ($association == 'not-accepted') {
+            $db->begin_transaction();
+            $result = $db->query_and_get_affected_rows("DELETE FROM triggerable_repositories WHERE trigrepo_triggerable = $1 AND trigrepo_repository = $2",
+                array($triggerable_id, $repository_id));
+            if ($result > 1) {
+                notice("Failed to update the association of repository $repository_id with triggerable $triggerable_id.");
+                $db->rollback_transaction();
+            } else
+                $db->commit_transaction();
+        }
     } else if ($action == 'update-repositories') {
-        $triggerable_id = intval($_POST['id']);
+        $group_id = intval($_POST['group']);
 
         $db->begin_transaction();
-        $db->query_and_get_affected_rows("DELETE FROM triggerable_repositories WHERE trigrepo_triggerable = $1", array($triggerable_id));
-
-        $repositories = array_get($_POST, 'repositories');
-        $suceeded = TRUE;
-        if ($repositories) {
-            foreach ($repositories as $repository_id) {
-                if (!$db->insert_row('triggerable_repositories', 'trigrepo', array('triggerable' => $triggerable_id, 'repository' => $repository_id), NULL)) {
-                    $suceeded = FALSE;
-                    notice("Failed to associate repository $repository_id with triggerable $triggerable_id.");
-                    break;
-                }
-            }
-        }
+        $db->query_and_get_affected_rows("DELETE FROM triggerable_repositories WHERE trigrepo_group = $1", array($group_id));
+        $suceeded = insert_triggerable_repositories($db, $group_id, array_get($_POST, 'repositories'));
         if ($suceeded) {
             $db->commit_transaction();
             notice('Updated the association.');
             regenerate_manifest();
         } else
             $db->rollback_transaction();
+    } else if ($action == 'add-repository-group') {
+        $triggerable_id = intval($_POST['triggerable']);
+        $name = $_POST['name'];
+
+        $db->begin_transaction();
+        $group_id = $db->insert_row('triggerable_repository_groups', 'repositorygroup', array('name' => $name, 'triggerable' => $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->rollback_transaction();
+        else {
+            $db->commit_transaction();
+            notice('Updated the association.');
+            regenerate_manifest();
+        }
     }
 
     $repository_rows = $db->fetch_table('repositories', 'repository_name');
-    $repository_names = array();
-
 
     $page = new AdministrativePage($db, 'build_triggerables', 'triggerable', array(
         'name' => array('editing_mode' => 'string'),
-        'repositories' => array('custom' => function ($triggerable_row) use (&$repository_rows) {
-            return array(generate_repository_checkboxes($triggerable_row['triggerable_id'], $repository_rows));
-        }),
+        'disabled' => array('editing_mode' => 'boolean', 'post_insertion' => TRUE),
+        'repositories' => array(
+            'label' => 'Repository Groups',
+            'subcolumns'=> array('ID', 'Name', 'Description', 'Accepts Roots', 'Repositories'),
+            'custom' => function ($triggerable_row) use (&$db, &$repository_rows) {
+                return generate_repository_list($db, $triggerable_row['triggerable_id'], $repository_rows);
+            }),
     ));
 
-    function generate_repository_checkboxes($triggerable_id, $repository_rows) {
-        global $db;
+    $page->render_table('name');
+    $page->render_form_to_add();
+}
+
+function insert_triggerable_repositories($db, $group_id, $repositories)
+{
+    if (!$repositories)
+        return TRUE;
+    foreach ($repositories as $repository_id) {
+        if (!$db->insert_row('triggerable_repositories', 'trigrepo', array('group' => $group_id, 'repository' => $repository_id), NULL)) {
+            notice("Failed to associate repository $repository_id with repository group $group_id.");
+            return FALSE;
+        }
+    }
+    return TRUE;
+}
+
 
-        $repository_rows = $db->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));
+function generate_repository_list($db, $triggerable_id, $repository_rows) {
+    $group_forms = array();
 
-        $form = <<< END
-<form method="POST">
-<input type="hidden" name="id" value="$triggerable_id">
-<input type="hidden" name="action" value="update-repositories">
+    $repository_groups = $db->select_rows('triggerable_repository_groups', 'repositorygroup', array('triggerable' => $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 = <<< END
+            <form method="POST">
+            <input type="hidden" name="action" value="update-group-name">
+            <input type="hidden" name="id" value="$group_id">
+            <input type="text" name="name" value="$group_name">
+            </form>
 END;
 
-        foreach ($repository_rows as $row) {
-            $checked = $row['trigrepo_triggerable'] ? ' checked' : '';
-            $form .= <<< END
-<label><input type="checkbox" name="repositories[]" value="{$row['repository_id']}"$checked>{$row['repository_name']}</label>
+        $group_description_form = <<< END
+            <form method="POST">
+            <input type="hidden" name="action" value="update-group-description">
+            <input type="hidden" name="id" value="$group_id">
+            <input name="description" value="$group_description">
+            </form>
 END;
-        }
 
-        return $form . <<< END
-<button>Save</button>
-</form>
+        $group_accepts_roots = <<< END
+            <form method="POST">
+            <input type="hidden" name="action" value="update-group-accept-roots">
+            <input type="hidden" name="id" value="$group_id">
+            <input type="checkbox" name="accepts" $checked_if_accepts_roots>
+            <button type="submit">Save</button>
+            </form>
 END;
+
+        array_push($group_forms, array($group_id, $group_name_form, $group_description_form, $group_accepts_roots, generate_repository_form($db, $repository_rows, $group_id)));
     }
 
-    $page->render_table('name');
-    $page->render_form_to_add();
+    $new_group_checkboxes = generate_repository_checkboxes($db, $repository_rows);
+    $new_group_form = <<< END
+        <form method="POST">
+        <input type="hidden" name="action" value="add-repository-group">
+        <input type="hidden" name="triggerable" value="$triggerable_id">
+        <input type="text" name="name" value="" required><br>
+        $new_group_checkboxes
+        <br><button type="submit">Add</button></form>
+END;
+
+    array_push($group_forms, $new_group_form);
+
+    return $group_forms;
+}
+
+function generate_repository_form($db, $repository_rows, $group_id)
+{
+    $checkboxes = generate_repository_checkboxes($db, $repository_rows, $group_id);
+    return <<< END
+        <form method="POST">
+        <input type="hidden" name="action" value="update-repositories">
+        <input type="hidden" name="group" value="$group_id">
+        $checkboxes
+        <br><button type="submit">Save</button></form>
+END;
+}
+
+function generate_repository_checkboxes($db, $repository_rows, $group_id = NULL)
+{
+    $repositories_in_group = array();
+    if ($group_id) {
+        $group_repository_rows = $db->select_rows('triggerable_repositories', 'trigrepo', array('group' => $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 .= "<label><input type=\"checkbox\" name=\"repositories[]\" value=\"$id\" $checked>$name</label>";
+    }
+    return $form;
 }
 
 require('../include/admin-footer.php');
index 3a71edf..0daa254 100644 (file)
@@ -189,32 +189,64 @@ class ManifestGenerator {
 
     static function fetch_triggerables($db, $query)
     {
-        $triggerables = $db->fetch_table('build_triggerables');
+        $triggerables = $db->select_rows('build_triggerables', 'triggerable', $query);
         if (!$triggerables)
             return array();
 
         $id_to_triggerable = array();
-        foreach ($triggerables as $row) {
+        $triggerable_id_to_repository_set = array();
+        foreach ($triggerables as &$row) {
             $id = $row['triggerable_id'];
             $id_to_triggerable[$id] = array(
-                'id' => $id,
                 'name' => $row['triggerable_name'],
+                'isDisabled' => Database::is_true($row['triggerable_disabled']),
                 'acceptedRepositories' => array(),
+                'repositoryGroups' => array(),
                 'configurations' => array());
+            $triggerable_id_to_repository_set[$id] = array();
         }
 
-        $repository_map = $db->fetch_table('triggerable_repositories');
-        if ($repository_map) {
-            foreach ($repository_map as $row) {
-                $triggerable = &$id_to_triggerable[$row['trigrepo_triggerable']];
-                array_push($triggerable['acceptedRepositories'], $row['trigrepo_repository']);
+        $repository_groups = $db->fetch_table('triggerable_repository_groups', 'repositorygroup_name');
+        $group_repositories = $db->fetch_table('triggerable_repositories');
+        if ($repository_groups && $group_repositories) {
+            $repository_set_by_group = array();
+            foreach ($group_repositories as &$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']);
+            }
+            foreach ($repository_groups as &$group_row) {
+                $triggerable_id = $group_row['repositorygroup_triggerable'];
+                if (!array_key_exists($triggerable_id, $id_to_triggerable))
+                    continue;
+                $triggerable = &$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' => $group_row['repositorygroup_id'],
+                    'name' => $group_row['repositorygroup_name'],
+                    'description' => $group_row['repositorygroup_description'],
+                    'acceptsCustomRoots' => Database::is_true($group_row['repositorygroup_accepts_roots']),
+                    'repositories' => $repository_list));
+                // V2 UI compatibility.
+                foreach ($repository_list as $repository_id) {
+                    $set = &$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);
+                }
+
             }
         }
 
         $configuration_map = $db->fetch_table('triggerable_configurations');
         if ($configuration_map) {
-            foreach ($configuration_map as $row) {
-                $triggerable = &$id_to_triggerable[$row['trigconfig_triggerable']];
+            foreach ($configuration_map as &$row) {
+                $triggerable_id = $row['trigconfig_triggerable'];
+                if (!array_key_exists($triggerable_id, $id_to_triggerable))
+                    continue;
+                $triggerable = &$id_to_triggerable[$triggerable_id];
                 array_push($triggerable['configurations'], array($row['trigconfig_test'], $row['trigconfig_platform']));
             }
         }
index 8fedc42..f8436da 100644 (file)
@@ -47,6 +47,14 @@ class Manifest {
             raw.acceptedRepositories = raw.acceptedRepositories.map((repositoryId) => {
                 return Repository.findById(repositoryId);
             });
+            raw.repositoryGroups = raw.repositoryGroups.map((group) => {
+                group.repositories = group.repositories.map((repositoryId) => Repository.findById(repositoryId));
+                return TriggerableRepositoryGroup.ensureSingleton(group.id, group);
+            });
+            raw.configurations = raw.configurations.map((configuration) => {
+                const [testId, platformId] = configuration;
+                return {test: Test.findById(testId), platform: Platform.findById(platformId)};
+            });
         });
 
         Instrumentation.endMeasuringTime('Manifest', '_didFetchManifest');
index 422b739..53d5090 100644 (file)
@@ -4,19 +4,26 @@ class Triggerable extends LabeledObject {
     {
         super(id, object);
         this._name = object.name;
+        this._isDisabled = !!object.isDisabled;
         this._acceptedRepositories = object.acceptedRepositories;
+        this._repositoryGroups = object.repositoryGroups;
         this._configurationList = object.configurations;
+        this._acceptedTests = new Set;
 
-        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
+        const configurationMap = this.ensureNamedStaticMap('testConfigurations');
         for (const config of object.configurations) {
-            const [testId, platformId] = config;
-            const key = `${testId}-${platformId}`;
+            const key = `${config.test.id()}-${config.platform.id()}`;
+            this._acceptedTests.add(config.test);
             console.assert(!(key in configurationMap));
             configurationMap[key] = this;
         }
     }
 
+    isDisabled() { return this._isDisabled; }
     acceptedRepositories() { return this._acceptedRepositories; }
+    repositoryGroups() { return this._repositoryGroups; }
+
+    acceptsTest(test) { return this._acceptedTests.has(test); }
 
     static findByTestConfiguration(test, platform)
     {
@@ -30,8 +37,25 @@ class Triggerable extends LabeledObject {
         }
         return null;
     }
+}
 
+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; }
 }
 
-if (typeof module != 'undefined')
+if (typeof module != 'undefined') {
     module.exports.Triggerable = Triggerable;
+    module.exports.TriggerableRepositoryGroup = TriggerableRepositoryGroup;
+}
+
index 6688d58..8a5c84b 100644 (file)
@@ -413,7 +413,8 @@ class AnalysisTaskPage extends PageWithHeading {
         const platform = task.platform();
         const metric = task.metric();
         const lastModified = platform.lastModified(metric);
-        this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
+        const triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
+        this._triggerable = triggerable && !triggerable.isDisabled() ? triggerable : null;
         this._metric = metric;
 
         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
index 833835c..54acb16 100644 (file)
@@ -268,11 +268,13 @@ describe('/api/manifest', function () {
             db.insert('repositories', {id: 101, name: 'WebKit', owner: 9, url: 'https://trac.webkit.org/$1'}),
             db.insert('build_triggerables', {id: 200, name: 'build.webkit.org'}),
             db.insert('build_triggerables', {id: 201, name: 'ios-build.webkit.org'}),
+            db.insert('build_triggerables', {id: 202, name: 'mac-build.webkit.org', disabled: true}),
             db.insert('tests', {id: 1, name: 'SomeTest'}),
             db.insert('tests', {id: 2, name: 'SomeOtherTest'}),
             db.insert('tests', {id: 3, name: 'ChildTest', parent: 1}),
             db.insert('platforms', {id: 23, name: 'iOS 9 iPhone 5s'}),
             db.insert('platforms', {id: 46, name: 'Trunk Mavericks'}),
+            db.insert('platforms', {id: 104, name: 'Trunk Sierra MacBookPro11,2'}),
             db.insert('test_metrics', {id: 5, test: 1, name: 'Time'}),
             db.insert('test_metrics', {id: 8, test: 2, name: 'FrameRate'}),
             db.insert('test_metrics', {id: 9, test: 3, name: 'Time'}),
@@ -282,58 +284,91 @@ describe('/api/manifest', function () {
             db.insert('test_configurations', {id: 104, metric: 5, platform: 23, type: 'current'}),
             db.insert('test_configurations', {id: 105, metric: 8, platform: 23, type: 'current'}),
             db.insert('test_configurations', {id: 106, metric: 9, platform: 23, type: 'current'}),
-            db.insert('triggerable_repositories', {triggerable: 200, repository: 11}),
-            db.insert('triggerable_repositories', {triggerable: 201, repository: 11}),
+            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}),
             db.insert('triggerable_configurations', {triggerable: 200, test: 1, platform: 46}),
             db.insert('triggerable_configurations', {triggerable: 200, test: 2, platform: 46}),
             db.insert('triggerable_configurations', {triggerable: 201, test: 1, platform: 23}),
             db.insert('triggerable_configurations', {triggerable: 201, test: 2, platform: 23}),
+            db.insert('triggerable_configurations', {triggerable: 202, test: 1, platform: 104}),
+            db.insert('triggerable_configurations', {triggerable: 202, test: 2, platform: 104}),
         ]).then(() => {
             return Manifest.fetch();
         }).then(() => {
-            let webkit = Repository.findById(11);
+            const webkit = Repository.findById(11);
             assert.equal(webkit.name(), 'WebKit');
             assert.equal(webkit.urlForRevision(123), 'https://trac.webkit.org/123');
 
-            let osWebkit1 = Repository.findById(101);
+            const osWebkit1 = Repository.findById(101);
             assert.equal(osWebkit1.name(), 'WebKit');
             assert.equal(osWebkit1.owner(), 9);
             assert.equal(osWebkit1.urlForRevision(123), 'https://trac.webkit.org/123');
 
-            let osx = Repository.findById(9);
+            const osx = Repository.findById(9);
             assert.equal(osx.name(), 'OS X');
 
-            let someTest = Test.findById(1);
+            const someTest = Test.findById(1);
             assert.equal(someTest.name(), 'SomeTest');
 
-            let someOtherTest = Test.findById(2);
+            const someOtherTest = Test.findById(2);
             assert.equal(someOtherTest.name(), 'SomeOtherTest');
 
-            let childTest = Test.findById(3);
+            const childTest = Test.findById(3);
             assert.equal(childTest.name(), 'ChildTest');
 
-            let ios9iphone5s = Platform.findById(23);
+            const ios9iphone5s = Platform.findById(23);
             assert.equal(ios9iphone5s.name(), 'iOS 9 iPhone 5s');
 
-            let mavericks = Platform.findById(46);
+            const mavericks = Platform.findById(46);
             assert.equal(mavericks.name(), 'Trunk Mavericks');
 
-            assert.equal(Triggerable.all().length, 2);
+            const sierra = Platform.findById(104);
+            assert.equal(sierra.name(), 'Trunk Sierra MacBookPro11,2');
 
-            let osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
+            assert.equal(Triggerable.all().length, 3);
+
+            const osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
             assert.equal(osxTriggerable.name(), 'build.webkit.org');
             assert.deepEqual(osxTriggerable.acceptedRepositories(), [webkit]);
 
             assert.equal(Triggerable.findByTestConfiguration(someOtherTest, mavericks), osxTriggerable);
             assert.equal(Triggerable.findByTestConfiguration(childTest, mavericks), osxTriggerable);
 
-            let iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
+            const iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
             assert.notEqual(iosTriggerable, osxTriggerable);
             assert.equal(iosTriggerable.name(), 'ios-build.webkit.org');
             assert.deepEqual(iosTriggerable.acceptedRepositories(), [webkit]);
 
             assert.equal(Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s), iosTriggerable);
             assert.equal(Triggerable.findByTestConfiguration(childTest, ios9iphone5s), iosTriggerable);
+
+            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]);
+
         });
     });
 
index 872f5a5..4fbd332 100644 (file)
@@ -145,6 +145,7 @@ const tableToPrefixMap = {
     'tests': 'test',
     'tracker_repositories': 'tracrepo',
     'triggerable_configurations': 'trigconfig',
+    'triggerable_repository_groups': 'repositorygroup',
     'triggerable_repositories': 'trigrepo',
     'platforms': 'platform',
     'reports': 'report',
index 31bfd16..23d9de6 100644 (file)
@@ -28,6 +28,7 @@ importFromV3('models/test.js', 'Test');
 importFromV3('models/test-group.js', 'TestGroup');
 importFromV3('models/time-series.js', 'TimeSeries');
 importFromV3('models/triggerable.js', 'Triggerable');
+importFromV3('models/triggerable.js', 'TriggerableRepositoryGroup');
 importFromV3('models/uploaded-file.js', 'UploadedFile');
 
 importFromV3('privileged-api.js', 'PrivilegedAPI');