Analysis task page should allow specifying commits that caused or fixed a regression...
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 16 Mar 2016 07:02:28 +0000 (07:02 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 16 Mar 2016 07:02:28 +0000 (07:02 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155529

Reviewed by Chris Dumez.

Added the capability to associate revisions that caused or fixed a progression or a regression for which
an analysis task was created. Added task_commits that stores this relationship and added the backend
support to retrieve this table in /api/analysis-tasks and an privileged API to update this table at
/privileged-api/associate-commit.

Also extracted a new component, MutableListView, out of AnalysisTaskPage to render and manipulate a list
of mutable items, and used it to render the list of associated bugs and commits. The view takes a list of
kinds (e.g. repositories or bug trackers), and accepts a pair of a kind and arbitrary text as a new item
value.

* init-database.sql: Added task_commits table.

* public/api/analysis-tasks.php:
(main):
(fetch_associated_data_for_tasks): Renamed from fetch_and_push_bugs_to_tasks now that it also fetches
the list of commits associated with each analysis task by calling CommitLogFetcher::fetch_for_tasks.
Also fixe the bug that we were not taking
(format_task): No longer sets 'category' since the computation of category now depends on the list of
commits associated with this analysis task which aren't available until fetch_associated_data_for_tasks.
(determine_category): Added. Categorize any analysis tasks with "fixes" commits as "closed" and "causes"
commits as "identified".

* public/include/commit-log-fetcher.php:
(CommitLogFetcher::__construct): Remove the unused instance variable.
(CommitLogFetcher::fetch_for_tasks): Added. Fetches all commits associated with a list of analysis tasks.
Assumes the caller (fetch_associated_data_for_tasks) had setup "fixes" and "causes" fields on each task.

* public/privileged-api/associate-commit.php: Added. Updates task_commits table to associate or disassociate
a commit with an analysis task. When the specified analysis task and the specified commit are already
associated, we simply update the table instead of adding a duplicating entry or error. For dissociation,
the front-end specifies the commit ID.
(main): Added.

* public/v3/index.html:
* public/v3/components/mutable-list-view.js: Added. Used by the list associated bugs and commits.
(MutableListView): Added.
(MutableListView.prototype.setList): Added.
(MutableListView.prototype.setKindList): Added.
(MutableListView.prototype.setAddCallback): Added. This callback is invoked when the user tries to add
a new item to the list.
(MutableListView.prototype.render): Added.
(MutableListView.prototype._submitted): Added.
(MutableListView.cssTemplate):
(MutableListView.htmlTemplate):
(MutableListItem): Added. RemovalLink could be a hyperlink or a callback and gets involved when the user
tries to delete this item.
(MutableListItem.prototype.content):

* public/v3/models/analysis-task.js:
(AnalysisTask): Added the support of the list of commits that fixed and caused changes.
(AnalysisTask.prototype.updateSingleton): Ditto.
(AnalysisTask.prototype.causes): Added.
(AnalysisTask.prototype.fixes): Added.
(AnalysisTask.prototype.associateCommit): Added. Use the API added at /privileged-api/associate-commit
to associate a new commit with this analysis task. Each commit has either caused or fixed the change.
(AnalysisTask.prototype.dissociateCommit): Added. Use the same API to disassociate each commit.
(AnalysisTask._constructAnalysisTasksFromRawData): Find all commits associated with each analysis task.
Because commit log objects use a fake ID fdue to /api/measurement-set not providing commit IDs, we must
use CommitLog.findByRemoteId to find each commit instead of usual CommitLog.findById.
(AnalysisTask._constructAnalysisTasksFromRawData.resolveCommits): Added.

* public/v3/models/build-request.js:
(BuildRequest.prototype.hasFinished): Renamed from hasCompleted since it was confusing for this._status
being "completed" wasn't a necessary condition for this function to return true.

* public/v3/models/commit-log.js:
(CommitLog): Added the static map for actual commit ID instead of a fake ID created in ensureSingleton.
(CommitLog.prototype.remoteId): Added. Returns the real commit ID.
(CommitLog.findByRemoteId): Added. Finds an CommitLog object using the real ID.

* public/v3/models/test-group.js:
(TestGroup.prototype.hasFinished): Renamed from hasCompleted to match the rename in BuildRequest.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage): Added lists for the commits that fixed and caused the change using MutableListView.
Also adopted MutableListView for the list of associated bugs.
(AnalysisTaskPage.prototype.render): Added the code to populate the newly added lists.
(AnalysisTaskPage.prototype._makeCommitListItem): Added.
(AnalysisTaskPage.prototype._associateBug): Now this is a callback from MutableListView.
(AnalysisTaskPage.prototype._associateCommit): Added.
(AnalysisTaskPage.prototype._dissociateCommit): Added.
(AnalysisTaskPage.htmlTemplate):
(AnalysisTaskPage.cssTemplate):

* public/v3/remote.js:
(getJSON): Spit out the entire responseText when JSON failed to parse to make debugging easier.

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

13 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/api/analysis-tasks.php
Websites/perf.webkit.org/public/include/commit-log-fetcher.php
Websites/perf.webkit.org/public/privileged-api/associate-commit.php [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/components/mutable-list-view.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/models/analysis-task.js
Websites/perf.webkit.org/public/v3/models/build-request.js
Websites/perf.webkit.org/public/v3/models/commit-log.js
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/public/v3/remote.js

index 927d970..4ec2ff7 100644 (file)
@@ -1,3 +1,97 @@
+2016-03-16  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Analysis task page should allow specifying commits that caused or fixed a regression or a progression
+        https://bugs.webkit.org/show_bug.cgi?id=155529
+
+        Reviewed by Chris Dumez.
+
+        Added the capability to associate revisions that caused or fixed a progression or a regression for which
+        an analysis task was created. Added task_commits that stores this relationship and added the backend
+        support to retrieve this table in /api/analysis-tasks and an privileged API to update this table at
+        /privileged-api/associate-commit.
+
+        Also extracted a new component, MutableListView, out of AnalysisTaskPage to render and manipulate a list
+        of mutable items, and used it to render the list of associated bugs and commits. The view takes a list of
+        kinds (e.g. repositories or bug trackers), and accepts a pair of a kind and arbitrary text as a new item
+        value.
+
+        * init-database.sql: Added task_commits table.
+
+        * public/api/analysis-tasks.php:
+        (main):
+        (fetch_associated_data_for_tasks): Renamed from fetch_and_push_bugs_to_tasks now that it also fetches
+        the list of commits associated with each analysis task by calling CommitLogFetcher::fetch_for_tasks.
+        Also fixe the bug that we were not taking 
+        (format_task): No longer sets 'category' since the computation of category now depends on the list of
+        commits associated with this analysis task which aren't available until fetch_associated_data_for_tasks.
+        (determine_category): Added. Categorize any analysis tasks with "fixes" commits as "closed" and "causes"
+        commits as "identified".
+
+        * public/include/commit-log-fetcher.php:
+        (CommitLogFetcher::__construct): Remove the unused instance variable.
+        (CommitLogFetcher::fetch_for_tasks): Added. Fetches all commits associated with a list of analysis tasks.
+        Assumes the caller (fetch_associated_data_for_tasks) had setup "fixes" and "causes" fields on each task.
+
+        * public/privileged-api/associate-commit.php: Added. Updates task_commits table to associate or disassociate
+        a commit with an analysis task. When the specified analysis task and the specified commit are already
+        associated, we simply update the table instead of adding a duplicating entry or error. For dissociation,
+        the front-end specifies the commit ID.
+        (main): Added.
+
+        * public/v3/index.html:
+        * public/v3/components/mutable-list-view.js: Added. Used by the list associated bugs and commits.
+        (MutableListView): Added.
+        (MutableListView.prototype.setList): Added.
+        (MutableListView.prototype.setKindList): Added.
+        (MutableListView.prototype.setAddCallback): Added. This callback is invoked when the user tries to add
+        a new item to the list.
+        (MutableListView.prototype.render): Added.
+        (MutableListView.prototype._submitted): Added.
+        (MutableListView.cssTemplate):
+        (MutableListView.htmlTemplate):
+        (MutableListItem): Added. RemovalLink could be a hyperlink or a callback and gets involved when the user
+        tries to delete this item.
+        (MutableListItem.prototype.content):
+
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask): Added the support of the list of commits that fixed and caused changes.
+        (AnalysisTask.prototype.updateSingleton): Ditto.
+        (AnalysisTask.prototype.causes): Added.
+        (AnalysisTask.prototype.fixes): Added.
+        (AnalysisTask.prototype.associateCommit): Added. Use the API added at /privileged-api/associate-commit
+        to associate a new commit with this analysis task. Each commit has either caused or fixed the change.
+        (AnalysisTask.prototype.dissociateCommit): Added. Use the same API to disassociate each commit.
+        (AnalysisTask._constructAnalysisTasksFromRawData): Find all commits associated with each analysis task.
+        Because commit log objects use a fake ID fdue to /api/measurement-set not providing commit IDs, we must
+        use CommitLog.findByRemoteId to find each commit instead of usual CommitLog.findById.
+        (AnalysisTask._constructAnalysisTasksFromRawData.resolveCommits): Added.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.prototype.hasFinished): Renamed from hasCompleted since it was confusing for this._status
+        being "completed" wasn't a necessary condition for this function to return true.
+
+        * public/v3/models/commit-log.js:
+        (CommitLog): Added the static map for actual commit ID instead of a fake ID created in ensureSingleton.
+        (CommitLog.prototype.remoteId): Added. Returns the real commit ID.
+        (CommitLog.findByRemoteId): Added. Finds an CommitLog object using the real ID.
+
+        * public/v3/models/test-group.js:
+        (TestGroup.prototype.hasFinished): Renamed from hasCompleted to match the rename in BuildRequest.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage): Added lists for the commits that fixed and caused the change using MutableListView.
+        Also adopted MutableListView for the list of associated bugs.
+        (AnalysisTaskPage.prototype.render): Added the code to populate the newly added lists.
+        (AnalysisTaskPage.prototype._makeCommitListItem): Added.
+        (AnalysisTaskPage.prototype._associateBug): Now this is a callback from MutableListView.
+        (AnalysisTaskPage.prototype._associateCommit): Added.
+        (AnalysisTaskPage.prototype._dissociateCommit): Added.
+        (AnalysisTaskPage.htmlTemplate):
+        (AnalysisTaskPage.cssTemplate):
+
+        * public/v3/remote.js:
+        (getJSON): Spit out the entire responseText when JSON failed to parse to make debugging easier.
+
 2016-03-15  Ryosuke Niwa  <rniwa@webkit.org>
 
         Extract the code to format commit logs into its own PHP file
