Skip to content

Commit

Permalink
Add support for Robot Comments
Browse files Browse the repository at this point in the history
Extend the PostReview REST endpoint to support posting of robot
comments. For this ReviewInput got a new field that can contain a list
of robot comments. For reading robot comments new REST endpoints have
been added.

Robot comments are only supported with NoteDb, but not with ReviewDb.

Robot comments are always stored as JSON, even if notedb.writeJson is
set to false.

In NoteDb robot comments are not stored in the change meta ref but in
a separate refs/changes/XX/YYYY/robot-comments ref. By storing robot
comments in a separate ref we can delete robot comments without
rewriting the history of the change meta branch which is needed for
auditing and hence cannot be rewritten. Deletion of robot comments is
not implemented in this change, but we may want to support this later
as the amount and size of robot comments can be large.

Draft robot comments are not supported, but robot comments are always
published comments.

Change-Id: I2d8a5ca59e9a8b2c34863c54a3a9576599f69526
Signed-off-by: Edwin Kempin <ekempin@google.com>
  • Loading branch information
EdwinKempin committed Sep 28, 2016
1 parent 76b1375 commit 3fde7e4
Show file tree
Hide file tree
Showing 46 changed files with 1,710 additions and 229 deletions.
49 changes: 49 additions & 0 deletions Documentation/config-robot-comments.txt
@@ -0,0 +1,49 @@
= Gerrit Code Review - Robot Comments

Gerrit has special support for inline comments that are generated by
automated third-party systems, so called "robot comments". For example
robot comments can be used to represent the results of code analyzers.

In contrast to regular inline comments which are free-text comments,
robot comments are more structured and can contain additional data,
such as a robot ID, a robot run ID and a URL, see
link:rest-api-changes.html#robot-comment-info[RobotCommentInfo] for
details.

It is planned to visualize robot comments differently in the web UI so
that they can be easily distinguished from human comments. Users should
also be able to use filtering on robot comments, so that only part of
the robot comments or no robot comments are shown. In addition it is
planned that robot comments can contain fixes, that users can apply by
a single click.

== REST endpoints

* Posting robot comments is done by the
link:rest-api-changes.html[Set Review] REST endpoint. The
link:rest-api-changes.html#review-input[input] for this REST endpoint
can contain robot comments in its `robot_comments` field.
* link:rest-api-changes.html#list-robot-comments[List Robot Comments]
* link:rest-api-changes.html#get-robot-comment[Get Robot Comment]

== Storage

Robot comments are stored per change in a
`refs/changes/XX/YYYY/robot-comments` ref, where `XX/YYYY` is the
sharded change ID.

Robot comments can be dropped by deleting this ref.

== Limitations

* Robot comments are only supported with NoteDb, but not with ReviewDb.
* Robot comments are not displayed in the web UI yet.
* There is no support for draft robot comments, but robot comments are
always published and visible to everyone who can see the change.

GERRIT
------
Part of link:index.html[Gerrit Code Review]

SEARCHBOX
---------
1 change: 1 addition & 0 deletions Documentation/index.txt
Expand Up @@ -45,6 +45,7 @@
. link:config-hooks.html[Hooks]
. link:config-mail.html[Mail Templates]
. link:config-cla.html[Contributor Agreements]
. link:config-robot-comments.html[Robot Comments]

== Server Administration
. link:install.html[Installation Guide]
Expand Down
122 changes: 122 additions & 0 deletions Documentation/rest-api-changes.txt
Expand Up @@ -3904,6 +3904,102 @@ describes the published comment.
}
----

[[list-comments]]
=== List Robot Comments
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
--

Lists the link:config-robot-comments.html[robot comments] of a
revision.

As result a map is returned that maps the file path to a list of
link:#robot-comment-info[RobotCommentInfo] entries. The entries in the
map are sorted by file path.

.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/ HTTP/1.0
----

.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8

)]}'
{
"gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
{
"id": "TvcXrmjM",
"line": 23,
"message": "unused import",
"updated": "2016-02-26 15:40:43.986000000",
"author": {
"_account_id": 1000110,
"name": "Code Analyzer",
"email": "code.analyzer@example.com"
},
"robotId": "importChecker",
"robotRunId": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
},
{
"id": "TveXwFiA",
"line": 49,
"message": "wrong indention",
"updated": "2016-02-26 15:40:45.328000000",
"author": {
"_account_id": 1000110,
"name": "Code Analyzer",
"email": "code.analyzer@example.com"
},
"robotId": "styleChecker",
"robotRunId": "5c606c425dd45184484f9d0a2ffd725a7607839b"
}
]
}
----

[[get-robot-comment]]
=== Get Robot Comment
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/link:#comment-id[\{comment-id\}]'
--

Retrieves a link:config-robot-comments.html[robot comment] of a
revision.

.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/TvcXrmjM HTTP/1.0
----

As response a link:#robot-comment-info[RobotCommentInfo] entity is
returned that describes the robot comment.

.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8

)]}'
{
"id": "TvcXrmjM",
"line": 23,
"message": "unused import",
"updated": "2016-02-26 15:40:43.986000000",
"author": {
"_account_id": 1000110,
"name": "Code Analyzer",
"email": "code.analyzer@example.com"
},
"robotId": "importChecker",
"robotRunId": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
}
----

