Merge "Add support for Robot Comments"
This commit is contained in:
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;
|
||||
}
|
Reference in New Issue
Block a user