Add the ability to report a commit with sub-commits.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Mar 2017 09:10:29 +0000 (09:10 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Mar 2017 09:10:29 +0000 (09:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=168962

Reviewed by Ryosuke Niwa.

Introduce 'commit_ownerships' which records ownership between commits.
On existing production server, run ```
    CREATE TABLE commit_ownerships (
        commit_owner integer NOT NULL REFERENCES commits ON DELETE CASCADE,
        commit_ownee integer NOT NULL REFERENCES commits ON DELETE CASCADE,
        PRIMARY KEY (commit_owner, commit_ownee)
    );
    ALTER TABLE repositories RENAME repository_parent TO repository_owner;
    ALTER TABLE repositories DROP repository_name_must_be_unique;
    CREATE UNIQUE INDEX repository_name_owner_unique_index ON repositories (repository_owner, repository_name) WHERE repository_owner IS NOT NULL;
    CREATE UNIQUE INDEX repository_name_unique_index ON repositories (repository_name) WHERE repository_owner IS NULL;
``` to update database.
Add unit-tests to cover this change.

* init-database.sql:
* public/api/report-commits.php:
* public/include/commit-log-fetcher.php:
* public/include/db.php:
* public/include/manifest-generator.php:
* public/include/report-processor.php:
* public/v3/models/repository.js:
(Repository):
(Repository.prototype.owner):
* server-tests/admin-reprocess-report-tests.js:
(addBuilderForReport.simpleReportWithRevisions.0.then):
(then):
* server-tests/api-manifest.js:
(then):
* server-tests/api-report-commits-tests.js:
(addSlaveForReport.sameRepositoryNameInSubCommitAndMajorCommit.then):
(then):
(addSlaveForReport.systemVersionCommitWithSubcommits.then):
(addSlaveForReport.multipleSystemVersionCommitsWithSubcommits.then):
(addSlaveForReport.systemVersionCommitWithEmptySubcommits.then):
(addSlaveForReport.systemVersionCommitAndSubcommitWithTimestamp.then):
* tools/js/database.js:

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

12 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/api/report-commits.php
Websites/perf.webkit.org/public/include/commit-log-fetcher.php
Websites/perf.webkit.org/public/include/db.php
Websites/perf.webkit.org/public/include/manifest-generator.php
Websites/perf.webkit.org/public/include/report-processor.php
Websites/perf.webkit.org/public/v3/models/repository.js
Websites/perf.webkit.org/server-tests/admin-reprocess-report-tests.js
Websites/perf.webkit.org/server-tests/api-manifest.js
Websites/perf.webkit.org/server-tests/api-report-commits-tests.js
Websites/perf.webkit.org/tools/js/database.js

index 59568cf..9ed84bf 100644 (file)
@@ -1,3 +1,47 @@
+2017-03-13  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Add the ability to report a commit with sub-commits.
+        https://bugs.webkit.org/show_bug.cgi?id=168962
+
+        Reviewed by Ryosuke Niwa.
+
+        Introduce 'commit_ownerships' which records ownership between commits.
+        On existing production server, run ```
+            CREATE TABLE commit_ownerships (
+                commit_owner integer NOT NULL REFERENCES commits ON DELETE CASCADE,
+                commit_ownee integer NOT NULL REFERENCES commits ON DELETE CASCADE,
+                PRIMARY KEY (commit_owner, commit_ownee)
+            );
+            ALTER TABLE repositories RENAME repository_parent TO repository_owner;
+            ALTER TABLE repositories DROP repository_name_must_be_unique;
+            CREATE UNIQUE INDEX repository_name_owner_unique_index ON repositories (repository_owner, repository_name) WHERE repository_owner IS NOT NULL;
+            CREATE UNIQUE INDEX repository_name_unique_index ON repositories (repository_name) WHERE repository_owner IS NULL;
+        ``` to update database.
+        Add unit-tests to cover this change.
+
+        * init-database.sql:
+        * public/api/report-commits.php:
+        * public/include/commit-log-fetcher.php:
+        * public/include/db.php:
+        * public/include/manifest-generator.php:
+        * public/include/report-processor.php:
+        * public/v3/models/repository.js:
+        (Repository):
+        (Repository.prototype.owner):
+        * server-tests/admin-reprocess-report-tests.js:
+        (addBuilderForReport.simpleReportWithRevisions.0.then):
+        (then):
+        * server-tests/api-manifest.js:
+        (then):
+        * server-tests/api-report-commits-tests.js:
+        (addSlaveForReport.sameRepositoryNameInSubCommitAndMajorCommit.then):
+        (then):
+        (addSlaveForReport.systemVersionCommitWithSubcommits.then):
+        (addSlaveForReport.multipleSystemVersionCommitsWithSubcommits.then):
+        (addSlaveForReport.systemVersionCommitWithEmptySubcommits.then):
+        (addSlaveForReport.systemVersionCommitAndSubcommitWithTimestamp.then):
+        * tools/js/database.js:
+
 2017-03-07  Ryosuke Niwa  <rniwa@webkit.org>
 
         Update ReadMe.md to use directory format for backing up & restoring the database
index 5a3dfcf..6bcf7e0 100644 (file)
@@ -7,6 +7,7 @@ DROP TABLE IF EXISTS builds CASCADE;
 DROP TABLE IF EXISTS committers CASCADE;
 DROP TABLE IF EXISTS commits CASCADE;
 DROP TABLE IF EXISTS build_commits CASCADE;
+DROP TABLE IF EXISTS commit_ownerships CASCADE;
 DROP TABLE IF EXISTS build_slaves CASCADE;
 DROP TABLE IF EXISTS builders CASCADE;
 DROP TABLE IF EXISTS repositories CASCADE;
@@ -38,11 +39,15 @@ CREATE TABLE platforms (
 
 CREATE TABLE repositories (
     repository_id serial PRIMARY KEY,
-    repository_parent integer REFERENCES repositories ON DELETE CASCADE,
+    repository_owner integer REFERENCES repositories ON DELETE CASCADE,
     repository_name varchar(64) NOT NULL,
     repository_url varchar(1024),
-    repository_blame_url varchar(1024),
-    CONSTRAINT repository_name_must_be_unique UNIQUE(repository_parent, repository_name));
+    repository_blame_url varchar(1024));
+
+CREATE UNIQUE INDEX repository_name_owner_unique_index ON repositories (repository_owner, repository_name)
+    WHERE repository_owner IS NOT NULL;
+CREATE UNIQUE INDEX repository_name_unique_index ON repositories (repository_name)
+    WHERE repository_owner IS NULL;
 
 CREATE TABLE bug_trackers (
     tracker_id serial PRIMARY KEY,
@@ -98,6 +103,12 @@ CREATE TABLE commits (
 CREATE INDEX commit_time_index ON commits(commit_time);
 CREATE INDEX commit_order_index ON commits(commit_order);
 
+CREATE TABLE commit_ownerships (
+    commit_owner integer NOT NULL REFERENCES commits ON DELETE CASCADE,
+    commit_owned integer NOT NULL REFERENCES commits ON DELETE CASCADE,
+    PRIMARY KEY (commit_owner, commit_owned)
+);
+
 CREATE TABLE build_commits (
     commit_build integer NOT NULL REFERENCES builds ON DELETE CASCADE,
     build_commit integer NOT NULL REFERENCES commits ON DELETE CASCADE,
index fb2cab4..51a94c2 100644 (file)
@@ -2,7 +2,8 @@
 
 require('../include/json-header.php');
 
-function main($post_data) {
+function main($post_data)
+{
     $db = new Database;
     if (!$db->connect())
         exit_with_error('DatabaseConnectionFailure');
@@ -25,56 +26,79 @@ function main($post_data) {
 
     $db->begin_transaction();
     foreach ($commits as $commit_info) {
-        $repository_id = $db->select_or_insert_row('repositories', 'repository', array('name' => $commit_info['repository']));
+        $repository_id = $db->select_or_insert_repository_row($commit_info['repository'], NULL);
         if (!$repository_id) {
             $db->rollback_transaction();
             exit_with_error('FailedToInsertRepository', array('commit' => $commit_info));
         }
+        $owner_commit_id = insert_commit($db, $commit_info, $repository_id, NULL);
+        if (!array_key_exists('subCommits', $commit_info))
+            continue;
 
-        $author = array_get($commit_info, 'author');
-        $committer_id = NULL;
-        if ($author) {
-            $account = array_get($author, 'account');
-            $committer_query = array('repository' => $repository_id, 'account' => $account);
-            $committer_data = $committer_query;
-            $name = array_get($author, 'name');
-            if ($name)
-                $committer_data['name'] = $name;
-            $committer_id = $db->update_or_insert_row('committers', 'committer', $committer_query, $committer_data);
-            if (!$committer_id) {
+        foreach($commit_info['subCommits'] as $sub_commit_repository_name => $sub_commit_info) {
+            if (array_key_exists('time', $sub_commit_info)) {
                 $db->rollback_transaction();
-                exit_with_error('FailedToInsertCommitter', array('committer' => $committer_data));
+                exit_with_error('SubCommitShouldNotContainTimestamp', array('commit' => $sub_commit_info));
             }
-        }
-
-        $previous_commit_revision = array_get($commit_info, 'previousCommit');
-        $previous_commit_id = NULL;
-        if ($previous_commit_revision) {
-            $previous_commit = $db->select_first_row('commits', 'commit', array('repository' => $repository_id, 'revision' => $previous_commit_revision));
-            if (!$previous_commit) {
+            $sub_commit_repository_id = $db->select_or_insert_repository_row($sub_commit_repository_name, $repository_id);
+            if (!$sub_commit_repository_id) {
                 $db->rollback_transaction();
-                exit_with_error('FailedToFindPreviousCommit', array('commit' => $commit_info));
+                exit_with_error('FailedToInsertRepository', array('commit' => $sub_commit_info));
             }
-            $previous_commit_id = $previous_commit['commit_id'];
+            insert_commit($db, $sub_commit_info, $sub_commit_repository_id, $owner_commit_id);
         }
-
-        $data = array(
-            'repository' => $repository_id,
-            'revision' => $commit_info['revision'],
-            'previous_commit' => $previous_commit_id,
-            'order' => array_get($commit_info, 'order'),
-            'time' => array_get($commit_info, 'time'),
-            'committer' => $committer_id,
-            'message' => array_get($commit_info, 'message'),
-            'reported' => true,
-        );
-        $db->update_or_insert_row('commits', 'commit', array('repository' => $repository_id, 'revision' => $data['revision']), $data);
     }
     $db->commit_transaction();
 
     exit_with_success();
 }
 
+function insert_commit($db, $commit_info, $repository_id, $owner_commit_id)
+{
+    $author = array_get($commit_info, 'author');
+    $committer_id = NULL;
+    if ($author) {
+        $account = array_get($author, 'account');
+        $committer_query = array('repository' => $repository_id, 'account' => $account);
+        $committer_data = $committer_query;
+        $name = array_get($author, 'name');
+        if ($name)
+            $committer_data['name'] = $name;
+        $committer_id = $db->update_or_insert_row('committers', 'committer', $committer_query, $committer_data);
+        if (!$committer_id) {
+            $db->rollback_transaction();
+            exit_with_error('FailedToInsertCommitter', array('committer' => $committer_data));
+        }
+    }
+
+    $previous_commit_revision = array_get($commit_info, 'previousCommit');
+    $previous_commit_id = NULL;
+    if ($previous_commit_revision) {
+        $previous_commit = $db->select_first_row('commits', 'commit', array('repository' => $repository_id, 'revision' => $previous_commit_revision));
+        if (!$previous_commit) {
+            $db->rollback_transaction();
+            exit_with_error('FailedToFindPreviousCommit', array('commit' => $commit_info));
+        }
+        $previous_commit_id = $previous_commit['commit_id'];
+    }
+
+    $data = array(
+        'repository' => $repository_id,
+        'revision' => $commit_info['revision'],
+        'previous_commit' => $previous_commit_id,
+        'order' => array_get($commit_info, 'order'),
+        'time' => array_get($commit_info, 'time'),
+        'committer' => $committer_id,
+        'message' => array_get($commit_info, 'message'),
+        'reported' => true,
+    );
+    $inserted_commit_id = $db->update_or_insert_row('commits', 'commit', array('repository' => $repository_id, 'revision' => $data['revision']), $data);
+
+    if ($owner_commit_id)
+        $db->select_or_insert_row('commit_ownerships', 'commit', array('owner' => $owner_commit_id, 'owned' => $inserted_commit_id), NULL, '*');
+    return $inserted_commit_id;
+}
+
 main($HTTP_RAW_POST_DATA);
 
 ?>
index 7472819..0d4205a 100644 (file)
@@ -27,10 +27,10 @@ class CommitLogFetcher {
 
     function repository_id_from_name($name)
     {
-        $repository_row = $this->db->select_first_row('repositories', 'repository', array('name' => $name));
+        $repository_row = $this->db->query_and_fetch_all('SELECT repository_id FROM repositories WHERE repository_name = $1 AND repository_owner is NULL', array($name));
         if (!$repository_row)
             return NULL;
-        return $repository_row['repository_id'];
+        return $repository_row[0]['repository_id'];
     }
 
     function fetch_between($repository_id, $first, $second, $keyword = NULL) {
index d805d67..2918db3 100644 (file)
@@ -188,6 +188,26 @@ class Database
         return $rows ? ($returning == '*' ? $rows[0] : $rows[0][$returning_column_name]) : NULL;
     }
 
+    // FIXME: Should improve _select_update_or_insert_row to handle the NULL column case.
+    function select_or_insert_repository_row($repository_name, $repository_owner_id)
+    {
+        $result = NULL;
+        if ($repository_owner_id == NULL) {
+            $result = $this->query_and_fetch_all('INSERT INTO repositories (repository_name) SELECT $1
+                WHERE NOT EXISTS (SELECT repository_id FROM repositories WHERE repository_name = $2 AND repository_owner IS NULL) RETURNING repository_id',
+                array($repository_name, $repository_name));
+            if (!$result)
+                $result = $this->query_and_fetch_all('SELECT repository_id FROM repositories WHERE repository_name = $1 AND repository_owner IS NULL', array($repository_name));
+        } else {
+            $result = $this->query_and_fetch_all('INSERT INTO repositories (repository_name, repository_owner) SELECT $1, $2
+                WHERE NOT EXISTS (SELECT repository_id FROM repositories WHERE (repository_name, repository_owner) = ($3, $4)) RETURNING repository_id',
+                array($repository_name, $repository_owner_id, $repository_name, $repository_owner_id));
+            if (!$result)
+                $result = $this->query_and_fetch_all('SELECT repository_id FROM repositories WHERE (repository_name, repository_owner) = ($1, $2)', array($repository_name, $repository_owner_id));
+        }
+        return $result ? $result[0]['repository_id'] : NULL;
+    }
+
     function select_first_row($table, $prefix, $params, $order_by = NULL) {
         return $this->select_first_or_last_row($table, $prefix, $params, $order_by, FALSE);
     }
index e8940fd..9ec772c 100644 (file)
@@ -138,6 +138,7 @@ class ManifestGenerator {
                 'name' => $row['repository_name'],
                 'url' => $row['repository_url'],
                 'blameUrl' => $row['repository_blame_url'],
+                'owner'=> $row['repository_owner'],
                 'hasReportedCommits' => in_array($row['repository_id'], $repositories_with_commit));
         }
 
index b58ca81..4908980 100644 (file)
@@ -150,7 +150,7 @@ class ReportProcessor {
 
 
         foreach ($revisions as $repository_name => $revision_data) {
-            $repository_id = $this->db->select_or_insert_row('repositories', 'repository', array('name' => $repository_name));
+            $repository_id = $this->db->select_or_insert_repository_row($repository_name, NULL);
             if (!$repository_id)
                 $this->exit_with_error('FailedToInsertRepository', array('name' => $repository_name));
 
@@ -309,7 +309,7 @@ class TestRunsGenerator {
                 foreach ($aggregators_and_values as $aggregator_and_values) {
                     if ($aggregator_and_values['aggregator'] == $aggregator) {
                         $values = $aggregator_and_values['values'];
-                        break;                        
+                        break;
                     }
                 }
                 if (!$values) {
@@ -404,7 +404,7 @@ class TestRunsGenerator {
                         }
                         $iteration_value = $iteration_value[1];
                     }
-                    array_push($flattened_value, $iteration_value);                    
+                    array_push($flattened_value, $iteration_value);
                 }
             }
             $this->values_to_commit[$i]['mean'] = $this->aggregate_values('Arithmetic', $flattened_value);
index 5192fd0..8c2acae 100644 (file)
@@ -7,6 +7,7 @@ class Repository extends LabeledObject {
         this._url = object.url;
         this._blameUrl = object.blameUrl;
         this._hasReportedCommits = object.hasReportedCommits;
+        this._owner = object.owner;
     }
 
     hasUrlForRevision() { return !!this._url; }
@@ -21,6 +22,11 @@ class Repository extends LabeledObject {
         return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
     }
 
+    owner()
+    {
+        return this._owner;
+    }
+
     static sortByNamePreferringOnesWithURL(repositories)
     {
         return repositories.sort(function (a, b) {
index def4dcf..47ce1b0 100644 (file)
@@ -24,6 +24,44 @@ describe("/admin/reprocess-report", function () {
             },
         }];
 
+    const simpleReportWithRevisions = [{
+        "buildNumber": "1986",
+        "buildTime": "2013-02-28T10:12:03",
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+                "test": {
+                    "metrics": {"FrameRate": { "current": [[1, 2, 3], [4, 5, 6]] }}
+                },
+            },
+        "revisions": {
+                "WebKit": {
+                    "timestamp": "2017-03-01T09:38:44.826833Z",
+                    "revision": "213214"
+                }
+            }
+        }];
+
+    it("should still create new repository when repository ownerships are different", function (done) {
+        let db = TestServer.database();
+        addBuilderForReport(simpleReportWithRevisions[0]).then(function () {
+            return db.insert('repositories', {'name': 'WebKit', 'owner': 1});
+        }).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', simpleReportWithRevisions);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectRows('repositories', {'name': 'WebKit'});
+        }).then(function (repositories) {
+            assert.equal(repositories.length, 2);
+            const webkitRepsitoryId = repositories[0].owner == 1 ? repositories[1].id : repositories[0].id;
+            return db.selectRows('commits', {'revision': '213214', 'repository': webkitRepsitoryId});
+        }).then(function (result) {
+            assert(result.length, 1);
+            done();
+        }).catch(done);
+    });
+
     it("should add build", function (done) {
         let db = TestServer.database();
         let reportId;
index f5a50be..7bd3ecc 100644 (file)
@@ -282,6 +282,7 @@ describe('/api/manifest', function () {
         Promise.all([
             db.insert('repositories', {id: 11, name: 'WebKit', url: 'https://trac.webkit.org/$1'}),
             db.insert('repositories', {id: 9, name: 'OS X'}),
+            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('tests', {id: 1, name: 'SomeTest'}),
@@ -311,6 +312,11 @@ describe('/api/manifest', function () {
             assert.equal(webkit.name(), 'WebKit');
             assert.equal(webkit.urlForRevision(123), 'https://trac.webkit.org/123');
 
+            let 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);
             assert.equal(osx.name(), 'OS X');
 
index 93d1ad9..cfd39a2 100644 (file)
@@ -287,4 +287,334 @@ describe("/api/report-commits/", function () {
         }).catch(done);
     });
 
+    const sameRepositoryNameInSubCommitAndMajorCommit = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "OSX",
+                "revision": "Sierra16D32",
+                "order": 1,
+                "subCommits": {
+                    "WebKit": {
+                        "revision": "141978",
+                        "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                        "message": "WebKit Commit",
+                    },
+                    "JavaScriptCore": {
+                        "revision": "141978",
+                        "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
+                        "message": "JavaScriptCore commit",
+                    }
+                }
+            },
+            {
+                "repository": "WebKit",
+                "revision": "141978",
+                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                "message": "WebKit Commit",
+            }
+        ]
+    }
+
+    it("should distinguish between repositories with the asme name but with a different owner.", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(sameRepositoryNameInSubCommitAndMajorCommit).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', sameRepositoryNameInSubCommitAndMajorCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectRows('repositories', {'name': 'WebKit'});
+        }).then(function (result) {
+            assert.equal(result.length, 2);
+            let osWebKit = result[0];
+            let webkitRepository = result[1];
+            assert.notEqual(osWebKit.id, webkitRepository.id);
+            assert.equal(osWebKit.name, webkitRepository.name);
+            assert.equal(webkitRepository.owner, null);
+            done();
+        })
+    });
+
+    const systemVersionCommitWithSubcommits = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "OSX",
+                "revision": "Sierra16D32",
+                "order": 1,
+                "subCommits": {
+                    "WebKit": {
+                        "revision": "141978",
+                        "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                        "message": "WebKit Commit",
+                    },
+                    "JavaScriptCore": {
+                        "revision": "141978",
+                        "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
+                        "message": "JavaScriptCore commit",
+                    }
+                }
+            }
+        ]
+    }
+
+    it("should accept inserting one commit with some sub commits", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(systemVersionCommitWithSubcommits).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', systemVersionCommitWithSubcommits);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectRows('commits', {'revision': 'Sierra16D32'}),
+                db.selectRows('commits', {'message': 'WebKit Commit'}),
+                db.selectRows('commits', {'message': 'JavaScriptCore commit'}),
+                db.selectRows('repositories', {'name': 'OSX'}),
+                db.selectRows('repositories', {'name': "WebKit"}),
+                db.selectRows('repositories', {'name': 'JavaScriptCore'})])
+        }).then(function (result) {
+            assert.equal(result.length, 6);
+
+            assert.equal(result[0].length, 1);
+            const osxCommit = result[0][0];
+            assert.notEqual(osxCommit, null);
+
+            assert.equal(result[1].length, 1);
+            const webkitCommit = result[1][0];
+            assert.notEqual(webkitCommit, null);
+
+            assert.equal(result[2].length, 1);
+            const jscCommit = result[2][0];
+            assert.notEqual(jscCommit, null);
+
+            assert.equal(result[3].length, 1);
+            const osxRepository = result[3][0];
+            assert.notEqual(osxRepository, null);
+
+            assert.equal(result[4].length, 1);
+            const webkitRepository = result[4][0];
+            assert.notEqual(webkitRepository, null);
+
+            assert.equal(result[5].length, 1);
+            const jscRepository = result[5][0];
+            assert.notEqual(jscRepository, null);
+
+            assert.equal(osxCommit.repository, osxRepository.id);
+            assert.equal(webkitCommit.repository, webkitRepository.id);
+            assert.equal(jscCommit.repository, jscRepository.id);
+            assert.equal(osxRepository.owner, null);
+            assert.equal(webkitRepository.owner, osxRepository.id);
+            assert.equal(jscRepository.owner, osxRepository.id);
+
+            return Promise.all([db.selectRows('commit_ownerships', {'owner': osxCommit.id, 'owned': webkitCommit.id}, {'sortBy': 'owner'}),
+                db.selectRows('commit_ownerships', {'owner': osxCommit.id, 'owned': jscCommit.id}, {'sortBy': 'owner'}),
+                db.selectRows('commits', {'repository': webkitRepository.id})]);
+        }).then(function (result) {
+            assert.equal(result.length, 3);
+
+            assert.equal(result[0].length, 1);
+            const ownerCommitForWebKitCommit = result[0][0];
+            assert.notEqual(ownerCommitForWebKitCommit, null);
+
+            assert.equal(result[1].length, 1);
+            const ownerCommitForJSCCommit =  result[1][0];
+            assert.notEqual(ownerCommitForJSCCommit, null);
+
+            assert.equal(result[2].length, 1);
+            done();
+        }).catch(done);
+    })
+
+    const multipleSystemVersionCommitsWithSubcommits = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "OSX",
+                "revision": "Sierra16D32",
+                "order": 2,
+                "subCommits": {
+                    "WebKit": {
+                        "revision": "141978",
+                        "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                        "message": "WebKit Commit",
+                    },
+                    "JavaScriptCore": {
+                        "revision": "141978",
+                        "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
+                        "message": "JavaScriptCore commit",
+                    }
+                }
+            },
+            {
+                "repository": "OSX",
+                "revision": "Sierra16C67",
+                "order": 1,
+                "subCommits": {
+                    "WebKit": {
+                        "revision": "141978",
+                        "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                        "message": "WebKit Commit",
+                    },
+                    "JavaScriptCore": {
+                        "revision": "141999",
+                        "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
+                        "message": "new JavaScriptCore commit",
+                    }
+                }
+            }
+        ]
+    };
+
+    it("should accept inserting multiple commits with multiple sub-commits", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(multipleSystemVersionCommitsWithSubcommits).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', multipleSystemVersionCommitsWithSubcommits);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectRows('commits', {'revision': 'Sierra16D32'}),
+                db.selectRows('commits', {'revision': 'Sierra16C67'}),
+                db.selectRows('commits', {'message': 'WebKit Commit'}),
+                db.selectRows('commits', {'message': 'JavaScriptCore commit'}),
+                db.selectRows('commits', {'message': 'new JavaScriptCore commit'}),
+                db.selectRows('repositories', {'name': 'OSX'}),
+                db.selectRows('repositories', {'name': "WebKit"}),
+                db.selectRows('repositories', {'name': 'JavaScriptCore'})])
+        }).then(function (result) {
+            assert.equal(result.length, 8);
+
+            assert.equal(result[0].length, 1);
+            const osxCommit0 = result[0][0];
+            assert.notEqual(osxCommit0, null);
+
+            assert.equal(result[1].length, 1);
+            const osxCommit1 = result[1][0];
+            assert.notEqual(osxCommit1, null);
+
+            assert.equal(result[2].length, 1);
+            const webkitCommit = result[2][0];
+            assert.notEqual(webkitCommit, null);
+
+            assert.equal(result[3].length, 1);
+            const jscCommit0 = result[3][0];
+            assert.notEqual(jscCommit0, null);
+
+            assert.equal(result[4].length, 1);
+            const jscCommit1 = result[4][0];
+            assert.notEqual(jscCommit1, null);
+
+            assert.equal(result[5].length, 1)
+            const osxRepository = result[5][0];
+            assert.notEqual(osxRepository, null);
+            assert.equal(osxRepository.owner, null);
+
+            assert.equal(result[6].length, 1)
+            const webkitRepository = result[6][0];
+            assert.equal(webkitRepository.owner, osxRepository.id);
+
+            assert.equal(result[7].length, 1);
+            const jscRepository = result[7][0];
+            assert.equal(jscRepository.owner, osxRepository.id);
+
+            assert.equal(osxCommit0.repository, osxRepository.id);
+            assert.equal(osxCommit1.repository, osxRepository.id);
+            assert.equal(webkitCommit.repository, webkitRepository.id);
+            assert.equal(jscCommit0.repository, jscRepository.id);
+            assert.equal(jscCommit1.repository, jscRepository.id);
+            assert.equal(osxRepository.owner, null);
+            assert.equal(webkitRepository.owner, osxRepository.id);
+            assert.equal(jscRepository.owner, osxRepository.id);
+
+            return Promise.all([db.selectRows('commit_ownerships', {'owner': osxCommit0.id, 'owned': webkitCommit.id}, {'sortBy': 'owner'}),
+                db.selectRows('commit_ownerships', {'owner': osxCommit1.id, 'owned': webkitCommit.id}, {'sortBy': 'owner'}),
+                db.selectRows('commit_ownerships', {'owner': osxCommit0.id, 'owned': jscCommit0.id}, {'sortBy': 'owner'}),
+                db.selectRows('commit_ownerships', {'owner': osxCommit1.id, 'owned': jscCommit1.id}, {'sortBy': 'owner'}),
+                db.selectRows('commits', {'repository': webkitRepository.id})]);
+        }).then(function (result) {
+            assert.equal(result.length, 5);
+
+            assert.equal(result[0].length, 1);
+            const ownerCommitForWebKitCommit0 = result[0][0];
+            assert.notEqual(ownerCommitForWebKitCommit0, null);
+
+            assert.equal(result[1].length, 1);
+            const ownerCommitForWebKitCommit1 = result[1][0];
+            assert.notEqual(ownerCommitForWebKitCommit1, null);
+
+            assert.equal(result[2].length, 1);
+            const ownerCommitForJSCCommit0 = result[2][0];
+            assert.notEqual(ownerCommitForJSCCommit0, null);
+
+            assert.equal(result[3].length, 1);
+            const ownerCommitForJSCCommit1 = result[3][0];
+            assert.notEqual(ownerCommitForJSCCommit1, null);
+
+            assert.equal(result[4].length, 1);
+
+            done();
+        }).catch(done);
+    });
+
+    const systemVersionCommitWithEmptySubcommits = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "OSX",
+                "revision": "Sierra16D32",
+                "order": 1,
+                "subCommits": {
+                }
+            }
+        ]
+    }
+
+    it("should accept inserting one commit with no sub commits", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(systemVersionCommitWithEmptySubcommits).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', systemVersionCommitWithEmptySubcommits);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('commits'), db.selectAll('repositories'), db.selectAll('commit_ownerships', 'owner')]);
+        }).then(function (result) {
+            let commits = result[0];
+            let repositories = result[1];
+            let commit_ownerships = result[2];
+            assert.equal(commits.length, 1);
+            assert.equal(repositories.length, 1);
+            assert.equal(commits[0].repository, repositories[0].id);
+            assert.equal(repositories[0].name, 'OSX');
+            assert.equal(commit_ownerships.length, 0);
+            done();
+        }).catch(done);
+    });
+
+    const systemVersionCommitAndSubcommitWithTimestamp = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "OSX",
+                "revision": "Sierra16D32",
+                "order": 1,
+                "subCommits": {
+                    "WebKit": {
+                        "revision": "141978",
+                        "time": "2013-02-06T08:55:20.9Z",
+                        "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                        "message": "WebKit Commit",
+                    }
+                }
+            }
+        ]
+    }
+
+    it("should reject inserting one commit with sub commits that contains timestamp", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(systemVersionCommitAndSubcommitWithTimestamp).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', systemVersionCommitAndSubcommitWithTimestamp);
+        }).then(function (response) {
+            assert.equal(response['status'], 'SubCommitShouldNotContainTimestamp');
+            done();
+        }).catch(done);
+    });
 });
index 802add6..6dfed38 100644 (file)
@@ -136,6 +136,7 @@ const tableToPrefixMap = {
     'builds': 'build',
     'builders': 'builder',
     'commits': 'commit',
+    'commit_ownerships': 'commit',
     'committers': 'committer',
     'test_configurations': 'config',
     'test_metrics': 'metric',