[[list-files]]
=== List Files
--
Expand Down Expand Up @@ -5465,6 +5561,9 @@ label names to the voting values.
|`comments` |optional|
The comments that should be added as a map that maps a file path to a
list of link:#comment-input[CommentInput] entities.
|`robot_comments` |optional|
The robot comments that should be added as a map that maps a file path
to a list of link:#robot-comment-input[RobotCommentInput] entities.
|`strict_labels` |`true` if not set|
Whether all labels are required to be within the user's permitted ranges
based on access controls. +
Expand Down Expand Up @@ -5591,6 +5690,29 @@ This field is always set if the option is requested; if no push
certificate was provided, it is set to an empty object.
|===========================

[[robot-comment-info]]
=== RobotCommentInfo
The `RobotCommentInfo` entity contains information about a robot inline
comment.

`RobotCommentInfo` has the same fields as link:#[CommentInfo].
In addition `RobotCommentInfo` has the following fields:

[options="header",cols="1,^1,5"]
|===========================
|Field Name ||Description
|`robot_id` ||The ID of the robot that generated this comment.
|`robot_run_id` ||An ID of the run of the robot.
|`url` |optional|URL to more information.
|===========================

[[robot-comment-input]]
=== RobotCommentInput
The `RobotCommentInput` entity contains information for creating an inline
robot comment.

`RobotCommentInput` has the same fields as link:#[RobotCommentInfo].

[[rule-input]]
=== RuleInput
The `RuleInput` entity contains information to test a Prolog rule.
Expand Down
@@ -0,0 +1,130 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.acceptance.api.revision;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;

import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;

import org.junit.Test;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RobotCommentsIT extends AbstractDaemonTest {
@Test
public void comments() throws Exception {
assume().that(notesMigration.enabled()).isTrue();

PushOneCommit.Result r = createChange();
RobotCommentInput in = createRobotCommentInput();
ReviewInput reviewInput = new ReviewInput();
Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
robotComments.put(in.path, Collections.singletonList(in));
reviewInput.robotComments = robotComments;
reviewInput.message = "comment test";
gApi.changes()
.id(r.getChangeId())
.current()
.review(reviewInput);

Map<String, List<RobotCommentInfo>> out = gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.robotComments();
assertThat(out).hasSize(1);
RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
assertRobotComment(comment, in, false);

List<RobotCommentInfo> list = gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.robotCommentsAsList();
assertThat(list).hasSize(1);

RobotCommentInfo comment2 = list.get(0);
assertRobotComment(comment2, in);

RobotCommentInfo comment3 = gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.robotComment(comment.id)
.get();
assertRobotComment(comment3, in);
}

@Test
public void robotCommentsNotSupported() throws Exception {
assume().that(notesMigration.enabled()).isFalse();

PushOneCommit.Result r = createChange();
RobotCommentInput in = createRobotCommentInput();
ReviewInput reviewInput = new ReviewInput();
Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
robotComments.put(FILE_NAME, Collections.singletonList(in));
reviewInput.robotComments = robotComments;
reviewInput.message = "comment test";

exception.expect(MethodNotAllowedException.class);
exception.expectMessage("robot comments not supported");
gApi.changes()
.id(r.getChangeId())
.current()
.review(reviewInput);
}

private RobotCommentInput createRobotCommentInput() {
RobotCommentInput in = new RobotCommentInput();
in.robotId = "happyRobot";
in.robotRunId = "1";
in.url = "http://www.happy-robot.com";
in.line = 1;
in.message = "nit: trailing whitespace";
in.path = FILE_NAME;
return in;
}

private void assertRobotComment(RobotCommentInfo c,
RobotCommentInput expected) {
assertRobotComment(c, expected, true);
}

private void assertRobotComment(RobotCommentInfo c,
RobotCommentInput expected, boolean expectPath) {
assertThat(c.robotId).isEqualTo(expected.robotId);
assertThat(c.robotRunId).isEqualTo(expected.robotRunId);
assertThat(c.url).isEqualTo(expected.url);
assertThat(c.line).isEqualTo(expected.line);
assertThat(c.message).isEqualTo(expected.message);

assertThat(c.author.email).isEqualTo(admin.email);

if (expectPath) {
assertThat(c.path).isEqualTo(expected.path);
} else {
assertThat(c.path).isNull();
}
}
}
Expand Up @@ -34,6 +34,7 @@ public class ReviewInput {

public Map<String, Short> labels;
public Map<String, List<CommentInput>> comments;
public Map<String, List<RobotCommentInput>> robotComments;

/**
* If true require all labels to be within the user's permitted ranges based
Expand Down Expand Up @@ -94,6 +95,12 @@ public enum DraftHandling {
public static class CommentInput extends Comment {
}

public static class RobotCommentInput extends CommentInput {
public String robotId;
public String robotRunId;
public String url;
}

public ReviewInput message(String msg) {
message = msg != null && !msg.isEmpty() ? msg : null;
return this;
Expand Down

0 comments on commit 3fde7e4

Please sign in to comment.