index 1ddeb1b..9271549 100644 (file)
@@ -16,6 +16,7 @@ DROP TABLE tests CASCADE;
 DROP TABLE reports CASCADE;
 DROP TABLE tracker_repositories CASCADE;
 DROP TABLE bug_trackers CASCADE;
+DROP TABLE task_commits CASCADE;
 DROP TABLE analysis_tasks CASCADE;
 DROP TABLE analysis_strategies CASCADE;
 DROP TYPE analysis_task_result_type CASCADE;
@@ -202,6 +203,12 @@ CREATE TABLE analysis_tasks (
     CONSTRAINT analysis_task_should_not_be_associated_with_single_run
         CHECK ((task_start_run IS NULL AND task_end_run IS NULL) OR (task_start_run IS NOT NULL AND task_end_run IS NOT NULL)));
 
+CREATE TABLE task_commits (
+    taskcommit_task integer NOT NULL REFERENCES analysis_tasks ON DELETE CASCADE,
+    taskcommit_commit integer NOT NULL REFERENCES commits ON DELETE CASCADE,
+    taskcommit_is_fix boolean NOT NULL
+    CONSTRAINT task_commit_must_be_unique UNIQUE(taskcommit_task, taskcommit_commit));
+
 CREATE TABLE bugs (
     bug_id serial PRIMARY KEY,
     bug_task integer REFERENCES analysis_tasks NOT NULL,
index 43673a5..f15a823 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 require('../include/json-header.php');
+require('../include/commit-log-fetcher.php');
 
 function main($path) {
     $db = new Database;
@@ -42,12 +43,10 @@ function main($path) {
     }
 
     $tasks = array_map("format_task", $tasks);
-    $bugs = fetch_and_push_bugs_to_tasks($db, $tasks);
-
-    exit_with_success(array('analysisTasks' => $tasks, 'bugs' => $bugs));
+    exit_with_success(fetch_associated_data_for_tasks($db, $tasks));
 }
 
-function fetch_and_push_bugs_to_tasks($db, &$tasks) {
+function fetch_associated_data_for_tasks($db, &$tasks) {
     $task_ids = array();
     $task_by_id = array();
     foreach ($tasks as &$task) {
@@ -65,10 +64,15 @@ function fetch_and_push_bugs_to_tasks($db, &$tasks) {
         array_push($associated_task['bugs'], $bug['id']);
     }
 
+    $commit_log_fetcher = new CommitLogFetcher($db);
+    $commits = $commit_log_fetcher->fetch_for_tasks($task_ids, $task_by_id);
+    if (!is_array($commits))
+        exit_with_error('FailedToFetchCommits');
+
     $task_build_counts = $db->query_and_fetch_all('SELECT
         testgroup_task AS "task",
         count(testgroup_id) as "total",
-        sum(case when request_status = \'failed\' or request_status = \'completed\' then 1 else 0 end) as "finished"
+        sum(case when request_status = \'failed\' or request_status = \'completed\' or request_status = \'canceled\' then 1 else 0 end) as "finished"
         FROM analysis_test_groups, build_requests
         WHERE request_group = testgroup_id AND testgroup_task = ANY($1) GROUP BY testgroup_task',
         array('{' . implode(', ', $task_ids) . '}'));
@@ -79,19 +83,13 @@ function fetch_and_push_bugs_to_tasks($db, &$tasks) {
         $task = &$task_by_id[$build_count['task']];
         $task['buildRequestCount'] = $build_count['total'];
         $task['finishedBuildRequestCount'] = $build_count['finished'];
+        $task['category'] = determine_category($task);
     }
 
-    return $bugs;
+    return array('analysisTasks' => $tasks, 'bugs' => $bugs, 'commits' => $commits);
 }
 
 function format_task($task_row) {
-    $category = 'unconfirmed';
-    $result = $task_row['task_result'];
-    if ($result == 'unchanged' || $result == 'inconclusive')
-        $category = 'closed';
-    else if ($result)
-        $category = 'bisecting';
-
     return array(
         'id' => $task_row['task_id'],
         'name' => $task_row['task_name'],
@@ -105,13 +103,29 @@ function format_task($task_row) {
         'startRunTime' => Database::to_js_time($task_row['task_start_run_time']),
         'endRun' => $task_row['task_end_run'],
         'endRunTime' => Database::to_js_time($task_row['task_end_run_time']),
-        'category' => $category,
-        'result' => $result,
+        'category' => null,
+        'result' => $task_row['task_result'],
         'needed' => $task_row['task_needed'] ? Database::is_true($task_row['task_needed']) : null,
         'bugs' => array(),
+        'causes' => array(),
+        'fixes' => array(),
     );
 }
 
+function determine_category($task) {
+    $category = 'unconfirmed';
+
+    $result = $task['result'];
+    if ($result == 'unchanged' || $result == 'inconclusive' || $task['fixes'])
+        $category = 'closed';
+    else if ($task['causes'])
+        $category = 'identified';
+    else if ($result)
+        $category = 'bisecting';
+
+    return $category;
+}
+
 main(array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array());
 
 ?>
index 2ae5e45..4ec6e1c 100644 (file)
@@ -4,7 +4,25 @@ class CommitLogFetcher {
 
     function __construct($db) {
         $this->db = $db;
-        $this->commits = array();
+    }
+
+    function fetch_for_tasks($task_id_list, $task_by_id)
+    {
+        $commit_rows = $this->db->query_and_fetch_all('SELECT task_commits.*, commits.*, committers.*
+            FROM task_commits, commits LEFT OUTER JOIN committers ON commit_committer = committer_id
+            WHERE taskcommit_commit = commit_id AND taskcommit_task = ANY ($1)', array('{' . implode(', ', $task_id_list) . '}'));
+        if (!is_array($commit_rows))
+            return NULL;
+
+        $commits = array();
+        foreach ($commit_rows as &$commit_row) {
+            $associated_task = &$task_by_id[$commit_row['taskcommit_task']];
+            $commit = $this->format_commit($commit_row, $commit_row);
+            $commit['repository'] = $commit_row['commit_repository'];
+            array_push($commits, $commit);
+            array_push($associated_task[Database::is_true($commit_row['taskcommit_is_fix']) ? 'fixes' : 'causes'], $commit_row['commit_id']);
+        }
+        return $commits;
     }
 
     function repository_id_from_name($name)
diff --git a/Websites/perf.webkit.org/public/privileged-api/associate-commit.php b/Websites/perf.webkit.org/public/privileged-api/associate-commit.php
new file mode 100644 (file)
index 0000000..07c1ce0
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+require_once('../include/json-header.php');
+
+function main() {
+    $data = ensure_privileged_api_data_and_token();
+
+    $analysis_task_id = array_get($data, 'task');
+    $repository_id = array_get($data, 'repository');
+    $revision = array_get($data, 'revision');
+    $kind = array_get($data, 'kind');
+    $commit_id_to_diassociate = array_get($data, 'commit');
+
+    $db = connect();
+    $db->begin_transaction();
+
+    require_format('AnalysisTask', $analysis_task_id, '/^\d+$/');
+    if ($commit_id_to_diassociate) {
+        require_format('Commit', $commit_id_to_diassociate, '/^\d*$/');
+
+        $count = $db->query_and_get_affected_rows("DELETE FROM task_commits WHERE taskcommit_task = $1 AND taskcommit_commit = $2",
+            array($analysis_task_id, $commit_id_to_diassociate));
+        if ($count != 1) {
+            $db->rollback_transaction();
+            exit_with_error('UnexpectedNumberOfAffectedRows', array('affectedRows' => $count));
+        }
+    } else {
+        require_format('Repository', $repository_id, '/^\d+$/');
+        require_format('Kind', $kind, '/^(cause|fix)$/');
+
+        $commit_info = array('repository' => $repository_id, 'revision' => $revision);
+        $commit_row = $db->select_first_row('commits', 'commit', $commit_info);
+        if (!$commit_row) {
+            $db->rollback_transaction();
+            exit_with_error('CommitNotFound', $commit_info);
+        }
+        $commit_id = $commit_row['commit_id'];
+
+        $association = array('task' => $analysis_task_id, 'commit' => $commit_id, 'is_fix' => Database::to_database_boolean($kind == 'fix'));
+        $commit_id = $db->update_or_insert_row('task_commits', 'taskcommit',
+            array('task' => $analysis_task_id, 'commit' => $commit_id), $association, 'commit');
+        if (!$commit_id) {
+            $db->rollback_transaction();
+            exit_with_error('FailedToAssociateCommit', $association);
+        }
+    }
+
+    $db->commit_transaction();
+
+    exit_with_success();
+}
+
+main();
+
+?>
diff --git a/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js b/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js
new file mode 100644 (file)
index 0000000..572d0b1
--- /dev/null
@@ -0,0 +1,104 @@
+
+
+class MutableListView extends ComponentBase {
+
+    constructor()
+    {
+        super('mutable-list-view');
+        this._list = [];
+        this._kindList = [];
+        this._addCallback = null;
+        this._kindMap = new Map;
+        this.content().querySelector('form').onsubmit = this._submitted.bind(this);
+    }
+
+    setList(list) { this._list = list; }
+    setKindList(list) { this._kindList = list; }
+    setAddCallback(callback) { this._addCallback = callback; }
+
+    render()
+    {
+        this.renderReplace(this.content().querySelector('.mutable-list'),
+            this._list.map(function (item) {
+                console.assert(item instanceof MutableListItem);
+                return item.content();
+            }));
+
+        var element = ComponentBase.createElement;
+        var kindMap = this._kindMap;
+        kindMap.clear();
+        this.renderReplace(this.content().querySelector('.kind'),
+            this._kindList.map(function (kind) {
+                kindMap.set(kind.id(), kind);
+                return element('option', {value: kind.id()}, kind.label());
+            }));
+    }
+
+    _submitted(event)
+    {
+        event.preventDefault();
+        if (this._addCallback)
+            this._addCallback(this._kindMap.get(this.content().querySelector('.kind').value), this.content().querySelector('.value').value);
+    }
+
+    static cssTemplate()
+    {
+        return `
+            .mutable-list,
+            .mutable-list li {
+                list-style: none;
+                padding: 0;
+                margin: 0;
+            }
+            
+            .mutable-list:not(:empty) {
+                margin-bottom: 1rem;
+            }
+
+            .mutable-list {
+                margin-bottom: 1rem;
+            }
+
+            .new-list-item-form {
+                white-space: nowrap;
+            }
+        `;
+    }
+
+    static htmlTemplate()
+    {
+        return `
+            <ul class="mutable-list"></ul>
+            <form class="new-list-item-form">
+                <select class="kind"></select>
+                <input class="value">
+                <button type="submit">Add</button>
+            </form>`;
+    }
+
+}
+
+class MutableListItem {
+    constructor(kind, value, valueTitle, valueLink, removalTitle, removalLink)
+    {
+        this._kind = kind;
+        this._value = value;
+        this._valueTitle = valueTitle;
+        this._valueLink = valueLink;
+        this._removalTitle = removalTitle;
+        this._removalLink = removalLink;
+    }
+
+    content()
+    {
+        var link = ComponentBase.createLink;
+        return ComponentBase.createElement('li', [
+            this._kind.label(),
+            ' ',
+            link(this._value, this._valueTitle, this._valueLink),
+            ' ',
+            link(new CloseButton, this._removalTitle, this._removalLink)]);
+    }
+}
+
+ComponentBase.defineElement('mutable-list-view', MutableListView);
index 6f919d5..f64287f 100644 (file)
@@ -79,6 +79,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/customizable-test-group-form.js"></script>
         <script src="components/chart-styles.js"></script>
         <script src="components/chart-pane-base.js"></script>
+        <script src="components/mutable-list-view.js"></script>
         <script src="pages/page.js"></script>
         <script src="pages/page-router.js"></script>
         <script src="pages/heading.js"></script>
index 22408bc..c29049e 100644 (file)
@@ -20,6 +20,8 @@ class AnalysisTask extends LabeledObject {
         this._changeType = object.result; // Can't change due to v2 compatibility.
         this._needed = object.needed;
         this._bugs = object.bugs || [];
+        this._causes = object.causes || [];
+        this._fixes = object.fixes || [];
         this._buildRequestCount = object.buildRequestCount;
         this._finishedBuildRequestCount = object.finishedBuildRequestCount;
     }
@@ -46,6 +48,8 @@ class AnalysisTask extends LabeledObject {
         this._changeType = object.result; // Can't change due to v2 compatibility.
         this._needed = object.needed;
         this._bugs = object.bugs || [];
+        this._causes = object.causes || [];
+        this._fixes = object.fixes || [];
         this._buildRequestCount = object.buildRequestCount;
         this._finishedBuildRequestCount = object.finishedBuildRequestCount;
     }
@@ -62,6 +66,8 @@ class AnalysisTask extends LabeledObject {
     author() { return this._author || ''; }
     createdAt() { return this._createdAt; }
     bugs() { return this._bugs; }
+    causes() { return this._causes; }
+    fixes() { return this._fixes; }
     platform() { return this._platform; }
     metric() { return this._metric; }
     category() { return this._category; }
@@ -110,6 +116,35 @@ class AnalysisTask extends LabeledObject {
         });
     }
 
+    associateCommit(kind, repository, revision)
+    {
+        console.assert(kind == 'cause' || kind == 'fix');
+        console.assert(repository instanceof Repository);
+        var id = this.id();
+        return PrivilegedAPI.sendRequest('associate-commit', {
+            task: id,
+            repository: repository.id(),
+            revision: revision,
+            kind: kind,
+        }).then(function (data) {
+            return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: id}, true)
+                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
+        });
+    }
+
+    dissociateCommit(commit)
+    {
+        console.assert(commit instanceof CommitLog);
+        var id = this.id();
+        return PrivilegedAPI.sendRequest('associate-commit', {
+            task: id,
+            commit: commit.remoteId(),
+        }).then(function (data) {
+            return AnalysisTask.cachedFetch('../api/analysis-tasks', {id: id}, true)
+                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
+        });
+    }
+
     static categories()
     {
         return [
@@ -193,6 +228,17 @@ class AnalysisTask extends LabeledObject {
             taskToBug[rawData.task].push(bug);
         }
 
+        for (var rawData of data.commits) {
+            rawData.repository = Repository.findById(rawData.repository);
+            if (!rawData.repository)
+                continue;
+            CommitLog.ensureSingleton(rawData.repository, rawData);
+        }
+
+        function resolveCommits(commits) {
+            return commits.map(function (id) { return CommitLog.findByRemoteId(id); }).filter(function (commit) { return !!commit; });
+        }
+
         var results = [];
         for (var rawData of data.analysisTasks) {
             rawData.platform = Platform.findById(rawData.platform);
@@ -201,6 +247,8 @@ class AnalysisTask extends LabeledObject {
                 continue;
 
             rawData.bugs = taskToBug[rawData.id];
+            rawData.causes = resolveCommits(rawData.causes);
+            rawData.fixes = resolveCommits(rawData.fixes);
             results.push(AnalysisTask.ensureSingleton(rawData.id, rawData));
         }
 
index 6f5692b..9f58c06 100644 (file)
@@ -30,7 +30,7 @@ class BuildRequest extends DataModelObject {
     order() { return this._order; }
     rootSet() { return this._rootSet; }
 
-    hasCompleted() { return this._status == 'failed' || this._status == 'completed' || this._status == 'canceled'; }
+    hasFinished() { return this._status == 'failed' || this._status == 'completed' || this._status == 'canceled'; }
     hasStarted() { return this._status != 'pending'; }
     hasPending() { return this._status == 'pending'; }
     statusLabel()
index b4d16ed..33a22ab 100644 (file)
@@ -5,6 +5,17 @@ class CommitLog extends DataModelObject {
         super(id);
         this._repository = rawData.repository;
         this._rawData = rawData;
+        this._remoteId = rawData.id;
+        if (this._remoteId)
+            this.ensureNamedStaticMap('remoteId')[this._remoteId] = this;
+    }
+
+    // FIXME: All this non-sense should go away once measurement-set start returning real commit id.
+    remoteId() { return this._remoteId; }
+    static findByRemoteId(id)
+    {
+        var remoteIdMap = super.namedStaticMap('remoteId');
+        return remoteIdMap ? remoteIdMap[id] : null;
     }
 
     static ensureSingleton(repository, rawData)
index f32143a..8c94c4e 100644 (file)
@@ -99,9 +99,9 @@ class TestGroup extends LabeledObject {
         this._allRootSets = null;
     }
 
-    hasCompleted()
+    hasFinished()
     {
-        return this._buildRequests.every(function (request) { return request.hasCompleted(); });
+        return this._buildRequests.every(function (request) { return request.hasFinished(); });
     }
 
     hasStarted()
@@ -126,7 +126,7 @@ class TestGroup extends LabeledObject {
 
         var result = {changeType: null, status: 'failed', label: 'Failed', fullLabel: 'Failed', isStatisticallySignificant: false};
 
-        var hasCompleted = this.hasCompleted();
+        var hasCompleted = this.hasFinished();
         if (!hasCompleted) {
             if (this.hasStarted()) {
                 result.status = 'running';
index 864e0e7..9fa5339 100644 (file)
@@ -61,9 +61,15 @@ class AnalysisTaskPage extends PageWithHeading {
         this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
         this._taskStatusControl = this.content().querySelector('.change-type-form select');
 
-        this.content().querySelector('.associate-bug-form').onsubmit = this._associateBug.bind(this);
-        this._bugTrackerControl = this.content().querySelector('.bug-tracker-control');
-        this._bugNumberControl = this.content().querySelector('.bug-number-control');
+        this._bugList = this.content().querySelector('.associated-bugs mutable-list-view').component();
+        this._bugList.setKindList(BugTracker.all());
+        this._bugList.setAddCallback(this._associateBug.bind(this));
+
+        this._causeList = this.content().querySelector('.cause-list mutable-list-view').component();
+        this._causeList.setAddCallback(this._associateCommit.bind(this, 'cause'));
+
+        this._fixList = this.content().querySelector('.fix-list mutable-list-view').component();
+        this._fixList.setAddCallback(this._associateCommit.bind(this, 'fix'));
 
         this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
         this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
@@ -229,24 +235,32 @@ class AnalysisTaskPage extends PageWithHeading {
             this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
             anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
 
-            var bugs = [];
-            for (var bug of this._task.bugs()) {
-                bugs.push(element('li', [
-                    bug.bugTracker().label() + ' ',
-                    link(bug.label(), bug.title(), bug.url()),
-                    ' ',
-                    link(new CloseButton, 'Disassociate this bug', this._disassociateBug.bind(this, bug))]));
-            }
-            this.renderReplace(this.content().querySelector('.associated-bugs'), bugs);
+            var self = this;
+            this._bugList.setList(this._task.bugs().map(function (bug) {
+                return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
+                    'Disassociate this bug', self._disassociateBug.bind(self, bug));
+            }));
+
+            this._causeList.setList(this._task.causes().map(this._makeCommitListItem.bind(this)));
+            this._fixList.setList(this._task.fixes().map(this._makeCommitListItem.bind(this)));
 
             this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
         }
 
-        var element = ComponentBase.createElement;
-        this.renderReplace(this._bugTrackerControl,
-            BugTracker.all().map(function (tracker) {
-                return element('option', {value: tracker.id()}, tracker.label());
-            }));
+        var repositoryList;
+        if (this._startPoint) {
+            var rootSet = this._startPoint.rootSet();
+            repositoryList = Repository.sortByNamePreferringOnesWithURL(rootSet.repositories());
+        } else
+            repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
+
+        this._bugList.render();
+
+        this._causeList.setKindList(repositoryList);
+        this._causeList.render();
+
+        this._fixList.setKindList(repositoryList);
+        this._fixList.render();
 
         this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
@@ -290,6 +304,12 @@ class AnalysisTaskPage extends PageWithHeading {
         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
     }
 
+    _makeCommitListItem(commit)
+    {
+        return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
+            'Disassociate this commit', this._dissociateCommit.bind(this, commit));
+    }
+
     _renderTestGroupList()
     {
         var element = ComponentBase.createElement;
@@ -446,14 +466,10 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _associateBug(event)
+    _associateBug(tracker, bugNumber)
     {
-        event.preventDefault();
-        console.assert(this._task);
-
-        var tracker = BugTracker.findById(this._bugTrackerControl.value);
-        console.assert(tracker);
-        var bugNumber = parseInt(this._bugNumberControl.value);
+        console.assert(tracker instanceof BugTracker);
+        bugNumber = parseInt(bugNumber);
 
         var render = this.render.bind(this);
         return this._task.associateBug(tracker, bugNumber).then(render, function (error) {
@@ -471,6 +487,24 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
+    _associateCommit(kind, repository, revision)
+    {
+        var render = this.render.bind(this);
+        return this._task.associateCommit(kind, repository, revision).then(render, function (error) {
+            render();
+            alert('Failed to associate the commit: ' + error);
+        });
+    }
+
+    _dissociateCommit(commit)
+    {
+        var render = this.render.bind(this);
+        return this._task.dissociateCommit(commit).then(render, function (error) {
+            render();
+            alert('Failed to disassociate the commit: ' + error);
+        });
+    }
+
     _retryCurrentTestGroup(repetitionCount)
     {
         console.assert(this._currentTestGroup);
@@ -595,14 +629,15 @@ class AnalysisTaskPage extends PageWithHeading {
                             <button type="submit">Save</button>
                         </form>
                     </section>
-                    <section>
+                    <section class="associated-bugs">
                         <h3>Associated Bugs</h3>
-                        <ul class="associated-bugs"></ul>
-                        <form class="associate-bug-form">
-                            <select class="bug-tracker-control"></select>
-                            <input type="number" class="bug-number-control">
-                            <button type="submit">Add</button>
-                        </form>
+                        <mutable-list-view></mutable-list-view>
+                    </section>
+                    <section class="cause-fix">
+                        <h3>Caused by</h3>
+                        <span class="cause-list"><mutable-list-view></mutable-list-view></span>
+                        <h3>Fixed by</h3>
+                        <span class="fix-list"><mutable-list-view></mutable-list-view></span>
                     </section>
                     <section class="related-tasks">
                         <h3>Related Tasks</h3>
@@ -680,16 +715,18 @@ class AnalysisTaskPage extends PageWithHeading {
 
             .analysis-task-status > section {
                 flex-grow: 1;
+                flex-shrink: 0;
                 border-left: solid 1px #eee;
                 padding-left: 1rem;
+                padding-right: 1rem;
             }
 
-            .analysis-task-status > section:first-child {
-                border-left: none;
+            .analysis-task-status > section.related-tasks {
+                flex-shrink: 1;
             }
 
-            .associated-bugs:not(:empty) {
-                margin-bottom: 1rem;
+            .analysis-task-status > section:first-child {
+                border-left: none;
             }
 
             .analysis-task-status h3 {
index 1925aa7..59f01dd 100644 (file)
@@ -19,6 +19,7 @@ function getJSON(path, data)
                 var parsed = JSON.parse(xhr.responseText);
                 resolve(parsed);
             } catch (error) {
+                console.error(xhr.responseText);
                 reject(xhr.status + ', ' + error);
             }
         };