Add support for Robot Comments
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>
This commit is contained in:
parent
76b1375aa8
commit
3fde7e4e75
49
Documentation/config-robot-comments.txt
Normal file
49
Documentation/config-robot-comments.txt
Normal file
@ -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
|
||||
---------
|
@ -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]
|
||||
|
@ -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
|
||||
--
|
||||
@ -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. +
|
||||
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -94,6 +95,12 @@ public class ReviewInput {
|
||||
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;
|
||||
|
@ -20,6 +20,7 @@ import com.google.gerrit.extensions.common.CommentInfo;
|
||||
import com.google.gerrit.extensions.common.CommitInfo;
|
||||
import com.google.gerrit.extensions.common.FileInfo;
|
||||
import com.google.gerrit.extensions.common.MergeableInfo;
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.NotImplementedException;
|
||||
@ -54,15 +55,18 @@ public interface RevisionApi {
|
||||
MergeableInfo mergeableOtherBranches() throws RestApiException;
|
||||
|
||||
Map<String, List<CommentInfo>> comments() throws RestApiException;
|
||||
Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException;
|
||||
Map<String, List<CommentInfo>> drafts() throws RestApiException;
|
||||
|
||||
List<CommentInfo> commentsAsList() throws RestApiException;
|
||||
List<CommentInfo> draftsAsList() throws RestApiException;
|
||||
List<RobotCommentInfo> robotCommentsAsList() throws RestApiException;
|
||||
|
||||
DraftApi createDraft(DraftInput in) throws RestApiException;
|
||||
DraftApi draft(String id) throws RestApiException;
|
||||
|
||||
CommentApi comment(String id) throws RestApiException;
|
||||
RobotCommentApi robotComment(String id) throws RestApiException;
|
||||
|
||||
/**
|
||||
* Returns patch of revision.
|
||||
@ -196,6 +200,12 @@ public interface RevisionApi {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<RobotCommentInfo>> robotComments()
|
||||
throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommentInfo> commentsAsList() throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
@ -206,6 +216,12 @@ public interface RevisionApi {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RobotCommentInfo> robotCommentsAsList()
|
||||
throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<CommentInfo>> drafts() throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
@ -226,6 +242,11 @@ public interface RevisionApi {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RobotCommentApi robotComment(String id) throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryResult patch() throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
|
@ -0,0 +1,35 @@
|
||||
// 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.extensions.api.changes;
|
||||
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.restapi.NotImplementedException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
|
||||
public interface RobotCommentApi {
|
||||
RobotCommentInfo get() throws RestApiException;
|
||||
|
||||
/**
|
||||
* A default implementation which allows source compatibility
|
||||
* when adding new methods to the interface.
|
||||
**/
|
||||
class NotImplemented implements RobotCommentApi {
|
||||
@Override
|
||||
public RobotCommentInfo get() throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// 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.extensions.common;
|
||||
|
||||
public class RobotCommentInfo extends CommentInfo {
|
||||
public String robotId;
|
||||
public String robotRunId;
|
||||
public String url;
|
||||
}
|
@ -149,6 +149,7 @@ public final class Change {
|
||||
}
|
||||
int ce = nextNonDigit(ref, cs);
|
||||
if (ref.substring(ce).equals(RefNames.META_SUFFIX)
|
||||
|| ref.substring(ce).equals(RefNames.ROBOT_COMMENTS_SUFFIX)
|
||||
|| PatchSet.Id.fromRef(ref, ce) >= 0) {
|
||||
return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
|
||||
}
|
||||
|
@ -68,6 +68,9 @@ public class RefNames {
|
||||
/** Suffix of a meta ref in the NoteDb. */
|
||||
public static final String META_SUFFIX = "/meta";
|
||||
|
||||
/** Suffix of a ref that stores robot comments in the NoteDb. */
|
||||
public static final String ROBOT_COMMENTS_SUFFIX = "/robot-comments";
|
||||
|
||||
public static final String EDIT_PREFIX = "edit-";
|
||||
|
||||
public static String fullName(String ref) {
|
||||
@ -92,6 +95,14 @@ public class RefNames {
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
public static String robotCommentsRef(Change.Id id) {
|
||||
StringBuilder r = new StringBuilder();
|
||||
r.append(REFS_CHANGES);
|
||||
r.append(shard(id.get()));
|
||||
r.append(ROBOT_COMMENTS_SUFFIX);
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
public static String refsUsers(Account.Id accountId) {
|
||||
StringBuilder r = new StringBuilder();
|
||||
r.append(REFS_USERS);
|
||||
|
@ -0,0 +1,54 @@
|
||||
// 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.reviewdb.client;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Objects;
|
||||
|
||||
public class RobotComment extends Comment {
|
||||
public String robotId;
|
||||
public String robotRunId;
|
||||
public String url;
|
||||
|
||||
public RobotComment(Key key, Account.Id author, Timestamp writtenOn,
|
||||
short side, String message, String serverId, String robotId,
|
||||
String robotRunId) {
|
||||
super(key, author, writtenOn, side, message, serverId);
|
||||
this.robotId = robotId;
|
||||
this.robotRunId = robotRunId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder()
|
||||
.append("RobotComment{")
|
||||
.append("key=").append(key).append(',')
|
||||
.append("robotId=").append(robotId).append(',')
|
||||
.append("robotRunId=").append(robotRunId).append(',')
|
||||
.append("lineNbr=").append(lineNbr).append(',')
|
||||
.append("author=").append(author.getId().get()).append(',')
|
||||
.append("writtenOn=").append(writtenOn.toString()).append(',')
|
||||
.append("side=").append(side).append(',')
|
||||
.append("message=").append(Objects.toString(message, "")).append(',')
|
||||
.append("parentUuid=")
|
||||
.append(Objects.toString(parentUuid, "")).append(',')
|
||||
.append("range=").append(Objects.toString(range, "")).append(',')
|
||||
.append("revId=").append(revId != null ? revId : "").append(',')
|
||||
.append("tag=").append(Objects.toString(tag, "")).append(',')
|
||||
.append("url=").append(url)
|
||||
.append('}')
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import static java.util.stream.Collectors.toList;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
@ -34,6 +35,7 @@ import com.google.gerrit.reviewdb.client.PatchLineComment;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.config.AllUsersName;
|
||||
import com.google.gerrit.server.config.GerritServerId;
|
||||
@ -156,9 +158,17 @@ public class CommentsUtil {
|
||||
}
|
||||
|
||||
notes.load();
|
||||
List<Comment> comments = new ArrayList<>();
|
||||
comments.addAll(notes.getComments().values());
|
||||
return sort(comments);
|
||||
return sort(Lists.newArrayList(notes.getComments().values()));
|
||||
}
|
||||
|
||||
public List<RobotComment> robotCommentsByChange(ChangeNotes notes)
|
||||
throws OrmException {
|
||||
if (!migration.readChanges()) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
notes.load();
|
||||
return sort(Lists.newArrayList(notes.getRobotComments().values()));
|
||||
}
|
||||
|
||||
public List<Comment> draftByChange(ReviewDb db, ChangeNotes notes)
|
||||
@ -221,6 +231,14 @@ public class CommentsUtil {
|
||||
commentsOnPatchSet(notes.load().getComments().values(), psId));
|
||||
}
|
||||
|
||||
public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes,
|
||||
PatchSet.Id psId) throws OrmException {
|
||||
if (!migration.readChanges()) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
|
||||
}
|
||||
|
||||
/**
|
||||
* For the commit message the A side in a diff view is always empty when a
|
||||
* comparison against an ancestor is done, so there can't be any comments on
|
||||
@ -309,6 +327,13 @@ public class CommentsUtil {
|
||||
.upsert(toPatchLineComments(update.getId(), status, comments));
|
||||
}
|
||||
|
||||
public void putRobotComments(ChangeUpdate update,
|
||||
Iterable<RobotComment> comments) {
|
||||
for (RobotComment c : comments) {
|
||||
update.putRobotComment(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteComments(ReviewDb db, ChangeUpdate update,
|
||||
Iterable<Comment> comments) throws OrmException {
|
||||
for (Comment c : comments) {
|
||||
@ -352,11 +377,11 @@ public class CommentsUtil {
|
||||
return sort(result);
|
||||
}
|
||||
|
||||
private static List<Comment> commentsOnPatchSet(
|
||||
Collection<Comment> allComments,
|
||||
private static <T extends Comment> List<T> commentsOnPatchSet(
|
||||
Collection<T> allComments,
|
||||
PatchSet.Id psId) {
|
||||
List<Comment> result = new ArrayList<>(allComments.size());
|
||||
for (Comment c : allComments) {
|
||||
List<T> result = new ArrayList<>(allComments.size());
|
||||
for (T c : allComments) {
|
||||
if (c.key.patchSetId == psId.get()) {
|
||||
result.add(c);
|
||||
}
|
||||
@ -400,7 +425,7 @@ public class CommentsUtil {
|
||||
RefNames.refsDraftCommentsPrefix(changeId)).values();
|
||||
}
|
||||
|
||||
private static List<Comment> sort(List<Comment> comments) {
|
||||
private static <T extends Comment> List<T> sort(List<T> comments) {
|
||||
Collections.sort(comments, COMMENT_ORDER);
|
||||
return comments;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ public class Module extends FactoryModule {
|
||||
|
||||
factory(ChangeApiImpl.Factory.class);
|
||||
factory(CommentApiImpl.Factory.class);
|
||||
factory(RobotCommentApiImpl.Factory.class);
|
||||
factory(DraftApiImpl.Factory.class);
|
||||
factory(RevisionApiImpl.Factory.class);
|
||||
factory(FileApiImpl.Factory.class);
|
||||
|
@ -26,6 +26,7 @@ import com.google.gerrit.extensions.api.changes.FileApi;
|
||||
import com.google.gerrit.extensions.api.changes.RebaseInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
import com.google.gerrit.extensions.api.changes.RevisionApi;
|
||||
import com.google.gerrit.extensions.api.changes.RobotCommentApi;
|
||||
import com.google.gerrit.extensions.api.changes.SubmitInput;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.common.ActionInfo;
|
||||
@ -33,6 +34,7 @@ import com.google.gerrit.extensions.common.CommentInfo;
|
||||
import com.google.gerrit.extensions.common.CommitInfo;
|
||||
import com.google.gerrit.extensions.common.FileInfo;
|
||||
import com.google.gerrit.extensions.common.MergeableInfo;
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.IdString;
|
||||
@ -50,6 +52,7 @@ import com.google.gerrit.server.change.GetPatch;
|
||||
import com.google.gerrit.server.change.GetRevisionActions;
|
||||
import com.google.gerrit.server.change.ListRevisionComments;
|
||||
import com.google.gerrit.server.change.ListRevisionDrafts;
|
||||
import com.google.gerrit.server.change.ListRobotComments;
|
||||
import com.google.gerrit.server.change.Mergeable;
|
||||
import com.google.gerrit.server.change.PostReview;
|
||||
import com.google.gerrit.server.change.PreviewSubmit;
|
||||
@ -58,6 +61,7 @@ import com.google.gerrit.server.change.Rebase;
|
||||
import com.google.gerrit.server.change.RebaseUtil;
|
||||
import com.google.gerrit.server.change.Reviewed;
|
||||
import com.google.gerrit.server.change.RevisionResource;
|
||||
import com.google.gerrit.server.change.RobotComments;
|
||||
import com.google.gerrit.server.change.Submit;
|
||||
import com.google.gerrit.server.change.TestSubmitType;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
@ -101,12 +105,15 @@ class RevisionApiImpl implements RevisionApi {
|
||||
private final Mergeable mergeable;
|
||||
private final FileApiImpl.Factory fileApi;
|
||||
private final ListRevisionComments listComments;
|
||||
private final ListRobotComments listRobotComments;
|
||||
private final ListRevisionDrafts listDrafts;
|
||||
private final CreateDraftComment createDraft;
|
||||
private final DraftComments drafts;
|
||||
private final DraftApiImpl.Factory draftFactory;
|
||||
private final Comments comments;
|
||||
private final CommentApiImpl.Factory commentFactory;
|
||||
private final RobotComments robotComments;
|
||||
private final RobotCommentApiImpl.Factory robotCommentFactory;
|
||||
private final GetRevisionActions revisionActions;
|
||||
private final TestSubmitType testSubmitType;
|
||||
private final TestSubmitType.Get getSubmitType;
|
||||
@ -131,12 +138,15 @@ class RevisionApiImpl implements RevisionApi {
|
||||
Mergeable mergeable,
|
||||
FileApiImpl.Factory fileApi,
|
||||
ListRevisionComments listComments,
|
||||
ListRobotComments listRobotComments,
|
||||
ListRevisionDrafts listDrafts,
|
||||
CreateDraftComment createDraft,
|
||||
DraftComments drafts,
|
||||
DraftApiImpl.Factory draftFactory,
|
||||
Comments comments,
|
||||
CommentApiImpl.Factory commentFactory,
|
||||
RobotComments robotComments,
|
||||
RobotCommentApiImpl.Factory robotCommentFactory,
|
||||
GetRevisionActions revisionActions,
|
||||
TestSubmitType testSubmitType,
|
||||
TestSubmitType.Get getSubmitType,
|
||||
@ -160,12 +170,15 @@ class RevisionApiImpl implements RevisionApi {
|
||||
this.mergeable = mergeable;
|
||||
this.fileApi = fileApi;
|
||||
this.listComments = listComments;
|
||||
this.robotComments = robotComments;
|
||||
this.listRobotComments = listRobotComments;
|
||||
this.listDrafts = listDrafts;
|
||||
this.createDraft = createDraft;
|
||||
this.drafts = drafts;
|
||||
this.draftFactory = draftFactory;
|
||||
this.comments = comments;
|
||||
this.commentFactory = commentFactory;
|
||||
this.robotCommentFactory = robotCommentFactory;
|
||||
this.revisionActions = revisionActions;
|
||||
this.testSubmitType = testSubmitType;
|
||||
this.getSubmitType = getSubmitType;
|
||||
@ -352,6 +365,15 @@ class RevisionApiImpl implements RevisionApi {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
|
||||
try {
|
||||
return listRobotComments.apply(revision);
|
||||
} catch (OrmException e) {
|
||||
throw new RestApiException("Cannot retrieve robot comments", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommentInfo> commentsAsList() throws RestApiException {
|
||||
try {
|
||||
@ -370,6 +392,15 @@ class RevisionApiImpl implements RevisionApi {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
|
||||
try {
|
||||
return listRobotComments.getComments(revision);
|
||||
} catch (OrmException e) {
|
||||
throw new RestApiException("Cannot retrieve robot comments", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommentInfo> draftsAsList() throws RestApiException {
|
||||
try {
|
||||
@ -412,6 +443,16 @@ class RevisionApiImpl implements RevisionApi {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RobotCommentApi robotComment(String id) throws RestApiException {
|
||||
try {
|
||||
return robotCommentFactory
|
||||
.create(robotComments.parse(revision, IdString.fromDecoded(id)));
|
||||
} catch (OrmException e) {
|
||||
throw new RestApiException("Cannot retrieve robot comment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryResult patch() throws RestApiException {
|
||||
try {
|
||||
|
@ -0,0 +1,49 @@
|
||||
// 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.server.api.changes;
|
||||
|
||||
import com.google.gerrit.extensions.api.changes.RobotCommentApi;
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.server.change.GetRobotComment;
|
||||
import com.google.gerrit.server.change.RobotCommentResource;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
public class RobotCommentApiImpl implements RobotCommentApi {
|
||||
interface Factory {
|
||||
RobotCommentApiImpl create(RobotCommentResource c);
|
||||
}
|
||||
|
||||
private final GetRobotComment getComment;
|
||||
private final RobotCommentResource comment;
|
||||
|
||||
@Inject
|
||||
RobotCommentApiImpl(GetRobotComment getComment,
|
||||
@Assisted RobotCommentResource comment) {
|
||||
this.getComment = getComment;
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RobotCommentInfo get() throws RestApiException {
|
||||
try {
|
||||
return getComment.apply(comment);
|
||||
} catch (OrmException e) {
|
||||
throw new RestApiException("Cannot retrieve robot comment", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,10 @@ import com.google.common.collect.FluentIterable;
|
||||
import com.google.gerrit.extensions.client.Comment.Range;
|
||||
import com.google.gerrit.extensions.client.Side;
|
||||
import com.google.gerrit.extensions.common.CommentInfo;
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.restapi.Url;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.server.account.AccountLoader;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
@ -55,100 +57,134 @@ class CommentJson {
|
||||
return this;
|
||||
}
|
||||
|
||||
CommentInfo format(Comment c) throws OrmException {
|
||||
AccountLoader loader = null;
|
||||
if (fillAccounts) {
|
||||
loader = accountLoaderFactory.create(true);
|
||||
}
|
||||
CommentInfo commentInfo = toCommentInfo(c, loader);
|
||||
if (fillAccounts) {
|
||||
loader.fill();
|
||||
}
|
||||
return commentInfo;
|
||||
public CommentFormatter newCommentFormatter() {
|
||||
return new CommentFormatter();
|
||||
}
|
||||
|
||||
Map<String, List<CommentInfo>> format(Iterable<Comment> l)
|
||||
throws OrmException {
|
||||
Map<String, List<CommentInfo>> out = new TreeMap<>();
|
||||
AccountLoader accountLoader = fillAccounts
|
||||
? accountLoaderFactory.create(true)
|
||||
: null;
|
||||
public RobotCommentFormatter newRobotCommentFormatter() {
|
||||
return new RobotCommentFormatter();
|
||||
}
|
||||
|
||||
for (Comment c : l) {
|
||||
CommentInfo o = toCommentInfo(c, accountLoader);
|
||||
List<CommentInfo> list = out.get(o.path);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
out.put(o.path, list);
|
||||
private abstract class BaseCommentFormatter<F extends Comment,
|
||||
T extends CommentInfo> {
|
||||
public T format(F comment) throws OrmException {
|
||||
AccountLoader loader =
|
||||
fillAccounts ? accountLoaderFactory.create(true) : null;
|
||||
T info = toInfo(comment, loader);
|
||||
if (loader != null) {
|
||||
loader.fill();
|
||||
}
|
||||
o.path = null;
|
||||
list.add(o);
|
||||
return info;
|
||||
}
|
||||
|
||||
for (List<CommentInfo> list : out.values()) {
|
||||
Collections.sort(list, COMMENT_INFO_ORDER);
|
||||
public Map<String, List<T>> format(Iterable<F> comments)
|
||||
throws OrmException {
|
||||
AccountLoader loader =
|
||||
fillAccounts ? accountLoaderFactory.create(true) : null;
|
||||
|
||||
Map<String, List<T>> out = new TreeMap<>();
|
||||
|
||||
for (F c : comments) {
|
||||
T o = toInfo(c, loader);
|
||||
List<T> list = out.get(o.path);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
out.put(o.path, list);
|
||||
}
|
||||
o.path = null;
|
||||
list.add(o);
|
||||
}
|
||||
|
||||
for (List<T> list : out.values()) {
|
||||
Collections.sort(list, COMMENT_INFO_ORDER);
|
||||
}
|
||||
|
||||
if (loader != null) {
|
||||
loader.fill();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (accountLoader != null) {
|
||||
accountLoader.fill();
|
||||
public List<T> formatAsList(Iterable<F> comments) throws OrmException {
|
||||
AccountLoader loader =
|
||||
fillAccounts ? accountLoaderFactory.create(true) : null;
|
||||
|
||||
List<T> out = FluentIterable.from(comments)
|
||||
.transform(c -> toInfo(c, loader))
|
||||
.toSortedList(COMMENT_INFO_ORDER);
|
||||
|
||||
if (loader != null) {
|
||||
loader.fill();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
protected abstract T toInfo(F comment, AccountLoader loader);
|
||||
|
||||
List<CommentInfo> formatAsList(Iterable<Comment> l)
|
||||
throws OrmException {
|
||||
AccountLoader accountLoader = fillAccounts
|
||||
? accountLoaderFactory.create(true)
|
||||
: null;
|
||||
List<CommentInfo> out = FluentIterable
|
||||
.from(l)
|
||||
.transform(c -> toCommentInfo(c, accountLoader))
|
||||
.toSortedList(COMMENT_INFO_ORDER);
|
||||
|
||||
if (accountLoader != null) {
|
||||
accountLoader.fill();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private CommentInfo toCommentInfo(Comment c, AccountLoader loader) {
|
||||
CommentInfo r = new CommentInfo();
|
||||
if (fillPatchSet) {
|
||||
r.patchSet = c.key.patchSetId;
|
||||
}
|
||||
r.id = Url.encode(c.key.uuid);
|
||||
r.path = c.key.filename;
|
||||
if (c.side <= 0) {
|
||||
r.side = Side.PARENT;
|
||||
if (c.side < 0) {
|
||||
r.parent = -c.side;
|
||||
protected void fillCommentInfo(Comment c, CommentInfo r,
|
||||
AccountLoader loader) {
|
||||
if (fillPatchSet) {
|
||||
r.patchSet = c.key.patchSetId;
|
||||
}
|
||||
r.id = Url.encode(c.key.uuid);
|
||||
r.path = c.key.filename;
|
||||
if (c.side <= 0) {
|
||||
r.side = Side.PARENT;
|
||||
if (c.side < 0) {
|
||||
r.parent = -c.side;
|
||||
}
|
||||
}
|
||||
if (c.lineNbr > 0) {
|
||||
r.line = c.lineNbr;
|
||||
}
|
||||
r.inReplyTo = Url.encode(c.parentUuid);
|
||||
r.message = Strings.emptyToNull(c.message);
|
||||
r.updated = c.writtenOn;
|
||||
r.range = toRange(c.range);
|
||||
r.tag = c.tag;
|
||||
if (loader != null) {
|
||||
r.author = loader.get(c.author.getId());
|
||||
}
|
||||
}
|
||||
if (c.lineNbr > 0) {
|
||||
r.line = c.lineNbr;
|
||||
|
||||
private Range toRange(Comment.Range commentRange) {
|
||||
Range range = null;
|
||||
if (commentRange != null) {
|
||||
range = new Range();
|
||||
range.startLine = commentRange.startLine;
|
||||
range.startCharacter = commentRange.startChar;
|
||||
range.endLine = commentRange.endLine;
|
||||
range.endCharacter = commentRange.endChar;
|
||||
}
|
||||
return range;
|
||||
}
|
||||
r.inReplyTo = Url.encode(c.parentUuid);
|
||||
r.message = Strings.emptyToNull(c.message);
|
||||
r.updated = c.writtenOn;
|
||||
r.range = toRange(c.range);
|
||||
r.tag = c.tag;
|
||||
if (loader != null) {
|
||||
r.author = loader.get(c.author.getId());
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private Range toRange(Comment.Range commentRange) {
|
||||
Range range = null;
|
||||
if (commentRange != null) {
|
||||
range = new Range();
|
||||
range.startLine = commentRange.startLine;
|
||||
range.startCharacter = commentRange.startChar;
|
||||
range.endLine = commentRange.endLine;
|
||||
range.endCharacter = commentRange.endChar;
|
||||
class CommentFormatter extends BaseCommentFormatter<Comment, CommentInfo> {
|
||||
@Override
|
||||
protected CommentInfo toInfo(Comment c, AccountLoader loader) {
|
||||
CommentInfo ci = new CommentInfo();
|
||||
fillCommentInfo(c, ci, loader);
|
||||
return ci;
|
||||
}
|
||||
|
||||
private CommentFormatter() {
|
||||
}
|
||||
}
|
||||
|
||||
class RobotCommentFormatter
|
||||
extends BaseCommentFormatter<RobotComment, RobotCommentInfo> {
|
||||
@Override
|
||||
protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) {
|
||||
RobotCommentInfo rci = new RobotCommentInfo();
|
||||
rci.robotId = c.robotId;
|
||||
rci.robotRunId = c.robotRunId;
|
||||
rci.url = c.url;
|
||||
fillCommentInfo(c, rci, loader);
|
||||
return rci;
|
||||
}
|
||||
|
||||
private RobotCommentFormatter() {
|
||||
}
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
@ -90,8 +90,8 @@ public class CreateDraftComment implements RestModifyView<RevisionResource, Draf
|
||||
Op op = new Op(rsrc.getPatchSet().getId(), in);
|
||||
bu.addOp(rsrc.getChange().getId(), op);
|
||||
bu.execute();
|
||||
return Response.created(
|
||||
commentJson.get().setFillAccounts(false).format(op.comment));
|
||||
return Response.created(commentJson.get().setFillAccounts(false)
|
||||
.newCommentFormatter().format(op.comment));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,6 @@ public class GetComment implements RestReadView<CommentResource> {
|
||||
|
||||
@Override
|
||||
public CommentInfo apply(CommentResource rsrc) throws OrmException {
|
||||
return commentJson.get().format(rsrc.getComment());
|
||||
return commentJson.get().newCommentFormatter().format(rsrc.getComment());
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,6 @@ public class GetDraftComment implements RestReadView<DraftCommentResource> {
|
||||
|
||||
@Override
|
||||
public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
|
||||
return commentJson.get().format(rsrc.getComment());
|
||||
return commentJson.get().newCommentFormatter().format(rsrc.getComment());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
// 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.server.change;
|
||||
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class GetRobotComment implements RestReadView<RobotCommentResource> {
|
||||
|
||||
private final Provider<CommentJson> commentJson;
|
||||
|
||||
@Inject
|
||||
GetRobotComment(Provider<CommentJson> commentJson) {
|
||||
this.commentJson = commentJson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RobotCommentInfo apply(RobotCommentResource rsrc) throws OrmException {
|
||||
return commentJson.get().newRobotCommentFormatter()
|
||||
.format(rsrc.getComment());
|
||||
}
|
||||
}
|
@ -53,6 +53,7 @@ public class ListChangeComments implements RestReadView<ChangeResource> {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(true)
|
||||
.setFillPatchSet(true)
|
||||
.newCommentFormatter()
|
||||
.format(commentsUtil.publishedByChange(db.get(), cd.notes()));
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,6 @@ public class ListChangeDrafts implements RestReadView<ChangeResource> {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(false)
|
||||
.setFillPatchSet(true)
|
||||
.format(drafts);
|
||||
.newCommentFormatter().format(drafts);
|
||||
}
|
||||
}
|
||||
|
@ -57,13 +57,13 @@ public class ListRevisionDrafts implements RestReadView<RevisionResource> {
|
||||
throws OrmException {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(includeAuthorInfo())
|
||||
.format(listComments(rsrc));
|
||||
.newCommentFormatter().format(listComments(rsrc));
|
||||
}
|
||||
|
||||
public List<CommentInfo> getComments(RevisionResource rsrc)
|
||||
throws OrmException {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(includeAuthorInfo())
|
||||
.formatAsList(listComments(rsrc));
|
||||
.newCommentFormatter().formatAsList(listComments(rsrc));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
// 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.server.change;
|
||||
|
||||
import com.google.gerrit.extensions.common.RobotCommentInfo;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.CommentsUtil;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Singleton
|
||||
public class ListRobotComments implements RestReadView<RevisionResource> {
|
||||
protected final Provider<ReviewDb> db;
|
||||
protected final Provider<CommentJson> commentJson;
|
||||
protected final CommentsUtil commentsUtil;
|
||||
|
||||
@Inject
|
||||
ListRobotComments(Provider<ReviewDb> db,
|
||||
Provider<CommentJson> commentJson,
|
||||
CommentsUtil commentsUtil) {
|
||||
this.db = db;
|
||||
this.commentJson = commentJson;
|
||||
this.commentsUtil = commentsUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
|
||||
throws OrmException {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(true)
|
||||
.newRobotCommentFormatter()
|
||||
.format(listComments(rsrc));
|
||||
}
|
||||
|
||||
public List<RobotCommentInfo> getComments(RevisionResource rsrc)
|
||||
throws OrmException {
|
||||
return commentJson.get()
|
||||
.setFillAccounts(true)
|
||||
.newRobotCommentFormatter()
|
||||
.formatAsList(listComments(rsrc));
|
||||
}
|
||||
|
||||
private Iterable<RobotComment> listComments(RevisionResource rsrc)
|
||||
throws OrmException {
|
||||
return commentsUtil.robotCommentsByPatchSet(
|
||||
rsrc.getNotes(), rsrc.getPatchSet().getId());
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import static com.google.gerrit.server.change.DraftCommentResource.DRAFT_COMMENT
|
||||
import static com.google.gerrit.server.change.FileResource.FILE_KIND;
|
||||
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
|
||||
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
|
||||
import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND;
|
||||
import static com.google.gerrit.server.change.VoteResource.VOTE_KIND;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
@ -37,11 +38,13 @@ public class Module extends RestApiModule {
|
||||
bind(Reviewers.class);
|
||||
bind(DraftComments.class);
|
||||
bind(Comments.class);
|
||||
bind(RobotComments.class);
|
||||
bind(Files.class);
|
||||
bind(Votes.class);
|
||||
|
||||
DynamicMap.mapOf(binder(), CHANGE_KIND);
|
||||
DynamicMap.mapOf(binder(), COMMENT_KIND);
|
||||
DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND);
|
||||
DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND);
|
||||
DynamicMap.mapOf(binder(), FILE_KIND);
|
||||
DynamicMap.mapOf(binder(), REVIEWER_KIND);
|
||||
@ -116,6 +119,9 @@ public class Module extends RestApiModule {
|
||||
child(REVISION_KIND, "comments").to(Comments.class);
|
||||
get(COMMENT_KIND).to(GetComment.class);
|
||||
|
||||
child(REVISION_KIND, "robotcomments").to(RobotComments.class);
|
||||
get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
|
||||
|
||||
child(REVISION_KIND, "files").to(Files.class);
|
||||
put(FILE_KIND, "reviewed").to(PutReviewed.class);
|
||||
delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
|
||||
|
@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
|
||||
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
@ -43,10 +44,12 @@ import com.google.gerrit.extensions.api.changes.NotifyHandling;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewResult;
|
||||
import com.google.gerrit.extensions.client.Side;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.Response;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
@ -59,6 +62,7 @@ import com.google.gerrit.reviewdb.client.Patch;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.ApprovalsUtil;
|
||||
import com.google.gerrit.server.ChangeMessagesUtil;
|
||||
@ -76,6 +80,7 @@ import com.google.gerrit.server.git.BatchUpdate.Context;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||
import com.google.gerrit.server.notedb.NotesMigration;
|
||||
import com.google.gerrit.server.patch.PatchListCache;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.query.change.ChangeData;
|
||||
@ -94,7 +99,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -118,6 +122,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
private final CommentAdded commentAdded;
|
||||
private final PostReviewers postReviewers;
|
||||
private final String serverId;
|
||||
private final NotesMigration migration;
|
||||
|
||||
@Inject
|
||||
PostReview(Provider<ReviewDb> db,
|
||||
@ -133,7 +138,8 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
EmailReviewComments.Factory email,
|
||||
CommentAdded commentAdded,
|
||||
PostReviewers postReviewers,
|
||||
@GerritServerId String serverId) {
|
||||
@GerritServerId String serverId,
|
||||
NotesMigration migration) {
|
||||
this.db = db;
|
||||
this.batchUpdateFactory = batchUpdateFactory;
|
||||
this.changes = changes;
|
||||
@ -148,6 +154,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
this.commentAdded = commentAdded;
|
||||
this.postReviewers = postReviewers;
|
||||
this.serverId = serverId;
|
||||
this.migration = migration;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -173,6 +180,12 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
if (input.comments != null) {
|
||||
checkComments(revision, input.comments);
|
||||
}
|
||||
if (input.robotComments != null) {
|
||||
if (!migration.readChanges()) {
|
||||
throw new MethodNotAllowedException("robot comments not supported");
|
||||
}
|
||||
checkRobotComments(revision, input.robotComments);
|
||||
}
|
||||
if (input.notify == null) {
|
||||
log.warn("notify = null; assuming notify = NONE");
|
||||
input.notify = NotifyHandling.NONE;
|
||||
@ -316,16 +329,16 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
}
|
||||
}
|
||||
|
||||
private void checkComments(RevisionResource revision, Map<String, List<CommentInput>> in)
|
||||
throws BadRequestException, OrmException {
|
||||
Iterator<Map.Entry<String, List<CommentInput>>> mapItr =
|
||||
in.entrySet().iterator();
|
||||
private <T extends CommentInput> void checkComments(RevisionResource revision,
|
||||
Map<String, List<T>> in) throws BadRequestException, OrmException {
|
||||
Iterator<? extends Map.Entry<String, List<T>>> mapItr =
|
||||
in.entrySet().iterator();
|
||||
Set<String> filePaths =
|
||||
Sets.newHashSet(changeDataFactory.create(
|
||||
db.get(), revision.getControl()).filePaths(
|
||||
revision.getPatchSet()));
|
||||
while (mapItr.hasNext()) {
|
||||
Map.Entry<String, List<CommentInput>> ent = mapItr.next();
|
||||
Map.Entry<String, List<T>> ent = mapItr.next();
|
||||
String path = ent.getKey();
|
||||
if (!filePaths.contains(path) && !Patch.isMagic(path)) {
|
||||
throw new BadRequestException(String.format(
|
||||
@ -333,7 +346,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
path, revision.getChange().currentPatchSetId()));
|
||||
}
|
||||
if (Patch.isMagic(path)) {
|
||||
for (CommentInput comment : ent.getValue()) {
|
||||
for (T comment : ent.getValue()) {
|
||||
if (comment.side == Side.PARENT && comment.parent == null) {
|
||||
throw new BadRequestException(
|
||||
String.format("cannot comment on %s on auto-merge", path));
|
||||
@ -341,15 +354,15 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
}
|
||||
}
|
||||
|
||||
List<CommentInput> list = ent.getValue();
|
||||
List<T> list = ent.getValue();
|
||||
if (list == null) {
|
||||
mapItr.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<CommentInput> listItr = list.iterator();
|
||||
Iterator<T> listItr = list.iterator();
|
||||
while (listItr.hasNext()) {
|
||||
CommentInput c = listItr.next();
|
||||
T c = listItr.next();
|
||||
if (c == null) {
|
||||
listItr.remove();
|
||||
continue;
|
||||
@ -370,6 +383,25 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRobotComments(RevisionResource revision,
|
||||
Map<String, List<RobotCommentInput>> in)
|
||||
throws BadRequestException, OrmException {
|
||||
for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
|
||||
String path = e.getKey();
|
||||
for (RobotCommentInput c : e.getValue()) {
|
||||
if (c.robotId == null) {
|
||||
throw new BadRequestException(String
|
||||
.format("robotId is missing for robot comment on %s", path));
|
||||
}
|
||||
if (c.robotRunId == null) {
|
||||
throw new BadRequestException(String
|
||||
.format("robotRunId is missing for robot comment on %s", path));
|
||||
}
|
||||
}
|
||||
}
|
||||
checkComments(revision, in);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to compare Comments with CommentInput comments.
|
||||
*/
|
||||
@ -427,6 +459,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
|
||||
boolean dirty = false;
|
||||
dirty |= insertComments(ctx);
|
||||
dirty |= insertRobotComments(ctx);
|
||||
dirty |= updateLabels(ctx);
|
||||
dirty |= insertMessage(ctx);
|
||||
return dirty;
|
||||
@ -471,7 +504,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
|
||||
Set<CommentSetEntry> existingIds = in.omitDuplicateComments
|
||||
? readExistingComments(ctx)
|
||||
: Collections.<CommentSetEntry>emptySet();
|
||||
: Collections.emptySet();
|
||||
|
||||
for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
|
||||
String path = ent.getKey();
|
||||
@ -530,14 +563,53 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
return !toDel.isEmpty() || !toPublish.isEmpty();
|
||||
}
|
||||
|
||||
private boolean insertRobotComments(ChangeContext ctx) throws OrmException {
|
||||
if (in.robotComments == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
|
||||
|
||||
Set<CommentSetEntry> existingIds = in.omitDuplicateComments
|
||||
? readExistingRobotComments(ctx)
|
||||
: Collections.emptySet();
|
||||
|
||||
for (Map.Entry<String, List<RobotCommentInput>> ent : in.robotComments.entrySet()) {
|
||||
String path = ent.getKey();
|
||||
for (RobotCommentInput c : ent.getValue()) {
|
||||
RobotComment e = new RobotComment(
|
||||
new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path,
|
||||
psId.get()),
|
||||
user.getAccountId(), ctx.getWhen(), c.side(), c.message, serverId,
|
||||
c.robotId, c.robotRunId);
|
||||
e.parentUuid = Url.decode(c.inReplyTo);
|
||||
e.url = c.url;
|
||||
e.setLineNbrAndRange(c.line, c.range);
|
||||
e.tag = in.tag;
|
||||
setCommentRevId(e, patchListCache, ctx.getChange(), ps);
|
||||
|
||||
if (existingIds.contains(CommentSetEntry.create(e))) {
|
||||
continue;
|
||||
}
|
||||
toAdd.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
commentsUtil.putRobotComments(ctx.getUpdate(psId), toAdd);
|
||||
comments.addAll(toAdd);
|
||||
return !toAdd.isEmpty();
|
||||
}
|
||||
|
||||
private Set<CommentSetEntry> readExistingComments(ChangeContext ctx)
|
||||
throws OrmException {
|
||||
Set<CommentSetEntry> r = new HashSet<>();
|
||||
for (Comment c : commentsUtil.publishedByChange(ctx.getDb(),
|
||||
ctx.getNotes())) {
|
||||
r.add(CommentSetEntry.create(c));
|
||||
}
|
||||
return r;
|
||||
return commentsUtil.publishedByChange(ctx.getDb(), ctx.getNotes())
|
||||
.stream().map(CommentSetEntry::create).collect(toSet());
|
||||
}
|
||||
|
||||
private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx)
|
||||
throws OrmException {
|
||||
return commentsUtil.robotCommentsByChange(ctx.getNotes())
|
||||
.stream().map(CommentSetEntry::create).collect(toSet());
|
||||
}
|
||||
|
||||
private Map<String, Comment> changeDrafts(ChangeContext ctx)
|
||||
|
@ -92,8 +92,9 @@ public class PutDraftComment implements RestModifyView<DraftCommentResource, Dra
|
||||
Op op = new Op(rsrc.getComment().key, in);
|
||||
bu.addOp(rsrc.getChange().getId(), op);
|
||||
bu.execute();
|
||||
return Response.ok(
|
||||
commentJson.get().setFillAccounts(false).format(op.comment));
|
||||
return Response.ok(commentJson.get()
|
||||
.setFillAccounts(false)
|
||||
.newCommentFormatter().format(op.comment));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
// 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.server.change;
|
||||
|
||||
import com.google.gerrit.extensions.restapi.RestResource;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.inject.TypeLiteral;
|
||||
|
||||
public class RobotCommentResource implements RestResource {
|
||||
public static final TypeLiteral<RestView<RobotCommentResource>> ROBOT_COMMENT_KIND =
|
||||
new TypeLiteral<RestView<RobotCommentResource>>() {};
|
||||
|
||||
private final RevisionResource rev;
|
||||
private final RobotComment comment;
|
||||
|
||||
public RobotCommentResource(RevisionResource rev, RobotComment c) {
|
||||
this.rev = rev;
|
||||
this.comment = c;
|
||||
}
|
||||
|
||||
public PatchSet getPatchSet() {
|
||||
return rev.getPatchSet();
|
||||
}
|
||||
|
||||
RobotComment getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return comment.key.uuid;
|
||||
}
|
||||
|
||||
Account.Id getAuthorId() {
|
||||
return comment.author.getId();
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
// 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.server.change;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.ChildCollection;
|
||||
import com.google.gerrit.extensions.restapi.IdString;
|
||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.server.CommentsUtil;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class RobotComments
|
||||
implements ChildCollection<RevisionResource, RobotCommentResource> {
|
||||
private final DynamicMap<RestView<RobotCommentResource>> views;
|
||||
private final ListRobotComments list;
|
||||
private final CommentsUtil commentsUtil;
|
||||
|
||||
@Inject
|
||||
RobotComments(DynamicMap<RestView<RobotCommentResource>> views,
|
||||
ListRobotComments list,
|
||||
CommentsUtil commentsUtil) {
|
||||
this.views = views;
|
||||
this.list = list;
|
||||
this.commentsUtil = commentsUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DynamicMap<RestView<RobotCommentResource>> views() {
|
||||
return views;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListRobotComments list() {
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RobotCommentResource parse(RevisionResource rev, IdString id)
|
||||
throws ResourceNotFoundException, OrmException {
|
||||
String uuid = id.get();
|
||||
ChangeNotes notes = rev.getNotes();
|
||||
|
||||
for (RobotComment c : commentsUtil.robotCommentsByPatchSet(
|
||||
notes, rev.getPatchSet().getId())) {
|
||||
if (uuid.equals(c.key.uuid)) {
|
||||
return new RobotCommentResource(rev, c);
|
||||
}
|
||||
}
|
||||
throw new ResourceNotFoundException(id);
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.Patch;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.server.CommentsUtil;
|
||||
import com.google.gerrit.server.patch.PatchFile;
|
||||
import com.google.gerrit.server.patch.PatchList;
|
||||
@ -165,6 +166,14 @@ public class CommentSender extends ReplyToChangeSender {
|
||||
|
||||
private void appendComment(StringBuilder out, int contextLines,
|
||||
PatchFile currentFileData, Comment comment) {
|
||||
if (comment instanceof RobotComment) {
|
||||
RobotComment robotComment = (RobotComment) comment;
|
||||
out.append("Robot Comment from ")
|
||||
.append(robotComment.robotId)
|
||||
.append(" (run ID ")
|
||||
.append(robotComment.robotRunId)
|
||||
.append("):\n");
|
||||
}
|
||||
short side = comment.side;
|
||||
Comment.Range range = comment.range;
|
||||
if (range != null) {
|
||||
|
@ -138,7 +138,7 @@ public class ChangeDraftUpdate extends AbstractChangeUpdate {
|
||||
private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
|
||||
ObjectId curr, CommitBuilder cb)
|
||||
throws ConfigInvalidException, OrmException, IOException {
|
||||
RevisionNoteMap rnm = getRevisionNoteMap(rw, curr);
|
||||
RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
|
||||
Set<RevId> updatedRevs =
|
||||
Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
|
||||
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
|
||||
@ -158,7 +158,7 @@ public class ChangeDraftUpdate extends AbstractChangeUpdate {
|
||||
for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
|
||||
updatedRevs.add(e.getKey());
|
||||
ObjectId id = ObjectId.fromString(e.getKey().get());
|
||||
byte[] data = e.getValue().build(noteUtil);
|
||||
byte[] data = e.getValue().build(noteUtil, noteUtil.getWriteJson());
|
||||
if (!Arrays.equals(data, e.getValue().baseRaw)) {
|
||||
touchedAnyRevs = true;
|
||||
}
|
||||
@ -189,8 +189,8 @@ public class ChangeDraftUpdate extends AbstractChangeUpdate {
|
||||
return cb;
|
||||
}
|
||||
|
||||
private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
|
||||
throws ConfigInvalidException, OrmException, IOException {
|
||||
private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw,
|
||||
ObjectId curr) throws ConfigInvalidException, OrmException, IOException {
|
||||
if (migration.readChanges()) {
|
||||
// If reading from changes is enabled, then the old DraftCommentNotes
|
||||
// already parsed the revision notes. We can reuse them as long as the ref
|
||||
@ -202,7 +202,8 @@ public class ChangeDraftUpdate extends AbstractChangeUpdate {
|
||||
if (draftNotes != null) {
|
||||
ObjectId idFromNotes =
|
||||
firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
|
||||
RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
|
||||
RevisionNoteMap<ChangeRevisionNote> rnm =
|
||||
draftNotes.getRevisionNoteMap();
|
||||
if (idFromNotes.equals(curr) && rnm != null) {
|
||||
return rnm;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.reviewdb.client.RevId;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
||||
import com.google.gerrit.server.ReviewerSet;
|
||||
@ -339,10 +340,11 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
|
||||
// Parsed note map state, used by ChangeUpdate to make in-place editing of
|
||||
// notes easier.
|
||||
RevisionNoteMap revisionNoteMap;
|
||||
RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
|
||||
|
||||
private NoteDbUpdateManager.Result rebuildResult;
|
||||
private DraftCommentNotes draftCommentNotes;
|
||||
private RobotCommentNotes robotCommentNotes;
|
||||
|
||||
@VisibleForTesting
|
||||
public ChangeNotes(Args args, Change change) {
|
||||
@ -448,6 +450,12 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
filtered);
|
||||
}
|
||||
|
||||
public ImmutableListMultimap<RevId, RobotComment> getRobotComments()
|
||||
throws OrmException {
|
||||
loadRobotComments();
|
||||
return robotCommentNotes.getComments();
|
||||
}
|
||||
|
||||
/**
|
||||
* If draft comments have already been loaded for this author, then they will
|
||||
* not be reloaded. However, this method will load the comments if no draft
|
||||
@ -464,11 +472,22 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRobotComments() throws OrmException {
|
||||
if (robotCommentNotes == null) {
|
||||
robotCommentNotes = new RobotCommentNotes(args, change);
|
||||
robotCommentNotes.load();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
DraftCommentNotes getDraftCommentNotes() {
|
||||
return draftCommentNotes;
|
||||
}
|
||||
|
||||
RobotCommentNotes getRobotCommentNotes() {
|
||||
return robotCommentNotes;
|
||||
}
|
||||
|
||||
public boolean containsComment(Comment c) throws OrmException {
|
||||
if (containsCommentPublished(c)) {
|
||||
return true;
|
||||
|
@ -73,14 +73,14 @@ public class ChangeNotesCache {
|
||||
* used as an optimization; {@link ChangeNotes} is capable of lazily loading
|
||||
* it as necessary.
|
||||
*/
|
||||
@Nullable abstract RevisionNoteMap revisionNoteMap();
|
||||
@Nullable abstract RevisionNoteMap<ChangeRevisionNote> revisionNoteMap();
|
||||
}
|
||||
|
||||
private class Loader implements Callable<ChangeNotesState> {
|
||||
private final Key key;
|
||||
private final ChangeNotesRevWalk rw;
|
||||
|
||||
private RevisionNoteMap revisionNoteMap;
|
||||
private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
|
||||
|
||||
private Loader(Key key, ChangeNotesRevWalk rw) {
|
||||
this.key = key;
|
||||
|
@ -149,7 +149,7 @@ class ChangeNotesParser {
|
||||
private String submissionId;
|
||||
private String tag;
|
||||
private PatchSet.Id currentPatchSetId;
|
||||
private RevisionNoteMap revisionNoteMap;
|
||||
private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
|
||||
|
||||
ChangeNotesParser(Change.Id changeId, ObjectId tip, ChangeNotesRevWalk walk,
|
||||
ChangeNoteUtil noteUtil, NoteDbMetrics metrics) {
|
||||
@ -195,7 +195,7 @@ class ChangeNotesParser {
|
||||
return buildState();
|
||||
}
|
||||
|
||||
RevisionNoteMap getRevisionNoteMap() {
|
||||
RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() {
|
||||
return revisionNoteMap;
|
||||
}
|
||||
|
||||
@ -630,18 +630,18 @@ class ChangeNotesParser {
|
||||
revisionNoteMap = RevisionNoteMap.parse(
|
||||
noteUtil, id, reader, NoteMap.read(reader, tipCommit),
|
||||
PatchLineComment.Status.PUBLISHED);
|
||||
Map<RevId, RevisionNote> rns = revisionNoteMap.revisionNotes;
|
||||
Map<RevId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
|
||||
|
||||
for (Map.Entry<RevId, RevisionNote> e : rns.entrySet()) {
|
||||
for (Comment c : e.getValue().comments) {
|
||||
for (Map.Entry<RevId, ChangeRevisionNote> e : rns.entrySet()) {
|
||||
for (Comment c : e.getValue().getComments()) {
|
||||
comments.put(e.getKey(), c);
|
||||
}
|
||||
}
|
||||
|
||||
for (PatchSet ps : patchSets.values()) {
|
||||
RevisionNote rn = rns.get(ps.getRevision());
|
||||
if (rn != null && rn.pushCert != null) {
|
||||
ps.setPushCertificate(rn.pushCert);
|
||||
ChangeRevisionNote rn = rns.get(ps.getRevision());
|
||||
if (rn != null && rn.getPushCert() != null) {
|
||||
ps.setPushCertificate(rn.getPushCert());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,114 @@
|
||||
// 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.server.notedb;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
||||
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.util.MutableInteger;
|
||||
import org.eclipse.jgit.util.RawParseUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
class ChangeRevisionNote extends RevisionNote<Comment> {
|
||||
private static final byte[] CERT_HEADER =
|
||||
"certificate version ".getBytes(UTF_8);
|
||||
// See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
|
||||
private static final byte[] END_SIGNATURE =
|
||||
"-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
|
||||
|
||||
private final ChangeNoteUtil noteUtil;
|
||||
private final Change.Id changeId;
|
||||
private final PatchLineComment.Status status;
|
||||
private String pushCert = null;
|
||||
|
||||
ChangeRevisionNote(ChangeNoteUtil noteUtil, Change.Id changeId,
|
||||
ObjectReader reader, ObjectId noteId, PatchLineComment.Status status) {
|
||||
super(reader, noteId);
|
||||
this.noteUtil = noteUtil;
|
||||
this.changeId = changeId;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getPushCert() {
|
||||
checkParsed();
|
||||
return pushCert;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Comment> parse(byte[] raw, int offset)
|
||||
throws IOException, ConfigInvalidException {
|
||||
MutableInteger p = new MutableInteger();
|
||||
p.value = offset;
|
||||
|
||||
if (isJson(raw, p.value)) {
|
||||
RevisionNoteData data = parseJson(noteUtil, raw, p.value);
|
||||
if (status == PatchLineComment.Status.PUBLISHED) {
|
||||
pushCert = data.pushCert;
|
||||
} else {
|
||||
pushCert = null;
|
||||
}
|
||||
return data.comments;
|
||||
}
|
||||
|
||||
if (status == PatchLineComment.Status.PUBLISHED) {
|
||||
pushCert = parsePushCert(changeId, raw, p);
|
||||
trimLeadingEmptyLines(raw, p);
|
||||
} else {
|
||||
pushCert = null;
|
||||
}
|
||||
return noteUtil.parseNote(raw, p, changeId);
|
||||
}
|
||||
|
||||
private static boolean isJson(byte[] raw, int offset) {
|
||||
return raw[offset] == '{' || raw[offset] == '[';
|
||||
}
|
||||
|
||||
private RevisionNoteData parseJson(ChangeNoteUtil noteUtil, byte[] raw,
|
||||
int offset) throws IOException {
|
||||
try (InputStream is = new ByteArrayInputStream(
|
||||
raw, offset, raw.length - offset);
|
||||
Reader r = new InputStreamReader(is)) {
|
||||
return noteUtil.getGson().fromJson(r, RevisionNoteData.class);
|
||||
}
|
||||
}
|
||||
|
||||
private static String parsePushCert(Change.Id changeId, byte[] bytes,
|
||||
MutableInteger p) throws ConfigInvalidException {
|
||||
if (RawParseUtils.match(bytes, p.value, CERT_HEADER) < 0) {
|
||||
return null;
|
||||
}
|
||||
int end = Bytes.indexOf(bytes, END_SIGNATURE);
|
||||
if (end < 0) {
|
||||
throw ChangeNotes.parseException(
|
||||
changeId, "invalid push certificate in note");
|
||||
}
|
||||
int start = p.value;
|
||||
p.value = end + END_SIGNATURE.length;
|
||||
return new String(bytes, start, p.value);
|
||||
}
|
||||
}
|
@ -51,6 +51,7 @@ import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RevId;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.server.GerritPersonIdent;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.config.AnonymousCowardName;
|
||||
@ -110,6 +111,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
|
||||
private final AccountCache accountCache;
|
||||
private final ChangeDraftUpdate.Factory draftUpdateFactory;
|
||||
private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
|
||||
private final NoteDbUpdateManager.Factory updateManagerFactory;
|
||||
|
||||
private final Table<String, Account.Id, Optional<Short>> approvals;
|
||||
@ -135,6 +137,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
private boolean isAllowWriteToNewtRef;
|
||||
|
||||
private ChangeDraftUpdate draftUpdate;
|
||||
private RobotCommentUpdate robotCommentUpdate;
|
||||
|
||||
@AssistedInject
|
||||
private ChangeUpdate(
|
||||
@ -144,11 +147,12 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
AccountCache accountCache,
|
||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||
ChangeDraftUpdate.Factory draftUpdateFactory,
|
||||
RobotCommentUpdate.Factory robotCommentUpdateFactory,
|
||||
ProjectCache projectCache,
|
||||
@Assisted ChangeControl ctl,
|
||||
ChangeNoteUtil noteUtil) {
|
||||
this(serverIdent, anonymousCowardName, migration, accountCache,
|
||||
updateManagerFactory, draftUpdateFactory,
|
||||
updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory,
|
||||
projectCache, ctl, serverIdent.getWhen(), noteUtil);
|
||||
}
|
||||
|
||||
@ -160,13 +164,14 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
AccountCache accountCache,
|
||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||
ChangeDraftUpdate.Factory draftUpdateFactory,
|
||||
RobotCommentUpdate.Factory robotCommentUpdateFactory,
|
||||
ProjectCache projectCache,
|
||||
@Assisted ChangeControl ctl,
|
||||
@Assisted Date when,
|
||||
ChangeNoteUtil noteUtil) {
|
||||
this(serverIdent, anonymousCowardName, migration, accountCache,
|
||||
updateManagerFactory, draftUpdateFactory, ctl,
|
||||
when,
|
||||
updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory,
|
||||
ctl, when,
|
||||
projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
|
||||
noteUtil);
|
||||
}
|
||||
@ -188,6 +193,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
AccountCache accountCache,
|
||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||
ChangeDraftUpdate.Factory draftUpdateFactory,
|
||||
RobotCommentUpdate.Factory robotCommentUpdateFactory,
|
||||
@Assisted ChangeControl ctl,
|
||||
@Assisted Date when,
|
||||
@Assisted Comparator<String> labelNameComparator,
|
||||
@ -196,6 +202,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
anonymousCowardName, noteUtil, when);
|
||||
this.accountCache = accountCache;
|
||||
this.draftUpdateFactory = draftUpdateFactory;
|
||||
this.robotCommentUpdateFactory = robotCommentUpdateFactory;
|
||||
this.updateManagerFactory = updateManagerFactory;
|
||||
this.approvals = approvals(labelNameComparator);
|
||||
}
|
||||
@ -208,6 +215,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
AccountCache accountCache,
|
||||
NoteDbUpdateManager.Factory updateManagerFactory,
|
||||
ChangeDraftUpdate.Factory draftUpdateFactory,
|
||||
RobotCommentUpdate.Factory robotCommentUpdateFactory,
|
||||
ChangeNoteUtil noteUtil,
|
||||
@Assisted Change change,
|
||||
@Assisted @Nullable Account.Id accountId,
|
||||
@ -218,6 +226,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
accountId, authorIdent, when);
|
||||
this.accountCache = accountCache;
|
||||
this.draftUpdateFactory = draftUpdateFactory;
|
||||
this.robotCommentUpdateFactory = robotCommentUpdateFactory;
|
||||
this.updateManagerFactory = updateManagerFactory;
|
||||
this.approvals = approvals(labelNameComparator);
|
||||
}
|
||||
@ -320,6 +329,12 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
public void putRobotComment(RobotComment c) {
|
||||
verifyComment(c);
|
||||
createRobotCommentUpdateIfNull();
|
||||
robotCommentUpdate.putComment(c);
|
||||
}
|
||||
|
||||
public void deleteComment(Comment c) {
|
||||
verifyComment(c);
|
||||
createDraftUpdateIfNull().deleteComment(c);
|
||||
@ -340,6 +355,21 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
return draftUpdate;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RobotCommentUpdate createRobotCommentUpdateIfNull() {
|
||||
if (robotCommentUpdate == null) {
|
||||
ChangeNotes notes = getNotes();
|
||||
if (notes != null) {
|
||||
robotCommentUpdate =
|
||||
robotCommentUpdateFactory.create(notes, accountId, authorIdent, when);
|
||||
} else {
|
||||
robotCommentUpdate = robotCommentUpdateFactory.create(
|
||||
getChange(), accountId, authorIdent, when);
|
||||
}
|
||||
}
|
||||
return robotCommentUpdate;
|
||||
}
|
||||
|
||||
private void verifyComment(Comment c) {
|
||||
checkArgument(c.revId != null, "RevId required for comment: %s", c);
|
||||
checkArgument(c.author.getId().equals(getAccountId()),
|
||||
@ -415,7 +445,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
if (comments.isEmpty() && pushCert == null) {
|
||||
return null;
|
||||
}
|
||||
RevisionNoteMap rnm = getRevisionNoteMap(rw, curr);
|
||||
RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
|
||||
|
||||
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
|
||||
for (Comment c : comments) {
|
||||
@ -431,15 +461,15 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
|
||||
for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
|
||||
ObjectId data = inserter.insert(
|
||||
OBJ_BLOB, e.getValue().build(noteUtil));
|
||||
OBJ_BLOB, e.getValue().build(noteUtil, noteUtil.getWriteJson()));
|
||||
rnm.noteMap.set(ObjectId.fromString(e.getKey().get()), data);
|
||||
}
|
||||
|
||||
return rnm.noteMap.writeTree(inserter);
|
||||
}
|
||||
|
||||
private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
|
||||
throws ConfigInvalidException, OrmException, IOException {
|
||||
private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw,
|
||||
ObjectId curr) throws ConfigInvalidException, OrmException, IOException {
|
||||
if (curr.equals(ObjectId.zeroId())) {
|
||||
return RevisionNoteMap.emptyMap();
|
||||
}
|
||||
@ -467,12 +497,12 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
PatchLineComment.Status.PUBLISHED);
|
||||
}
|
||||
|
||||
private void checkComments(Map<RevId, RevisionNote> existingNotes,
|
||||
private void checkComments(Map<RevId, ChangeRevisionNote> existingNotes,
|
||||
Map<RevId, RevisionNoteBuilder> toUpdate) throws OrmException {
|
||||
// Prohibit various kinds of illegal operations on comments.
|
||||
Set<Comment.Key> existing = new HashSet<>();
|
||||
for (RevisionNote rn : existingNotes.values()) {
|
||||
for (Comment c : rn.comments) {
|
||||
for (ChangeRevisionNote rn : existingNotes.values()) {
|
||||
for (Comment c : rn.getComments()) {
|
||||
existing.add(c.key);
|
||||
if (draftUpdate != null) {
|
||||
// Take advantage of an existing update on All-Users to prune any
|
||||
@ -677,6 +707,10 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
return draftUpdate;
|
||||
}
|
||||
|
||||
RobotCommentUpdate getRobotCommentUpdate() {
|
||||
return robotCommentUpdate;
|
||||
}
|
||||
|
||||
public void setAllowWriteToNewRef(boolean allow) {
|
||||
isAllowWriteToNewtRef = allow;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
|
||||
private final NoteDbUpdateManager.Result rebuildResult;
|
||||
|
||||
private ImmutableListMultimap<RevId, Comment> comments;
|
||||
private RevisionNoteMap revisionNoteMap;
|
||||
private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
|
||||
|
||||
@AssistedInject
|
||||
DraftCommentNotes(
|
||||
@ -103,7 +103,7 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
|
||||
this.rebuildResult = rebuildResult;
|
||||
}
|
||||
|
||||
RevisionNoteMap getRevisionNoteMap() {
|
||||
RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() {
|
||||
return revisionNoteMap;
|
||||
}
|
||||
|
||||
@ -144,8 +144,8 @@ public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
|
||||
args.noteUtil, getChangeId(), reader, NoteMap.read(reader, tipCommit),
|
||||
PatchLineComment.Status.DRAFT);
|
||||
Multimap<RevId, Comment> cs = ArrayListMultimap.create();
|
||||
for (RevisionNote rn : revisionNoteMap.revisionNotes.values()) {
|
||||
for (Comment c : rn.comments) {
|
||||
for (ChangeRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
|
||||
for (Comment c : rn.getComments()) {
|
||||
cs.put(new RevId(c.revId), c);
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ public class NoteDbModule extends FactoryModule {
|
||||
factory(ChangeUpdate.Factory.class);
|
||||
factory(ChangeDraftUpdate.Factory.class);
|
||||
factory(DraftCommentNotes.Factory.class);
|
||||
factory(RobotCommentUpdate.Factory.class);
|
||||
factory(RobotCommentNotes.Factory.class);
|
||||
factory(NoteDbUpdateManager.Factory.class);
|
||||
if (!useTestBindings) {
|
||||
install(ChangeNotesCache.module());
|
||||
|
@ -179,6 +179,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
|
||||
private final Project.NameKey projectName;
|
||||
private final ListMultimap<String, ChangeUpdate> changeUpdates;
|
||||
private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
|
||||
private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
|
||||
private final Set<Change.Id> toDelete;
|
||||
|
||||
private OpenRepo changeRepo;
|
||||
@ -199,6 +200,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
|
||||
this.projectName = projectName;
|
||||
changeUpdates = ArrayListMultimap.create();
|
||||
draftUpdates = ArrayListMultimap.create();
|
||||
robotCommentUpdates = ArrayListMultimap.create();
|
||||
toDelete = new HashSet<>();
|
||||
}
|
||||
|
||||
@ -273,6 +275,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
|
||||
}
|
||||
return changeUpdates.isEmpty()
|
||||
&& draftUpdates.isEmpty()
|
||||
&& robotCommentUpdates.isEmpty()
|
||||
&& toDelete.isEmpty();
|
||||
}
|
||||
|
||||
@ -294,6 +297,10 @@ public class NoteDbUpdateManager implements AutoCloseable {
|
||||
if (du != null) {
|
||||
draftUpdates.put(du.getRefName(), du);
|
||||
}
|
||||
RobotCommentUpdate rcu = update.getRobotCommentUpdate();
|
||||
if (rcu != null) {
|
||||
robotCommentUpdates.put(rcu.getRefName(), rcu);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(ChangeDraftUpdate draftUpdate) {
|
||||
@ -453,6 +460,9 @@ public class NoteDbUpdateManager implements AutoCloseable {
|
||||
if (!draftUpdates.isEmpty()) {
|
||||
addUpdates(draftUpdates, allUsersRepo);
|
||||
}
|
||||
if (!robotCommentUpdates.isEmpty()) {
|
||||
addUpdates(robotCommentUpdates, changeRepo);
|
||||
}
|
||||
for (Change.Id id : toDelete) {
|
||||
doDelete(id);
|
||||
}
|
||||
|
@ -14,106 +14,66 @@
|
||||
|
||||
package com.google.gerrit.server.notedb;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
||||
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.util.MutableInteger;
|
||||
import org.eclipse.jgit.util.RawParseUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
class RevisionNote {
|
||||
abstract class RevisionNote<T extends Comment> {
|
||||
static final int MAX_NOTE_SZ = 25 << 20;
|
||||
|
||||
private static final byte[] CERT_HEADER =
|
||||
"certificate version ".getBytes(UTF_8);
|
||||
// See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
|
||||
private static final byte[] END_SIGNATURE =
|
||||
"-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
|
||||
|
||||
private static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
|
||||
protected static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
|
||||
while (p.value < bytes.length && bytes[p.value] == '\n') {
|
||||
p.value++;
|
||||
}
|
||||
}
|
||||
|
||||
private static String parsePushCert(Change.Id changeId, byte[] bytes,
|
||||
MutableInteger p) throws ConfigInvalidException {
|
||||
if (RawParseUtils.match(bytes, p.value, CERT_HEADER) < 0) {
|
||||
return null;
|
||||
}
|
||||
int end = Bytes.indexOf(bytes, END_SIGNATURE);
|
||||
if (end < 0) {
|
||||
throw ChangeNotes.parseException(
|
||||
changeId, "invalid push certificate in note");
|
||||
}
|
||||
int start = p.value;
|
||||
p.value = end + END_SIGNATURE.length;
|
||||
return new String(bytes, start, p.value);
|
||||
private final ObjectReader reader;
|
||||
private final ObjectId noteId;
|
||||
|
||||
private byte[] raw;
|
||||
private ImmutableList<T> comments;
|
||||
|
||||
RevisionNote(ObjectReader reader, ObjectId noteId) {
|
||||
this.reader = reader;
|
||||
this.noteId = noteId;
|
||||
}
|
||||
|
||||
final byte[] raw;
|
||||
final ImmutableList<Comment> comments;
|
||||
final String pushCert;
|
||||
public byte[] getRaw() {
|
||||
checkParsed();
|
||||
return raw;
|
||||
}
|
||||
|
||||
RevisionNote(ChangeNoteUtil noteUtil, Change.Id changeId,
|
||||
ObjectReader reader, ObjectId noteId, PatchLineComment.Status status)
|
||||
throws ConfigInvalidException, IOException {
|
||||
public ImmutableList<T> getComments() {
|
||||
checkParsed();
|
||||
return comments;
|
||||
}
|
||||
|
||||
public void parse() throws IOException, ConfigInvalidException {
|
||||
raw = reader.open(noteId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
||||
MutableInteger p = new MutableInteger();
|
||||
trimLeadingEmptyLines(raw, p);
|
||||
if (p.value >= raw.length) {
|
||||
comments = null;
|
||||
pushCert = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJson(raw, p.value)) {
|
||||
RevisionNoteData data = parseJson(noteUtil, p.value);
|
||||
comments = ImmutableList.copyOf(data.comments);
|
||||
if (status == PatchLineComment.Status.PUBLISHED) {
|
||||
pushCert = data.pushCert;
|
||||
} else {
|
||||
pushCert = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == PatchLineComment.Status.PUBLISHED) {
|
||||
pushCert = parsePushCert(changeId, raw, p);
|
||||
trimLeadingEmptyLines(raw, p);
|
||||
} else {
|
||||
pushCert = null;
|
||||
}
|
||||
comments = ImmutableList.copyOf(noteUtil.parseNote(raw, p, changeId));
|
||||
comments = ImmutableList.copyOf(parse(raw, p.value));
|
||||
}
|
||||
|
||||
private static boolean isJson(byte[] raw, int offset) {
|
||||
return raw[offset] == '{' || raw[offset] == '[';
|
||||
}
|
||||
protected abstract List<T> parse(byte[] raw, int offset)
|
||||
throws IOException, ConfigInvalidException;
|
||||
|
||||
private RevisionNoteData parseJson(ChangeNoteUtil noteUtil, int offset)
|
||||
throws IOException{
|
||||
RevisionNoteData data;
|
||||
try (InputStream is = new ByteArrayInputStream(
|
||||
raw, offset, raw.length - offset);
|
||||
Reader r = new InputStreamReader(is)) {
|
||||
data = noteUtil.getGson().fromJson(r, RevisionNoteData.class);
|
||||
}
|
||||
return data;
|
||||
protected void checkParsed() {
|
||||
checkState(raw != null, "revision note not parsed yet");
|
||||
}
|
||||
}
|
||||
|
@ -37,10 +37,12 @@ import java.util.Set;
|
||||
|
||||
class RevisionNoteBuilder {
|
||||
static class Cache {
|
||||
private final RevisionNoteMap revisionNoteMap;
|
||||
private final RevisionNoteMap<?
|
||||
extends RevisionNote<? extends Comment>> revisionNoteMap;
|
||||
private final Map<RevId, RevisionNoteBuilder> builders;
|
||||
|
||||
Cache(RevisionNoteMap revisionNoteMap) {
|
||||
Cache(RevisionNoteMap<?
|
||||
extends RevisionNote<? extends Comment>> revisionNoteMap) {
|
||||
this.revisionNoteMap = revisionNoteMap;
|
||||
this.builders = new HashMap<>();
|
||||
}
|
||||
@ -61,18 +63,20 @@ class RevisionNoteBuilder {
|
||||
}
|
||||
|
||||
final byte[] baseRaw;
|
||||
final List<Comment> baseComments;
|
||||
final List<? extends Comment> baseComments;
|
||||
final Map<Comment.Key, Comment> put;
|
||||
final Set<Comment.Key> delete;
|
||||
|
||||
private String pushCert;
|
||||
|
||||
RevisionNoteBuilder(RevisionNote base) {
|
||||
RevisionNoteBuilder(RevisionNote<? extends Comment> base) {
|
||||
if (base != null) {
|
||||
baseRaw = base.raw;
|
||||
baseComments = base.comments;
|
||||
put = Maps.newHashMapWithExpectedSize(base.comments.size());
|
||||
pushCert = base.pushCert;
|
||||
baseRaw = base.getRaw();
|
||||
baseComments = base.getComments();
|
||||
put = Maps.newHashMapWithExpectedSize(baseComments.size());
|
||||
if (base instanceof ChangeRevisionNote) {
|
||||
pushCert = ((ChangeRevisionNote) base).getPushCert();
|
||||
}
|
||||
} else {
|
||||
baseRaw = new byte[0];
|
||||
baseComments = Collections.emptyList();
|
||||
@ -82,9 +86,10 @@ class RevisionNoteBuilder {
|
||||
delete = new HashSet<>();
|
||||
}
|
||||
|
||||
public byte[] build(ChangeNoteUtil noteUtil) throws IOException {
|
||||
public byte[] build(ChangeNoteUtil noteUtil, boolean writeJson)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
if (noteUtil.getWriteJson()) {
|
||||
if (writeJson) {
|
||||
buildNoteJson(noteUtil, out);
|
||||
} else {
|
||||
buildNoteLegacy(noteUtil, out);
|
||||
@ -123,7 +128,7 @@ class RevisionNoteBuilder {
|
||||
return all;
|
||||
}
|
||||
|
||||
private void buildNoteJson(final ChangeNoteUtil noteUtil, OutputStream out)
|
||||
private void buildNoteJson(ChangeNoteUtil noteUtil, OutputStream out)
|
||||
throws IOException {
|
||||
Multimap<Integer, Comment> comments = buildCommentMap();
|
||||
if (comments.isEmpty() && pushCert == null) {
|
||||
|
@ -16,6 +16,7 @@ package com.google.gerrit.server.notedb;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Comment;
|
||||
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
||||
import com.google.gerrit.reviewdb.client.RevId;
|
||||
|
||||
@ -28,30 +29,45 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class RevisionNoteMap {
|
||||
class RevisionNoteMap<T extends RevisionNote<? extends Comment>> {
|
||||
final NoteMap noteMap;
|
||||
final ImmutableMap<RevId, RevisionNote> revisionNotes;
|
||||
final ImmutableMap<RevId, T> revisionNotes;
|
||||
|
||||
static RevisionNoteMap parse(ChangeNoteUtil noteUtil,
|
||||
static RevisionNoteMap<ChangeRevisionNote> parse(ChangeNoteUtil noteUtil,
|
||||
Change.Id changeId, ObjectReader reader, NoteMap noteMap,
|
||||
PatchLineComment.Status status)
|
||||
throws ConfigInvalidException, IOException {
|
||||
Map<RevId, RevisionNote> result = new HashMap<>();
|
||||
throws ConfigInvalidException, IOException {
|
||||
Map<RevId, ChangeRevisionNote> result = new HashMap<>();
|
||||
for (Note note : noteMap) {
|
||||
RevisionNote rn = new RevisionNote(
|
||||
ChangeRevisionNote rn = new ChangeRevisionNote(
|
||||
noteUtil, changeId, reader, note.getData(), status);
|
||||
rn.parse();
|
||||
result.put(new RevId(note.name()), rn);
|
||||
}
|
||||
return new RevisionNoteMap(noteMap, ImmutableMap.copyOf(result));
|
||||
return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
|
||||
}
|
||||
|
||||
static RevisionNoteMap emptyMap() {
|
||||
return new RevisionNoteMap(NoteMap.newEmptyMap(),
|
||||
ImmutableMap.<RevId, RevisionNote> of());
|
||||
static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
|
||||
ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
|
||||
throws ConfigInvalidException, IOException {
|
||||
Map<RevId, RobotCommentsRevisionNote> result = new HashMap<>();
|
||||
for (Note note : noteMap) {
|
||||
RobotCommentsRevisionNote rn = new RobotCommentsRevisionNote(
|
||||
noteUtil, reader, note.getData());
|
||||
rn.parse();
|
||||
result.put(new RevId(note.name()), rn);
|
||||
}
|
||||
return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
|
||||
}
|
||||
|
||||
static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T>
|
||||
emptyMap() {
|
||||
return new RevisionNoteMap<>(NoteMap.newEmptyMap(),
|
||||
ImmutableMap.<RevId, T> of());
|
||||
}
|
||||
|
||||
private RevisionNoteMap(NoteMap noteMap,
|
||||
ImmutableMap<RevId, RevisionNote> revisionNotes) {
|
||||
ImmutableMap<RevId, T> revisionNotes) {
|
||||
this.noteMap = noteMap;
|
||||
this.revisionNotes = revisionNotes;
|
||||
}
|
||||
|
@ -0,0 +1,108 @@
|
||||
// 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.server.notedb;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.reviewdb.client.RevId;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
import com.google.inject.assistedinject.AssistedInject;
|
||||
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.notes.NoteMap;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RobotCommentNotes extends AbstractChangeNotes<RobotCommentNotes> {
|
||||
public interface Factory {
|
||||
RobotCommentNotes create(Change change);
|
||||
}
|
||||
|
||||
private final Change change;
|
||||
|
||||
private ImmutableListMultimap<RevId, RobotComment> comments;
|
||||
private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
|
||||
|
||||
@AssistedInject
|
||||
RobotCommentNotes(
|
||||
Args args,
|
||||
@Assisted Change change) {
|
||||
super(args, change.getId(), false);
|
||||
this.change = change;
|
||||
}
|
||||
|
||||
RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap() {
|
||||
return revisionNoteMap;
|
||||
}
|
||||
|
||||
public ImmutableListMultimap<RevId, RobotComment> getComments() {
|
||||
return comments;
|
||||
}
|
||||
|
||||
public boolean containsComment(RobotComment c) {
|
||||
for (RobotComment existing : comments.values()) {
|
||||
if (c.key.equals(existing.key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getRefName() {
|
||||
return RefNames.robotCommentsRef(getChangeId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoad(LoadHandle handle)
|
||||
throws IOException, ConfigInvalidException {
|
||||
ObjectId rev = handle.id();
|
||||
if (rev == null) {
|
||||
loadDefaults();
|
||||
return;
|
||||
}
|
||||
|
||||
RevCommit tipCommit = handle.walk().parseCommit(rev);
|
||||
ObjectReader reader = handle.walk().getObjectReader();
|
||||
revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader,
|
||||
NoteMap.read(reader, tipCommit));
|
||||
Multimap<RevId, RobotComment> cs = ArrayListMultimap.create();
|
||||
for (RobotCommentsRevisionNote rn :
|
||||
revisionNoteMap.revisionNotes.values()) {
|
||||
for (RobotComment c : rn.getComments()) {
|
||||
cs.put(new RevId(c.revId), c);
|
||||
}
|
||||
}
|
||||
comments = ImmutableListMultimap.copyOf(cs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadDefaults() {
|
||||
comments = ImmutableListMultimap.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Project.NameKey getProjectName() {
|
||||
return change.getProject();
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
// 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.server.notedb;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.gerrit.reviewdb.client.RefNames.robotCommentsRef;
|
||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RevId;
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
import com.google.gerrit.server.GerritPersonIdent;
|
||||
import com.google.gerrit.server.config.AnonymousCowardName;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
import com.google.inject.assistedinject.AssistedInject;
|
||||
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.CommitBuilder;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectInserter;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.notes.NoteMap;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A single delta to apply atomically to a change.
|
||||
* <p>
|
||||
* This delta contains only robot comments on a single patch set of a change by
|
||||
* a single author. This delta will become a single commit in the repository.
|
||||
* <p>
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
public class RobotCommentUpdate extends AbstractChangeUpdate{
|
||||
public interface Factory {
|
||||
RobotCommentUpdate create(ChangeNotes notes, Account.Id accountId,
|
||||
PersonIdent authorIdent, Date when);
|
||||
RobotCommentUpdate create(Change change, Account.Id accountId,
|
||||
PersonIdent authorIdent, Date when);
|
||||
}
|
||||
|
||||
private List<RobotComment> put = new ArrayList<>();
|
||||
|
||||
@AssistedInject
|
||||
private RobotCommentUpdate(
|
||||
@GerritPersonIdent PersonIdent serverIdent,
|
||||
@AnonymousCowardName String anonymousCowardName,
|
||||
NotesMigration migration,
|
||||
ChangeNoteUtil noteUtil,
|
||||
@Assisted ChangeNotes notes,
|
||||
@Assisted Account.Id accountId,
|
||||
@Assisted PersonIdent authorIdent,
|
||||
@Assisted Date when) {
|
||||
super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
|
||||
accountId, authorIdent, when);
|
||||
}
|
||||
|
||||
@AssistedInject
|
||||
private RobotCommentUpdate(
|
||||
@GerritPersonIdent PersonIdent serverIdent,
|
||||
@AnonymousCowardName String anonymousCowardName,
|
||||
NotesMigration migration,
|
||||
ChangeNoteUtil noteUtil,
|
||||
@Assisted Change change,
|
||||
@Assisted Account.Id accountId,
|
||||
@Assisted PersonIdent authorIdent,
|
||||
@Assisted Date when) {
|
||||
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
|
||||
accountId, authorIdent, when);
|
||||
}
|
||||
|
||||
public void putComment(RobotComment c) {
|
||||
verifyComment(c);
|
||||
put.add(c);
|
||||
}
|
||||
|
||||
private void verifyComment(RobotComment comment) {
|
||||
checkArgument(comment.author.getId().equals(accountId),
|
||||
"The author for the following comment does not match the author of"
|
||||
+ " this RobotCommentUpdate (%s): %s", accountId, comment);
|
||||
}
|
||||
|
||||
private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
|
||||
ObjectId curr, CommitBuilder cb)
|
||||
throws ConfigInvalidException, OrmException, IOException {
|
||||
RevisionNoteMap<RobotCommentsRevisionNote> rnm =
|
||||
getRevisionNoteMap(rw, curr);
|
||||
Set<RevId> updatedRevs =
|
||||
Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
|
||||
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
|
||||
|
||||
for (RobotComment c : put) {
|
||||
cache.get(new RevId(c.revId)).putComment(c);
|
||||
}
|
||||
|
||||
Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
|
||||
boolean touchedAnyRevs = false;
|
||||
boolean hasComments = false;
|
||||
for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
|
||||
updatedRevs.add(e.getKey());
|
||||
ObjectId id = ObjectId.fromString(e.getKey().get());
|
||||
byte[] data = e.getValue().build(noteUtil, true);
|
||||
if (!Arrays.equals(data, e.getValue().baseRaw)) {
|
||||
touchedAnyRevs = true;
|
||||
}
|
||||
if (data.length == 0) {
|
||||
rnm.noteMap.remove(id);
|
||||
} else {
|
||||
hasComments = true;
|
||||
ObjectId dataBlob = ins.insert(OBJ_BLOB, data);
|
||||
rnm.noteMap.set(id, dataBlob);
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't touch any notes, tell the caller this was a no-op update. We
|
||||
// couldn't have done this in isEmpty() below because we hadn't read the old
|
||||
// data yet.
|
||||
if (!touchedAnyRevs) {
|
||||
return NO_OP_UPDATE;
|
||||
}
|
||||
|
||||
// If we touched every revision and there are no comments left, tell the
|
||||
// caller to delete the entire ref.
|
||||
boolean touchedAllRevs = updatedRevs.equals(rnm.revisionNotes.keySet());
|
||||
if (touchedAllRevs && !hasComments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cb.setTreeId(rnm.noteMap.writeTree(ins));
|
||||
return cb;
|
||||
}
|
||||
|
||||
private RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap(
|
||||
RevWalk rw, ObjectId curr)
|
||||
throws ConfigInvalidException, OrmException, IOException {
|
||||
if (curr.equals(ObjectId.zeroId())) {
|
||||
return RevisionNoteMap.emptyMap();
|
||||
}
|
||||
if (migration.readChanges()) {
|
||||
// If reading from changes is enabled, then the old RobotCommentNotes
|
||||
// already parsed the revision notes. We can reuse them as long as the ref
|
||||
// hasn't advanced.
|
||||
ChangeNotes changeNotes = getNotes();
|
||||
if (changeNotes != null) {
|
||||
RobotCommentNotes robotCommentNotes =
|
||||
changeNotes.load().getRobotCommentNotes();
|
||||
if (robotCommentNotes != null) {
|
||||
ObjectId idFromNotes =
|
||||
firstNonNull(robotCommentNotes.getRevision(), ObjectId.zeroId());
|
||||
RevisionNoteMap<RobotCommentsRevisionNote> rnm =
|
||||
robotCommentNotes.getRevisionNoteMap();
|
||||
if (idFromNotes.equals(curr) && rnm != null) {
|
||||
return rnm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NoteMap noteMap;
|
||||
if (!curr.equals(ObjectId.zeroId())) {
|
||||
noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
|
||||
} else {
|
||||
noteMap = NoteMap.newEmptyMap();
|
||||
}
|
||||
// Even though reading from changes might not be enabled, we need to
|
||||
// parse any existing revision notes so we can merge them.
|
||||
return RevisionNoteMap.parseRobotComments(
|
||||
noteUtil,
|
||||
rw.getObjectReader(),
|
||||
noteMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins,
|
||||
ObjectId curr) throws OrmException, IOException {
|
||||
CommitBuilder cb = new CommitBuilder();
|
||||
cb.setMessage("Update robot comments");
|
||||
try {
|
||||
return storeCommentsInNotes(rw, ins, curr, cb);
|
||||
} catch (ConfigInvalidException e) {
|
||||
throw new OrmException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Project.NameKey getProjectName() {
|
||||
return getNotes().getProjectName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getRefName() {
|
||||
return robotCommentsRef(getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return put.isEmpty();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
// 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.server.notedb;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
public class RobotCommentsRevisionNote extends RevisionNote<RobotComment> {
|
||||
private final ChangeNoteUtil noteUtil;
|
||||
|
||||
RobotCommentsRevisionNote(ChangeNoteUtil noteUtil, ObjectReader reader,
|
||||
ObjectId noteId) {
|
||||
super(reader, noteId);
|
||||
this.noteUtil = noteUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<RobotComment> parse(byte[] raw, int offset)
|
||||
throws IOException {
|
||||
try (InputStream is = new ByteArrayInputStream(
|
||||
raw, offset, raw.length - offset);
|
||||
Reader r = new InputStreamReader(is)) {
|
||||
return noteUtil.getGson().fromJson(r,
|
||||
RobotCommentsRevisionNoteData.class).comments;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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.server.notedb;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.RobotComment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RobotCommentsRevisionNoteData {
|
||||
List<RobotComment> comments;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user