[cloud functions] Detect commits and reviews that are reverts
Saves a 'revert_of' field in the Firestore documents for commits and reviews
if they are a revert of a landed commit.
Change-Id: I2f66d3f2ceeb4dab9fbee2bcce9523be257194e2
Bug: https://github.com/dart-lang/dart_ci/issues/78
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/136420
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/functions/node/builder.dart b/functions/node/builder.dart
index 51ce560..ba3204b 100644
--- a/functions/node/builder.dart
+++ b/functions/node/builder.dart
@@ -169,12 +169,14 @@
var index = lastIndex + 1;
for (Map<String, dynamic> commit in commits.reversed) {
final review = _review(commit);
+ final reverted = _revert(commit);
await firestore.addCommit(commit['commit'], {
'author': commit['author']['email'],
'created': parseGitilesDateTime(commit['committer']['time']),
'index': index,
'title': commit['message'].split('\n').first,
- if (review != null) 'review': review
+ if (review != null) 'review': review,
+ if (reverted != null) 'revert_of': reverted
});
if (review != null) {
await landReview(commit, index);
@@ -254,6 +256,12 @@
return null;
}
+final revertRegExp =
+ RegExp('^This reverts commit ([\\da-f]+)\\.\$', multiLine: true);
+
+String _revert(Map<String, dynamic> commit) =>
+ revertRegExp.firstMatch(commit['message'])?.group(1);
+
String testResult(Map<String, dynamic> change) => [
change['name'],
change['result'],
diff --git a/functions/node/gerrit_change.dart b/functions/node/gerrit_change.dart
index ec7a77a..263ad4a 100644
--- a/functions/node/gerrit_change.dart
+++ b/functions/node/gerrit_change.dart
@@ -10,6 +10,8 @@
class GerritInfo {
static const gerritUrl = 'https://dart-review.googlesource.com/changes';
+ static const gerritQuery =
+ 'o=ALL_REVISIONS&o=DETAILED_ACCOUNTS&o=CURRENT_COMMIT';
static const trivialKinds = const {
'TRIVIAL_REBASE',
'NO_CHANGE',
@@ -31,14 +33,18 @@
Future<void> update() async {
if (await firestore.hasPatchset(review, patchset)) return;
// Get the Gerrit change's commit from the Gerrit API.
- final url = '$gerritUrl/$review?o=ALL_REVISIONS&o=DETAILED_ACCOUNTS';
+ final url = '$gerritUrl/$review?$gerritQuery';
final response = await httpClient.get(url);
final protectedJson = response.body;
if (!protectedJson.startsWith(prefix))
throw Exception('Gerrit response missing prefix $prefix: $protectedJson');
final reviewInfo = jsonDecode(protectedJson.substring(prefix.length))
as Map<String, dynamic>;
- await firestore.storeReview(review, {'subject': reviewInfo['subject']});
+ final reverted = revert(reviewInfo);
+ await firestore.storeReview(review, {
+ 'subject': reviewInfo['subject'],
+ if (reverted != null) 'revert_of': reverted
+ });
// Add the patchset information to the patchsets subcollection.
final revisions = reviewInfo['revisions'].values.toList()
@@ -57,4 +63,12 @@
});
}
}
+
+ static String revert(Map<String, dynamic> reviewInfo) {
+ final current = reviewInfo['current_revision'];
+ final commit = reviewInfo['revisions'][current]['commit'];
+ final regExp =
+ RegExp('^This reverts commit ([\\da-f]+)\\.\$', multiLine: true);
+ return regExp.firstMatch(commit['message'])?.group(1);
+ }
}
diff --git a/functions/node/test/gerrit_review_json.dart b/functions/node/test/gerrit_review_json.dart
new file mode 100644
index 0000000..2da5c32
--- /dev/null
+++ b/functions/node/test/gerrit_review_json.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This is the JSON log for an actual review that is a revert,
+// with emails and names removed.
+String revertReviewGerritLog = r'''
+{
+ "id": "sdk~master~Ie212fae88bc1977e34e4d791c644b77783a8deb1",
+ "project": "sdk",
+ "branch": "master",
+ "hashtags": [],
+ "change_id": "Ie212fae88bc1977e34e4d791c644b77783a8deb1",
+ "subject": "Revert \"[SDK] Adds IndirectGoto implementation of sync-yield.\"",
+ "status": "MERGED",
+ "created": "2020-02-17 12:17:05.000000000",
+ "updated": "2020-02-17 12:17:25.000000000",
+ "submitted": "2020-02-17 12:17:25.000000000",
+ "submitter": {
+ "_account_id": 5260,
+ "name": "commit-bot@chromium.org",
+ "email": "commit-bot@chromium.org"
+ },
+ "insertions": 61,
+ "deletions": 155,
+ "total_comment_count": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "revert_of": 133586,
+ "submission_id": "136125",
+ "_number": 136125,
+ "owner": {
+ },
+ "current_revision": "82f3f81fc82d06c575b0137ddbe71408826d8b46",
+ "revisions": {
+ "82f3f81fc82d06c575b0137ddbe71408826d8b46": {
+ "kind": "REWORK",
+ "_number": 2,
+ "created": "2020-02-17 12:17:25.000000000",
+ "uploader": {
+ "_account_id": 5260,
+ "name": "commit-bot@chromium.org",
+ "email": "commit-bot@chromium.org"
+ },
+ "ref": "refs/changes/25/136125/2",
+ "fetch": {
+ "rpc": {
+ "url": "rpc://dart/sdk",
+ "ref": "refs/changes/25/136125/2"
+ },
+ "http": {
+ "url": "https://dart.googlesource.com/sdk",
+ "ref": "refs/changes/25/136125/2"
+ },
+ "sso": {
+ "url": "sso://dart/sdk",
+ "ref": "refs/changes/25/136125/2"
+ }
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "d2d00ff357bd64a002697b3c96c92a0fec83328c",
+ "subject": "[cfe] Allow unassigned late local variables"
+ }
+ ],
+ "author": {
+ "name": "gerrit_user",
+ "email": "gerrit_user@example.com",
+ "date": "2020-02-17 12:17:25.000000000",
+ "tz": 0
+ },
+ "committer": {
+ "name": "commit-bot@chromium.org",
+ "email": "commit-bot@chromium.org",
+ "date": "2020-02-17 12:17:25.000000000",
+ "tz": 0
+ },
+ "subject": "Revert \"[SDK] Adds IndirectGoto implementation of sync-yield.\"",
+ "message": "Revert \"[SDK] Adds IndirectGoto implementation of sync-yield.\"\n\nThis reverts commit 7ed1690b4ed6b56bc818173dff41a7a2530991a2.\n\nReason for revert: Crashes precomp.\n\nOriginal change\u0027s description:\n\u003e [SDK] Adds IndirectGoto implementation of sync-yield.\n\u003e \n\u003e Sets a threshold of five continuations determining if the old\n\u003e if-else or the new igoto-based implementation will be used.\n\u003e Informal benchmarking on x64 and arm_x64 point towards the overhead\n\u003e of the igoto-based impl. dropping off around this point.\n\u003e \n\u003e Benchmarks of this CL (threshold\u003d5) show drastic improvement in\n\u003e Calls.IterableManualIterablePolymorphicManyYields of about ~35-65%\n\u003e across {dart,dart-aot}-{ia32,x64,armv7hf,armv8}.\n\u003e \n\u003e Bug: https://github.com/dart-lang/sdk/issues/37754\n\u003e Change-Id: I6e113f1f98e9ab0f994cf93004227d616e9e4d07\n\u003e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/133586\n\u003e Commit-Queue: XXXXX \u003cxxxx@example.com\u003e\n\u003e Reviewed-by: xxxx \u003cxxxxxx@example.com\u003e\n\nChange-Id: Ie212fae88bc1977e34e4d791c644b77783a8deb1\nNo-Presubmit: true\nNo-Tree-Checks: true\nNo-Try: true\nBug: https://github.com/dart-lang/sdk/issues/37754\nReviewed-on: https://dart-review.googlesource.com/c/sdk/+/136125\nReviewed-by: XXX\nCommit-Queue: XXXXXX\n"
+ },
+ "description": "Rebase"
+ },
+ "8bae95c4001a0815e89ebc4c89dc5ad42337a01b": {
+ "kind": "REWORK",
+ "_number": 1,
+ "created": "2020-02-17 12:17:05.000000000",
+ "uploader": {
+ },
+ "ref": "refs/changes/25/136125/1",
+ "fetch": {
+ "rpc": {
+ "url": "rpc://dart/sdk",
+ "ref": "refs/changes/25/136125/1"
+ },
+ "http": {
+ "url": "https://dart.googlesource.com/sdk",
+ "ref": "refs/changes/25/136125/1"
+ },
+ "sso": {
+ "url": "sso://dart/sdk",
+ "ref": "refs/changes/25/136125/1"
+ }
+ }
+ }
+ },
+ "requirements": []
+}
+''';
diff --git a/functions/node/test/test.dart b/functions/node/test/test.dart
index 375ce23..e9f51e7 100644
--- a/functions/node/test/test.dart
+++ b/functions/node/test/test.dart
@@ -115,6 +115,42 @@
verifyNoMoreInteractions(firestore);
});
+ test('fetch commit that is a revert', () async {
+ final client = HttpClientMock();
+ final firestore = FirestoreServiceMock();
+ when(firestore.getCommit(existingCommitHash))
+ .thenAnswer((_) => Future.value(existingCommit));
+ final revertResponses = [null, revertCommit];
+ when(firestore.getCommit(revertCommitHash))
+ .thenAnswer((_) => Future(() => revertResponses.removeAt(0)));
+ when(firestore.getLastCommit()).thenAnswer(
+ (_) => Future(() => {...existingCommit, 'id': existingCommitHash}));
+
+ when(firestore.tryApprovals(any)).thenAnswer((_) => Future(() => []));
+ when(firestore.reviewIsLanded(any)).thenAnswer((_) => Future.value(true));
+
+ when(client.get(any))
+ .thenAnswer((_) => Future(() => ResponseFake(revertGitilesLog)));
+
+ final builder =
+ Build(revertCommitHash, revertCommitChange, null, firestore, client);
+ await builder.storeBuildCommitsInfo();
+ expect(builder.endIndex, revertCommit['index']);
+ expect(builder.startIndex, existingCommit['index'] + 1);
+ expect(builder.tryApprovals, {});
+ verifyInOrder([
+ verify(firestore.getCommit(revertCommitHash)).called(2),
+ verify(firestore.getLastCommit()),
+ verify(firestore.addCommit(revertCommitHash, revertCommit)),
+ verify(firestore.reviewIsLanded(revertReview)),
+ // We want to verify firestore.getCommit(revertCommitHash) here,
+ // but it is already matched by the earlier check for the same call.
+ verify(firestore.getCommit(existingCommitHash)),
+ verify(firestore.tryApprovals(revertReview))
+ ]);
+ verifyNoMoreInteractions(firestore);
+ });
+
test("copy approvals from try results", () async {
final firestore = FirestoreServiceMock();
final client = HttpClientMock();
diff --git a/functions/node/test/test_data.dart b/functions/node/test/test_data.dart
index 1f88a2a..4286fb1 100644
--- a/functions/node/test/test_data.dart
+++ b/functions/node/test/test_data.dart
@@ -28,11 +28,11 @@
};
const String existingCommitHash = 'an already existing commit hash';
-
+const int existingCommitIndex = 52;
Map<String, dynamic> existingCommit = {
'author': 'test_user@example.com',
'created': DateTime.parse('2019-11-22 22:19:00Z'),
- 'index': 52,
+ 'index': existingCommitIndex,
'title': 'A commit used for testing, with index 52'
};
@@ -45,19 +45,22 @@
};
const String commit53Hash = 'commit 53 landing CL 77779 hash';
+const int commit53Index = existingCommitIndex + 1;
Map<String, dynamic> commit53 = {
'author': 'user@example.com',
'created': DateTime.parse('2019-11-28 12:07:55 +0000'),
- 'index': 53,
+ 'index': commit53Index,
'title': 'A commit on the git log',
'review': 77779
};
const String landedCommitHash = 'a commit landing a CL hash';
+const int landedCommitIndex = commit53Index + 1;
+
Map<String, dynamic> landedCommit = {
'author': 'gerrit_user@example.com',
'created': DateTime.parse('2019-11-29 15:15:00Z'),
- 'index': 54,
+ 'index': landedCommitIndex,
'title': 'A commit used for testing tryjob approvals, with index 54',
'review': 44445
};
@@ -85,6 +88,44 @@
"changed": true
};
+const String revertCommitHash = '23456ababa23456ababa';
+const String revertedHash = '012345abcde012345abcde';
+const int revertReview = 85937;
+const int revertCommitIndex = existingCommitIndex + 1;
+Map<String, dynamic> revertCommit = {
+ 'author': 'gerrit_user@example.com',
+ 'created': DateTime.parse('2019-11-29 15:15:00 +0000'),
+ 'index': revertCommitIndex,
+ 'title': 'A commit that reverts another commit',
+ 'review': revertReview,
+ 'revert_of': revertedHash,
+};
+
+// This change is an unchanged passing result, used as the first result in
+// a chunk with no changed results.
+const Map<String, dynamic> revertCommitChange = {
+ "name": "dart2js_extra/local_function_signatures_strong_test/none",
+ "configuration": "dart2js-new-rti-linux-x64-d8",
+ "suite": "dart2js_extra",
+ "test_name": "local_function_signatures_strong_test/none",
+ "time_ms": 2384,
+ "result": "Pass",
+ "expected": "Pass",
+ "matches": false,
+ "bot_name": "luci-dart-try-xenial-70-8fkh",
+ "commit_hash": revertCommitHash,
+ "commit_time": 1563576771,
+ "build_number": "401",
+ "builder_name": "dart2js-rti-linux-x64-d8",
+ "flaky": false,
+ "previous_flaky": false,
+ "previous_result": "Pass",
+ "previous_commit_hash": existingCommitHash,
+ "previous_commit_time": 1563576211,
+ "previous_build_number": "400",
+ "changed": false
+};
+
const Map<String, dynamic> activeFailureResult = {
"name": "test_suite/active_failing_test",
"configurations": [testConfiguration, 'configuration 2', 'configuration 3'],
@@ -250,3 +291,22 @@
]
}
''';
+
+String revertGitilesLog = '''
+)]}'
+{
+ "log": [
+ {
+ "commit": "$revertCommitHash",
+ "parents": ["$existingCommitHash"],
+ "author": {
+ "email": "gerrit_user@example.com"
+ },
+ "committer": {
+ "time": "Fri Nov 29 15:15:00 2019 +0000"
+ },
+ "message": "A commit that reverts another commit\\n\\nThis reverts commit $revertedHash.\\nChange-Id: I89b88c3d9f7c743fc340ee73a45c3f57059bcf30\\nReviewed-on: https://dart-review.googlesource.com/c/sdk/+/$revertReview\\n\\n"
+ }
+ ]
+}
+''';
diff --git a/functions/node/test/test_gerrit.dart b/functions/node/test/test_gerrit.dart
new file mode 100644
index 0000000..b42d139
--- /dev/null
+++ b/functions/node/test/test_gerrit.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+
+import '../gerrit_change.dart';
+import 'gerrit_review_json.dart';
+
+void main() async {
+ test("get revert information from Gerrit log api output", () {
+ expect(GerritInfo.revert(json.decode(revertReviewGerritLog)),
+ "7ed1690b4ed6b56bc818173dff41a7a2530991a2");
+ });
+}
diff --git a/results_feed/doc/results_feed.proto b/results_feed/doc/results_feed.proto
index a19f535..0aab202 100644
--- a/results_feed/doc/results_feed.proto
+++ b/results_feed/doc/results_feed.proto
@@ -45,8 +45,8 @@
bool approved = 13; // May be missing in older records, interpreted as false.
}
-// Similar to a Result message, but representing a result of a try builder on the CQ.
-// There is no blamelist, instead, a patchset on a Gerrit CL describes the build.
+// Similar to a Result message, but representing results from a try builder.
+// There is no blamelist. Gerrit CL and patchset numbers identify the build.
message TryResult {
// Unique document ID created by Firestore for this document.
string id = 1;
@@ -115,7 +115,8 @@
string author = 3; // The author of the commit, as an email address.
google.protobuf.Timestamp created = 4;
string title = 5; // The first line of the commit message.
- int32 review = 6; // If the commit is a landed Gerrit review, its number.
+ int32 review = 6; // Optional: the id of the review this commit lands.
+ string revert_of = 7; // Optional: the hash of the commit this commit reverts.
}
// A map from configuration names to builder names.
@@ -129,6 +130,7 @@
message Review {
string id = 1; // The Gerrit review number (CL number)
string subject = 2; // The first line of the commit message.
+ string revert_of = 3; // Optional: the hash of the commit this review reverts.
message Patchset {
string id = 1; // The patchset number.
@@ -145,7 +147,7 @@
}
repeated Patchset patchsets = 3;
- int32 landed_index = 4; // When a review lands, this is the index of the landed commit.
+ int32 landed_index = 4; // Optional: The index of the commit landing the CL.
}
message Comment {