Merge "Modify draft comment tooltip text"
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 59ed6ff..998b143 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -129,9 +129,9 @@
-t::
Apply a 'TAG' to the change message, votes, and inline comments. The 'TAG'
can represent an external system like CI that does automated verification
- of the change. Comments with specific 'TAG' values can be filtered out in
- the web UI.
- Note that to apply different tags on on different votes/comments, multiple
+ of the change. Comments that contain TAG values with 'autogenerated:' prefix
+ can be filtered out in the web UI.
+ Note that to apply different tags on different votes/comments, multiple
invocations of the SSH command are required.
== ACCESS
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index cb3b88b..c144a09 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2475,7 +2475,66 @@
"username": "jdoe"
},
"date": "2013-03-23 21:34:02.419000000",
- "message": "Change message removed by: Administrator; Reason: spam",
+ "message": "a change message",
+ "_revision_number": 1
+ }
+----
+
+[[delete-change-message]]
+=== Delete Change Message
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}' +
+'POST /changes/link:#change-id[\{change-id\}]//message/link:#change-message-id[\{change-message-id\}/delete'
+--
+
+Deletes a change message by replacing the change message with a new message,
+which contains the name of the user who deleted the change message and the
+reason why it was deleted. The reason can be provided in the request body as a
+link:#delete-change-message-input[DeleteChangeMessageInput] entity.
+
+Note that only users with the
+link:access-control.html#capability_administrateServer[Administrate Server]
+global capability are permitted to delete a change message.
+
+To delete a change message, send a DELETE request:
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780 HTTP/1.0
+----
+
+To provide a reason for the deletion, use a POST request:
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message/aaee04dcb46bafc8be24d8aa70b3b1beb7df5780/delete HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "reason": "spam"
+ }
+----
+
+As response a link:#change-message-info[ChangeMessageInfo] entity is returned that
+describes the updated change message.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "aaee04dcb46bafc8be24d8aa70b3b1beb7df5780",
+ "author": {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ },
+ "date": "2013-03-23 21:34:02.419000000",
+ "message": "Change message removed by: Administrator\nReason: spam",
"_revision_number": 1
}
----
@@ -4796,8 +4855,8 @@
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
----
-As result a map is returned that maps the link:#file-id[file path] to a list of
-link:#file-info[FileInfo] entries. The entries in the map are
+As result a map is returned that maps the link:#file-id[file path] to a
+link:#file-info[FileInfo] entry. The entries in the map are
sorted by file path.
.Response
@@ -5589,8 +5648,9 @@
The time and date describing when the approval was made.
|`tag` |optional|
Value of the `tag` field from link:#review-input[ReviewInput] set
-while posting the review.
-NOTE: To apply different tags on on different votes/comments multiple
+while posting the review. Votes/comments that contain `tag` with
+'autogenerated:' prefix can be filtered out in the web UI.
+NOTE: To apply different tags on different votes/comments multiple
invocations of the REST call are required.
|`post_submit` |not set if `false`|
If true, this vote was made after the change was submitted.
@@ -5853,8 +5913,9 @@
|`message` ||The text left by the user.
|`tag` |optional|
Value of the `tag` field from link:#review-input[ReviewInput] set
-while posting the review.
-NOTE: To apply different tags on on different votes/comments multiple
+while posting the review. Votes/comments that contain `tag` with
+'autogenerated:' prefix can be filtered out in the web UI.
+NOTE: To apply different tags on different votes/comments multiple
invocations of the REST call are required.
|`_revision_number` |optional|
Which patchset (if any) generated this message.
@@ -5974,7 +6035,8 @@
|`tag` |optional, drafts only|
Value of the `tag` field. Only allowed on link:#create-draft[draft comment] +
inputs; for published comments, use the `tag` field in +
-link#review-input[ReviewInput]
+link#review-input[ReviewInput]. Votes/comments that contain `tag` with
+'autogenerated:' prefix can be filtered out in the web UI.
|`unresolved` |optional|
Whether or not the comment must be addressed by the user. This value will
default to false if the comment is an orphan, or the value of the `in_reply_to`
@@ -6049,6 +6111,20 @@
of recipient type to link:#notify-info[NotifyInfo] entity.
|=============================
+[[delete-change-message-input]]
+=== DeleteChangeMessageInput
+The `DeleteChangeMessageInput` entity contains the options for deleting a change message.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name ||Description
+|`reason` |optional|
+The reason why the change message should be deleted. +
+If set, the change message will be replaced with
+"Change message removed by: `name`\nReason: `reason`",
+or just "Change message removed by: `name`." if not set.
+|=============================
+
[[delete-comment-input]]
=== DeleteCommentInput
The `DeleteCommentInput` entity contains the option for deleting a comment.
@@ -6792,8 +6868,8 @@
|`tag` |optional|
Apply this tag to the review comment message, votes, and inline
comments. Tags may be used by CI or other automated systems to
-distinguish them from human reviews. Comments with specific tag
-values can be filtered out in the web UI.
+distinguish them from human reviews. Votes/comments that contain `tag` with
+'autogenerated:' prefix can be filtered out in the web UI.
|`labels` |optional|
The votes that should be added to the revision as a map that maps the
label names to the voting values.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index f32480b..5fd8be4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2525,6 +2525,51 @@
}
----
+[[list-files]]
+=== List Files
+--
+'GET /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/files/'
+--
+
+Lists the files that were modified, added or deleted in a commit.
+
+.Request
+----
+ GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/files/ HTTP/1.0
+----
+
+As result a map is returned that maps the link:rest-api-changes.html#file-id[file path] to a
+link:rest-api-changes.html#file-info[FileInfo] entry. The entries in the map are
+sorted by file path.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "/COMMIT_MSG": {
+ "status": "A",
+ "lines_inserted": 7,
+ "size_delta": 551,
+ "size": 551
+ },
+ "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
+ "lines_inserted": 5,
+ "lines_deleted": 3,
+ "size_delta": 98,
+ "size": 23348
+ }
+ }
+----
+
+The integer-valued request parameter `parent` changes the response to return a
+list of the files which are different in this commit compared to the given
+parent commit. This is useful for supporting review of merge commits. The value
+is the 1-based index of the parent's position in the commit object.
+
[[dashboard-endpoints]]
== Dashboard Endpoints
diff --git a/WORKSPACE b/WORKSPACE
index f255ac7..4491b12 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -680,30 +680,36 @@
sha1 = "4785a3c21320980282f9f33d0d1264a69040538f",
)
-TRUTH_VERS = "0.40"
+TRUTH_VERS = "0.42"
maven_jar(
name = "truth",
artifact = "com.google.truth:truth:" + TRUTH_VERS,
- sha1 = "0d74e716afec045cc4a178dbbfde2a8314ae5574",
+ sha1 = "b5768f644b114e6cf5c3962c2ebcb072f788dcbb",
)
maven_jar(
name = "truth-java8-extension",
artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
- sha1 = "636e49d675bc28e0b3ae0edd077d6acbbb159166",
+ sha1 = "4d01dfa5b3780632a3d109e14e101f01d10cce2c",
)
maven_jar(
name = "truth-liteproto-extension",
artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
- sha1 = "21210ac07e5cfbe83f04733f806224a6c0ae4d2d",
+ sha1 = "c231e6735aa6c133c7e411ae1c1c90b124900a8b",
)
maven_jar(
name = "truth-proto-extension",
artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
- sha1 = "5a2b504143a5fec2b6be8bce292b3b7577a81789",
+ sha1 = "c41d22e8b4a61b4171e57c44a2959ebee0091a14",
+)
+
+maven_jar(
+ name = "diffutils",
+ artifact = "com.googlecode.java-diff-utils:diffutils:1.3.0",
+ sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
)
# When bumping the easymock version number, make sure to also move powermock to a compatible version
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 960b558..1d87880 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -33,6 +33,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
@@ -63,6 +64,7 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeType;
+import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -76,6 +78,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -148,6 +151,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -167,6 +171,7 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.FetchResult;
@@ -1695,4 +1700,29 @@
}
}
}
+
+ protected List<RevCommit> getChangeMetaCommitsInReverseOrder(Change.Id changeId)
+ throws IOException {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk revWalk = new RevWalk(repo)) {
+ revWalk.sort(RevSort.TOPO);
+ revWalk.sort(RevSort.REVERSE);
+ Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId));
+ revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId()));
+ return Lists.newArrayList(revWalk);
+ }
+ }
+
+ protected List<CommentInfo> getChangeSortedComments(int changeNum) throws Exception {
+ List<CommentInfo> comments = new ArrayList<>();
+ Map<String, List<CommentInfo>> commentsMap = gApi.changes().id(changeNum).comments();
+ for (Map.Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) {
+ for (CommentInfo c : e.getValue()) {
+ c.path = e.getKey(); // Set the comment's path field.
+ comments.add(c);
+ }
+ }
+ comments.sort(Comparator.comparing(c -> c.id));
+ return comments;
+ }
}
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index f2d0f70..df57c27 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance;
+import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.gerrit.extensions.api.changes.RecipientType.BCC;
import static com.google.gerrit.extensions.api.changes.RecipientType.CC;
@@ -100,7 +101,7 @@
public FakeEmailSenderSubject notSent() {
if (actual().peekMessage() != null) {
- fail("a message wasn't sent");
+ failWithoutActual(fact("expected message", "sent"));
}
return this;
}
@@ -108,7 +109,7 @@
public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
message = actual().nextMessage();
if (message == null) {
- fail("a message was sent");
+ failWithoutActual(fact("expected message", "not sent"));
}
recipients = new HashMap<>();
recipients.put(TO, parseAddresses(message, "To"));
@@ -123,11 +124,12 @@
.collect(toList()));
this.users = users;
if (!message.headers().containsKey("X-Gerrit-MessageType")) {
- fail("a message was sent with X-Gerrit-MessageType header");
+ failWithoutActual(
+ fact("expected to have message sent with", "X-Gerrit-MessageType header"));
}
EmailHeader header = message.headers().get("X-Gerrit-MessageType");
if (!header.equals(new EmailHeader.String(messageType))) {
- fail("message of type " + messageType + " was sent; X-Gerrit-MessageType is " + header);
+ failWithoutActual(fact("expected message of type", messageType));
}
// Return a named subject that displays a human-readable table of
@@ -191,9 +193,10 @@
private void rcpt(@Nullable RecipientType type, String email, boolean expected) {
if (recipients.get(type).contains(email) != expected) {
- fail(
- expected ? "notifies" : "doesn't notify",
- "]\n" + type + ": " + users.emailToName(email) + "\n]");
+ failWithoutActual(
+ fact(
+ expected ? "notifies" : "doesn't notify",
+ "[\n" + type + ": " + users.emailToName(email) + "\n]"));
}
if (expected) {
accountedFor.add(email);
@@ -219,9 +222,10 @@
}
}
if (!ok) {
- fail(
- "was fully tested, missing assertions for: "
- + recipientMapToString(unaccountedFor, e -> users.emailToName(e)));
+ failWithoutActual(
+ fact(
+ "expected assertions for",
+ recipientMapToString(unaccountedFor, e -> users.emailToName(e))));
}
return this;
}
@@ -282,7 +286,7 @@
private void rcpt(@Nullable RecipientType type, NotifyType watch) {
if (!users.watchers.containsKey(watch)) {
- fail("configured to watch", watch);
+ failWithoutActual(fact("expected to be configured to watch", watch));
}
rcpt(type, users.watchers.get(watch));
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
index eb2d2b9..66356f1 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
@@ -24,6 +24,14 @@
ChangeMessageInfo get() throws RestApiException;
/**
+ * Deletes a change message by replacing its message. For NoteDb, it's implemented by rewriting
+ * the commit history of change meta branch.
+ *
+ * @return the change message with its message updated.
+ */
+ ChangeMessageInfo delete(DeleteChangeMessageInput input) throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -32,5 +40,10 @@
public ChangeMessageInfo get() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public ChangeMessageInfo delete(DeleteChangeMessageInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/api/changes/DeleteChangeMessageInput.java b/java/com/google/gerrit/extensions/api/changes/DeleteChangeMessageInput.java
new file mode 100644
index 0000000..ece5a61
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/DeleteChangeMessageInput.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 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.restapi.DefaultInput;
+
+/** Input for deleting a change message. */
+public class DeleteChangeMessageInput {
+ /** An optional reason for deleting the change message. */
+ @DefaultInput public String reason;
+
+ public DeleteChangeMessageInput() {
+ this.reason = "";
+ }
+
+ public DeleteChangeMessageInput(String reason) {
+ this.reason = reason;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index a2e61b8..07ad71b 100644
--- a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -45,4 +45,24 @@
public int hashCode() {
return Objects.hash(id, tag, author, realAuthor, date, message, _revisionNumber);
}
+
+ @Override
+ public String toString() {
+ return "ChangeMessageInfo{"
+ + "id="
+ + id
+ + ", tag="
+ + tag
+ + ", author="
+ + author
+ + ", realAuthor="
+ + realAuthor
+ + ", date="
+ + date
+ + ", _revisionNumber"
+ + _revisionNumber
+ + ", message=["
+ + message
+ + "]}";
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/FileInfo.java b/java/com/google/gerrit/extensions/common/FileInfo.java
index a812908..747f9e2 100644
--- a/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.common;
+import java.util.Objects;
+
public class FileInfo {
public Character status;
public Boolean binary;
@@ -22,4 +24,18 @@
public Integer linesDeleted;
public long sizeDelta;
public long size;
+
+ public boolean equals(Object o) {
+ if (o instanceof FileInfo) {
+ FileInfo fileInfo = (FileInfo) o;
+ return Objects.equals(status, fileInfo.status)
+ && Objects.equals(binary, fileInfo.binary)
+ && Objects.equals(oldPath, fileInfo.oldPath)
+ && Objects.equals(linesInserted, fileInfo.linesInserted)
+ && Objects.equals(linesDeleted, fileInfo.linesDeleted)
+ && Objects.equals(sizeDelta, fileInfo.sizeDelta)
+ && Objects.equals(size, fileInfo.size);
+ }
+ return false;
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
index b478a7e..db7f0d1 100644
--- a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.common.testing;
+import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
@@ -51,14 +52,14 @@
public void isValid() {
isNotNull();
if (!actual().isValid()) {
- fail("is valid");
+ failWithoutActual(fact("expected", "valid"));
}
}
public void isInvalid() {
isNotNull();
if (actual().isValid()) {
- fail("is invalid");
+ failWithoutActual(fact("expected", "not valid"));
}
}
}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index e635072..a6dbb53 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.common.Nullable;
@@ -27,7 +28,9 @@
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.update.BatchUpdateReviewDb;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -129,6 +132,71 @@
}
/**
+ * Replace an existing change message with the provided new message.
+ *
+ * <p>The ID of a change message is different between NoteDb and ReviewDb. In NoteDb, it's the
+ * commit SHA-1, but in ReviewDb it's generated randomly. To make sure the change message can be
+ * deleted from both NoteDb and ReviewDb, the index of the change message must be used rather than
+ * its ID.
+ *
+ * @param db the {@code ReviewDb} instance to update.
+ * @param update change update.
+ * @param targetMessageIdx the index of the target change message.
+ * @param newMessage the new message which is going to replace the old.
+ * @throws OrmException
+ */
+ public void replaceChangeMessage(
+ ReviewDb db, ChangeUpdate update, int targetMessageIdx, String newMessage)
+ throws OrmException {
+ if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
+ if (db instanceof BatchUpdateReviewDb) {
+ db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+ }
+ db = unwrapDb(db);
+
+ List<ChangeMessage> messagesInReviewDb =
+ sortChangeMessages(db.changeMessages().byChange(update.getId()));
+ if (migration.readChanges()) {
+ sanityCheckForChangeMessages(messagesInReviewDb, update.getNotes().getChangeMessages());
+ }
+ ChangeMessage targetMessage = messagesInReviewDb.get(targetMessageIdx);
+ targetMessage.setMessage(newMessage);
+ db.changeMessages().upsert(Collections.singleton(targetMessage));
+ }
+
+ update.deleteChangeMessageByRewritingHistory(targetMessageIdx, newMessage);
+ }
+
+ private static void sanityCheckForChangeMessages(
+ List<ChangeMessage> messagesInReviewDb, List<ChangeMessage> messagesInNoteDb) {
+ String message =
+ String.format(
+ "Change messages in ReivewDb and NoteDb don't match: NoteDb %s; ReviewDb %s",
+ messagesInNoteDb, messagesInReviewDb);
+ if (messagesInReviewDb.size() != messagesInNoteDb.size()) {
+ throw new IllegalStateException(message);
+ }
+
+ for (int i = 0; i < messagesInReviewDb.size(); i++) {
+ ChangeMessage messageInReviewDb = messagesInReviewDb.get(i);
+ ChangeMessage messageInNoteDb = messagesInNoteDb.get(i);
+
+ // Don't compare the keys because they are different for the same change message in NoteDb and
+ // ReviewDb.
+ boolean isEqual =
+ Objects.equals(messageInReviewDb.getAuthor(), messageInNoteDb.getAuthor())
+ && Objects.equals(messageInReviewDb.getWrittenOn(), messageInNoteDb.getWrittenOn())
+ && Objects.equals(messageInReviewDb.getMessage(), messageInNoteDb.getMessage())
+ && Objects.equals(messageInReviewDb.getPatchSetId(), messageInNoteDb.getPatchSetId())
+ && Objects.equals(messageInReviewDb.getTag(), messageInNoteDb.getTag())
+ && Objects.equals(messageInReviewDb.getRealAuthor(), messageInNoteDb.getRealAuthor());
+ if (!isEqual) {
+ throw new IllegalStateException(message);
+ }
+ }
+ }
+
+ /**
* @param tag value of a tag, or null.
* @return whether the tag starts with the autogenerated prefix.
*/
diff --git a/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
index 988ac91..14310e8 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeMessageApiImpl.java
@@ -17,9 +17,11 @@
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.api.changes.ChangeMessageApi;
+import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.change.ChangeMessageResource;
+import com.google.gerrit.server.restapi.change.DeleteChangeMessage;
import com.google.gerrit.server.restapi.change.GetChangeMessage;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -30,12 +32,16 @@
}
private final GetChangeMessage getChangeMessage;
+ private final DeleteChangeMessage deleteChangeMessage;
private final ChangeMessageResource changeMessageResource;
@Inject
ChangeMessageApiImpl(
- GetChangeMessage getChangeMessage, @Assisted ChangeMessageResource changeMessageResource) {
+ GetChangeMessage getChangeMessage,
+ DeleteChangeMessage deleteChangeMessage,
+ @Assisted ChangeMessageResource changeMessageResource) {
this.getChangeMessage = getChangeMessage;
+ this.deleteChangeMessage = deleteChangeMessage;
this.changeMessageResource = changeMessageResource;
}
@@ -47,4 +53,13 @@
throw asRestApiException("Cannot retrieve change message", e);
}
}
+
+ @Override
+ public ChangeMessageInfo delete(DeleteChangeMessageInput input) throws RestApiException {
+ try {
+ return deleteChangeMessage.apply(changeMessageResource, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot delete change message", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/change/FileInfoJson.java b/java/com/google/gerrit/server/change/FileInfoJson.java
index f4b7457..56cc8df 100644
--- a/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -20,6 +20,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
@@ -68,7 +69,12 @@
private Map<String, FileInfo> toFileInfoMap(Change change, PatchListKey key)
throws PatchListNotAvailableException {
- PatchList list = patchListCache.get(key, change.getProject());
+ return toFileInfoMap(change.getProject(), key);
+ }
+
+ public Map<String, FileInfo> toFileInfoMap(Project.NameKey project, PatchListKey key)
+ throws PatchListNotAvailableException {
+ PatchList list = patchListCache.get(key, project);
Map<String, FileInfo> files = new TreeMap<>();
for (PatchListEntry e : list.getPatches()) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 516dcd4..d4c013d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -45,7 +45,6 @@
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
@@ -1486,19 +1485,6 @@
}
}
- /**
- * Gets an unmodifiable view of the pushOptions.
- *
- * <p>The collection is empty if the client does not support push options, or if the client did
- * not send any options.
- *
- * @return an unmodifiable view of pushOptions.
- */
- @Nullable
- ListMultimap<String, String> getPushOptions() {
- return ImmutableListMultimap.copyOf(pushOptions);
- }
-
private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
// Permit exactly one new change request per push.
if (magicBranch != null) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 6cc6eb5..1cf71c0 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -238,25 +238,17 @@
public static class ChangeIdValidator implements CommitValidationListener {
private static final String CHANGE_ID_PREFIX = FooterConstants.CHANGE_ID.getName() + ":";
private static final String MISSING_CHANGE_ID_MSG =
- "[%s] missing " + FooterConstants.CHANGE_ID.getName() + " in commit message footer";
+ "[%s] missing Change-Id in commit message footer";
private static final String MISSING_SUBJECT_MSG =
- "[%s] missing subject; "
- + FooterConstants.CHANGE_ID.getName()
- + " must be in commit message footer";
+ "[%s] missing subject; Change-Id must be in commit message footer";
private static final String MULTIPLE_CHANGE_ID_MSG =
- "[%s] multiple " + FooterConstants.CHANGE_ID.getName() + " lines in commit message footer";
+ "[%s] multiple Change-Id lines in commit message footer";
private static final String INVALID_CHANGE_ID_MSG =
- "[%s] invalid "
- + FooterConstants.CHANGE_ID.getName()
- + " line format in commit message footer";
+ "[%s] invalid Change-Id line format in commit message footer";
@VisibleForTesting
public static final String CHANGE_ID_MISMATCH_MSG =
- "[%s] "
- + FooterConstants.CHANGE_ID.getName()
- + " in commit message footer does not match"
- + FooterConstants.CHANGE_ID.getName()
- + " of target change";
+ "[%s] Change-Id in commit message footer does not match Change-Id of target change";
private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
@@ -367,10 +359,9 @@
// If there are no SSH keys, the commit-msg hook must be installed via
// HTTP(S)
if (hostKeys.isEmpty()) {
- String p = "${gitdir}/hooks/commit-msg";
return String.format(
- " gitdir=$(git rev-parse --git-dir); curl -o %s %stools/hooks/commit-msg ; chmod +x %s",
- p, canonicalWebUrl, p);
+ " f=\"$(git rev-parse --git-dir)/hooks/commit-msg\"; curl -o \"$f\" %stools/hooks/commit-msg ; chmod +x \"$f\"",
+ canonicalWebUrl);
}
// SSH keys exist, so the hook can be installed with scp.
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 75053c8..707056c 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -237,7 +237,7 @@
.sorted(CommentsUtil.COMMENT_ORDER)
.collect(toList());
Project.NameKey project = cd.project();
- String changeUrl = canonicalUrl.get() + "#/c/" + cd.getId().get();
+ String changeUrl = canonicalUrl.get() + "c/" + cd.project().get() + "/+/" + cd.getId().get();
List<MailComment> parsedComments;
if (useHtmlParser(message)) {
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 00a496a2..4ba3b67 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -221,10 +221,7 @@
/** Get a link to the change; null if the server doesn't know its own address. */
public String getChangeUrl() {
if (getGerritUrl() != null) {
- final StringBuilder r = new StringBuilder();
- r.append(getGerritUrl());
- r.append(change.getChangeId());
- return r.toString();
+ return getGerritUrl() + "c/" + change.getProject().get() + "/+/" + change.getChangeId();
}
return null;
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 903a982..bd7f8fc 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -160,6 +160,7 @@
private ChangeDraftUpdate draftUpdate;
private RobotCommentUpdate robotCommentUpdate;
private DeleteCommentRewriter deleteCommentRewriter;
+ private DeleteChangeMessageRewriter deleteChangeMessageRewriter;
@AssistedInject
private ChangeUpdate(
@@ -395,6 +396,11 @@
deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage);
}
+ public void deleteChangeMessageByRewritingHistory(int targetMessageIdx, String newMessage) {
+ deleteChangeMessageRewriter =
+ new DeleteChangeMessageRewriter(getChange().getId(), targetMessageIdx, newMessage);
+ }
+
@VisibleForTesting
ChangeDraftUpdate createDraftUpdateIfNull() {
if (draftUpdate == null) {
@@ -609,7 +615,9 @@
@Override
protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
throws OrmException, IOException {
- checkState(deleteCommentRewriter == null, "cannot update and rewrite ref in one BatchUpdate");
+ checkState(
+ deleteCommentRewriter == null && deleteChangeMessageRewriter == null,
+ "cannot update and rewrite ref in one BatchUpdate");
CommitBuilder cb = new CommitBuilder();
@@ -823,6 +831,10 @@
return deleteCommentRewriter;
}
+ public DeleteChangeMessageRewriter getDeleteChangeMessageRewriter() {
+ return deleteChangeMessageRewriter;
+ }
+
public void setAllowWriteToNewRef(boolean allow) {
isAllowWriteToNewtRef = allow;
}
diff --git a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
new file mode 100644
index 0000000..212aa37
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2018 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.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
+import static org.eclipse.jgit.util.RawParseUtils.decode;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Optional;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Deletes a change message from NoteDb by rewriting the commit history. After deletion, the whole
+ * change message will be replaced by a new message indicating the original change message has been
+ * deleted for the given reason.
+ */
+public class DeleteChangeMessageRewriter implements NoteDbRewriter {
+
+ private final Change.Id changeId;
+ private final int targetMessageIdx;
+ private final String newChangeMessage;
+
+ DeleteChangeMessageRewriter(Change.Id changeId, int targetMessageIdx, String newChangeMessage) {
+ this.changeId = changeId;
+ this.targetMessageIdx = targetMessageIdx;
+ this.newChangeMessage = newChangeMessage;
+ }
+
+ @Override
+ public String getRefName() {
+ return RefNames.changeMetaRef(changeId);
+ }
+
+ @Override
+ public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
+ throws IOException {
+ checkArgument(!currTip.equals(ObjectId.zeroId()));
+
+ // Walk from the first commit of the branch.
+ revWalk.reset();
+ revWalk.markStart(revWalk.parseCommit(currTip));
+ revWalk.sort(RevSort.TOPO);
+ revWalk.sort(RevSort.REVERSE);
+
+ ObjectId newTipId = null;
+ RevCommit originalCommit;
+ int idx = 0;
+ while ((originalCommit = revWalk.next()) != null) {
+ if (idx < targetMessageIdx) {
+ newTipId = originalCommit;
+ idx++;
+ continue;
+ }
+
+ String newCommitMessage =
+ (idx == targetMessageIdx)
+ ? createNewCommitMessage(originalCommit)
+ : originalCommit.getFullMessage();
+ newTipId = rewriteOneCommit(originalCommit, newTipId, newCommitMessage, inserter);
+
+ idx++;
+ }
+ return newTipId;
+ }
+
+ private String createNewCommitMessage(RevCommit commit) {
+ byte[] raw = commit.getRawBuffer();
+
+ Optional<ChangeNoteUtil.CommitMessageRange> range = parseCommitMessageRange(commit);
+ checkState(range.isPresent(), "failed to parse commit message");
+
+ // Only replace the commit message body, which is the user-provided message. The subject and
+ // footers are NoteDb metadata.
+ Charset encoding = RawParseUtils.parseEncoding(raw);
+ String prefix =
+ decode(encoding, raw, range.get().subjectStart(), range.get().changeMessageStart());
+ String postfix = decode(encoding, raw, range.get().changeMessageEnd() + 1, raw.length);
+ return prefix + newChangeMessage + postfix;
+ }
+
+ /**
+ * Rewrites one commit.
+ *
+ * @param originalCommit the original commit to be rewritten.
+ * @param parentCommitId the parent of the new commit. For the first rewritten commit, it's the
+ * parent of 'originalCommit'. For the latter rewritten commits, it's the commit rewritten
+ * just before it.
+ * @param commitMessage the full commit message of the new commit.
+ * @param inserter the {@code ObjectInserter} for the rewrite process.
+ * @return the {@code objectId} of the new commit.
+ * @throws IOException
+ */
+ private ObjectId rewriteOneCommit(
+ RevCommit originalCommit,
+ ObjectId parentCommitId,
+ String commitMessage,
+ ObjectInserter inserter)
+ throws IOException {
+ CommitBuilder cb = new CommitBuilder();
+ if (parentCommitId != null) {
+ cb.setParentId(parentCommitId);
+ }
+ cb.setTreeId(originalCommit.getTree());
+ cb.setMessage(commitMessage);
+ cb.setCommitter(originalCommit.getCommitterIdent());
+ cb.setAuthor(originalCommit.getAuthorIdent());
+ cb.setEncoding(originalCommit.getEncoding());
+ return inserter.insert(cb);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index b20bc73..fd25059 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -467,14 +467,33 @@
if (rcu != null) {
robotCommentUpdates.put(rcu.getRefName(), rcu);
}
- DeleteCommentRewriter rwt = update.getDeleteCommentRewriter();
- if (rwt != null) {
- // Checks whether there is any ChangeUpdate added earlier trying to update the same ref.
+ DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter();
+ if (deleteCommentRewriter != null) {
+ // Checks whether there is any ChangeUpdate or rewriter added earlier for the same ref.
checkArgument(
- !changeUpdates.containsKey(rwt.getRefName()),
+ !changeUpdates.containsKey(deleteCommentRewriter.getRefName()),
"cannot update & rewrite ref %s in one BatchUpdate",
- rwt.getRefName());
- rewriters.put(rwt.getRefName(), rwt);
+ deleteCommentRewriter.getRefName());
+ checkArgument(
+ !rewriters.containsKey(deleteCommentRewriter.getRefName()),
+ "cannot rewrite the same ref %s in one BatchUpdate",
+ deleteCommentRewriter.getRefName());
+ rewriters.put(deleteCommentRewriter.getRefName(), deleteCommentRewriter);
+ }
+
+ DeleteChangeMessageRewriter deleteChangeMessageRewriter =
+ update.getDeleteChangeMessageRewriter();
+ if (deleteChangeMessageRewriter != null) {
+ // Checks whether there is any ChangeUpdate or rewriter added earlier for the same ref.
+ checkArgument(
+ !changeUpdates.containsKey(deleteChangeMessageRewriter.getRefName()),
+ "cannot update & rewrite ref %s in one BatchUpdate",
+ deleteChangeMessageRewriter.getRefName());
+ checkArgument(
+ !rewriters.containsKey(deleteChangeMessageRewriter.getRefName()),
+ "cannot rewrite the same ref %s in one BatchUpdate",
+ deleteChangeMessageRewriter.getRefName());
+ rewriters.put(deleteChangeMessageRewriter.getRefName(), deleteChangeMessageRewriter);
}
changeUpdates.put(update.getRefName(), update);
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 1f51fda..df80e35 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -146,7 +146,9 @@
} catch (Exception e) {
if (!(e.getCause() instanceof RepositoryNotFoundException)) {
logger.atWarning().withCause(e).log("Cannot read project %s", projectName.get());
- Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
+ if (e.getCause() != null) {
+ Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
+ }
throw new IOException(e);
}
logger.atFine().withCause(e).log("Cannot find project %s", projectName.get());
diff --git a/java/com/google/gerrit/server/restapi/account/Capabilities.java b/java/com/google/gerrit/server/restapi/account/Capabilities.java
index 85f9da0..07b1214 100644
--- a/java/com/google/gerrit/server/restapi/account/Capabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/Capabilities.java
@@ -72,7 +72,7 @@
GlobalOrPluginPermission perm = parse(id);
try {
- permissionBackend.user(target).check(perm);
+ permissionBackend.absentUser(target.getAccountId()).check(perm);
return new AccountResource.Capability(target, globalOrPluginPermissionName(perm));
} catch (AuthException e) {
throw new ResourceNotFoundException(id);
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index d2236fd..7889f6e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -85,7 +85,7 @@
PermissionBackend.WithUser perm = permissionBackend.currentUser();
if (!self.get().hasSameAccountId(resource.getUser())) {
perm.check(GlobalPermission.ADMINISTRATE_SERVER);
- perm = permissionBackend.user(resource.getUser());
+ perm = permissionBackend.absentUser(resource.getUser().getAccountId());
}
Map<String, Object> have = new LinkedHashMap<>();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
new file mode 100644
index 0000000..67c54c2
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2018 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.restapi.change;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.ChangeMessageResource;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+
+/** Deletes a change message by rewriting history. */
+@Singleton
+public class DeleteChangeMessage
+ extends RetryingRestModifyView<
+ ChangeMessageResource, DeleteChangeMessageInput, Response<ChangeMessageInfo>> {
+
+ private final Provider<CurrentUser> userProvider;
+ private final Provider<ReviewDb> dbProvider;
+ private final PermissionBackend permissionBackend;
+ private final ChangeMessagesUtil changeMessagesUtil;
+ private final AccountLoader.Factory accountLoaderFactory;
+ private final ChangeNotes.Factory notesFactory;
+
+ @Inject
+ public DeleteChangeMessage(
+ Provider<CurrentUser> userProvider,
+ Provider<ReviewDb> dbProvider,
+ PermissionBackend permissionBackend,
+ ChangeMessagesUtil changeMessagesUtil,
+ AccountLoader.Factory accountLoaderFactory,
+ ChangeNotes.Factory notesFactory,
+ RetryHelper retryHelper) {
+ super(retryHelper);
+ this.userProvider = userProvider;
+ this.dbProvider = dbProvider;
+ this.permissionBackend = permissionBackend;
+ this.changeMessagesUtil = changeMessagesUtil;
+ this.accountLoaderFactory = accountLoaderFactory;
+ this.notesFactory = notesFactory;
+ }
+
+ @Override
+ public Response<ChangeMessageInfo> applyImpl(
+ BatchUpdate.Factory updateFactory,
+ ChangeMessageResource resource,
+ DeleteChangeMessageInput input)
+ throws RestApiException, PermissionBackendException, OrmException, UpdateException,
+ IOException {
+ CurrentUser user = userProvider.get();
+ permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+
+ String newChangeMessage =
+ createNewChangeMessage(user.asIdentifiedUser().getName(), input.reason);
+ DeleteChangeMessageOp deleteChangeMessageOp =
+ new DeleteChangeMessageOp(resource.getChangeMessageIndex(), newChangeMessage);
+ try (BatchUpdate batchUpdate =
+ updateFactory.create(
+ dbProvider.get(), resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
+ batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
+ }
+
+ ChangeMessageInfo updatedMessageInfo =
+ createUpdatedChangeMessageInfo(resource.getChangeId(), resource.getChangeMessageIndex());
+ return Response.created(updatedMessageInfo);
+ }
+
+ private ChangeMessageInfo createUpdatedChangeMessageInfo(Change.Id id, int targetIdx)
+ throws OrmException, PermissionBackendException {
+ List<ChangeMessage> messages =
+ changeMessagesUtil.byChange(dbProvider.get(), notesFactory.createChecked(id));
+ ChangeMessage updatedChangeMessage = messages.get(targetIdx);
+ AccountLoader accountLoader = accountLoaderFactory.create(true);
+ ChangeMessageInfo info = createChangeMessageInfo(updatedChangeMessage, accountLoader);
+ accountLoader.fill();
+ return info;
+ }
+
+ @VisibleForTesting
+ public static String createNewChangeMessage(String deletedBy, @Nullable String deletedReason) {
+ checkNotNull(deletedBy, "user name must not be null");
+
+ if (Strings.isNullOrEmpty(deletedReason)) {
+ return createNewChangeMessage(deletedBy);
+ }
+ return String.format("Change message removed by: %s\nReason: %s", deletedBy, deletedReason);
+ }
+
+ @VisibleForTesting
+ public static String createNewChangeMessage(String deletedBy) {
+ checkNotNull(deletedBy, "user name must not be null");
+
+ return "Change message removed by: " + deletedBy;
+ }
+
+ private class DeleteChangeMessageOp implements BatchUpdateOp {
+ private final int targetMessageIdx;
+ private final String newMessage;
+
+ DeleteChangeMessageOp(int targetMessageIdx, String newMessage) {
+ this.targetMessageIdx = targetMessageIdx;
+ this.newMessage = newMessage;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException {
+ PatchSet.Id psId = ctx.getChange().currentPatchSetId();
+ changeMessagesUtil.replaceChangeMessage(
+ ctx.getDb(), ctx.getUpdate(psId), targetMessageIdx, newMessage);
+ return true;
+ }
+ }
+
+ @Singleton
+ public static class DefaultDeleteChangeMessage
+ extends RetryingRestModifyView<ChangeMessageResource, Input, Response<ChangeMessageInfo>> {
+ private final DeleteChangeMessage deleteChangeMessage;
+
+ @Inject
+ public DefaultDeleteChangeMessage(
+ DeleteChangeMessage deleteChangeMessage, RetryHelper retryHelper) {
+ super(retryHelper);
+ this.deleteChangeMessage = deleteChangeMessage;
+ }
+
+ @Override
+ protected Response<ChangeMessageInfo> applyImpl(
+ BatchUpdate.Factory updateFactory, ChangeMessageResource resource, Input input)
+ throws Exception {
+ return deleteChangeMessage.applyImpl(updateFactory, resource, new DeleteChangeMessageInput());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 5b28971..5a5d2bb 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -182,6 +182,8 @@
child(CHANGE_KIND, "messages").to(ChangeMessages.class);
get(CHANGE_MESSAGE_KIND).to(GetChangeMessage.class);
+ delete(CHANGE_MESSAGE_KIND).to(DeleteChangeMessage.DefaultDeleteChangeMessage.class);
+ post(CHANGE_MESSAGE_KIND, "delete").to(DeleteChangeMessage.class);
factory(AccountLoader.Factory.class);
factory(ChangeInserter.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 20f3d8a..9bbaf69 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -33,6 +33,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
+import com.google.common.flogger.FluentLogger;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.Nullable;
@@ -145,6 +146,8 @@
@Singleton
public class PostReview
extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
public static final String ERROR_ONLY_OWNER_CAN_MODIFY_WORK_IN_PROGRESS =
"only change owner can specify work_in_progress or ready";
@@ -1216,8 +1219,15 @@
}
forceCallerAsReviewer(projectState, ctx, current, ups, del);
- ctx.getDb().patchSetApprovals().delete(del);
- ctx.getDb().patchSetApprovals().upsert(ups);
+
+ if (!del.isEmpty()) {
+ ctx.getDb().patchSetApprovals().delete(del);
+ }
+
+ if (!ups.isEmpty()) {
+ ctx.getDb().patchSetApprovals().upsert(ups);
+ }
+
return !del.isEmpty() || !ups.isEmpty();
}
@@ -1303,8 +1313,15 @@
if (del.isEmpty()) {
// If no existing label is being set to 0, hack in the caller
// as a reviewer by picking the first server-wide LabelType.
- LabelId labelId =
- projectState.getLabelTypes(ctx.getNotes()).getLabelTypes().get(0).getLabelId();
+ List<LabelType> labelTypes = projectState.getLabelTypes(ctx.getNotes()).getLabelTypes();
+ if (labelTypes.isEmpty()) {
+ logger.atWarning().log(
+ "no label type found for project %s, change %s",
+ projectState.getName(), ctx.getChange().getChangeId());
+ return;
+ }
+
+ LabelId labelId = labelTypes.get(0).getLabelId();
PatchSetApproval c = ApprovalsUtil.newApproval(psId, user, labelId, 0, ctx.getWhen());
c.setTag(in.tag);
c.setGranted(ctx.getWhen());
diff --git a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
index 53411b8..09f973b 100644
--- a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
@@ -14,34 +14,46 @@
package com.google.gerrit.server.restapi.project;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
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.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.change.FileInfoJson;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.CommitResource;
import com.google.gerrit.server.project.FileResource;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.kohsuke.args4j.Option;
@Singleton
public class FilesInCommitCollection implements ChildCollection<CommitResource, FileResource> {
private final DynamicMap<RestView<FileResource>> views;
+ private final Provider<ListFiles> list;
private final GitRepositoryManager repoManager;
@Inject
FilesInCommitCollection(
- DynamicMap<RestView<FileResource>> views, GitRepositoryManager repoManager) {
+ DynamicMap<RestView<FileResource>> views,
+ Provider<ListFiles> list,
+ GitRepositoryManager repoManager) {
this.views = views;
+ this.list = list;
this.repoManager = repoManager;
}
@Override
public RestView<CommitResource> list() throws ResourceNotFoundException {
- throw new ResourceNotFoundException();
+ return list.get();
}
@Override
@@ -57,4 +69,32 @@
public DynamicMap<RestView<FileResource>> views() {
return views;
}
+
+ public static final class ListFiles implements RestReadView<CommitResource> {
+ @Option(name = "--parent", metaVar = "parent-number")
+ int parentNum;
+
+ private final FileInfoJson fileInfoJson;
+
+ @Inject
+ public ListFiles(FileInfoJson fileInfoJson) {
+ this.fileInfoJson = fileInfoJson;
+ }
+
+ @Override
+ public Object apply(CommitResource resource) throws PatchListNotAvailableException {
+ RevCommit commit = resource.getCommit();
+ PatchListKey key;
+
+ if (parentNum > 0) {
+ key =
+ PatchListKey.againstParentNum(
+ parentNum, commit, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
+ } else {
+ key = PatchListKey.againstCommit(null, commit, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
+ }
+
+ return fileInfoJson.toFileInfoMap(resource.getProjectState().getNameKey(), key);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index cb2e306..9cfa272 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -35,6 +35,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -315,7 +316,7 @@
throw new ResourceConflictException("submit rule error: " + record.errorMessage);
case NOT_READY:
- throw new ResourceConflictException(describeLabels(cd, record.labels));
+ throw new ResourceConflictException(describeNotReady(cd, record));
case FORCED:
default:
@@ -336,6 +337,21 @@
return cd.submitRecords(submitRuleOptions(allowClosed));
}
+ private static String describeNotReady(ChangeData cd, SubmitRecord record) throws OrmException {
+ List<String> blockingConditions = new ArrayList<>();
+ if (record.labels != null) {
+ blockingConditions.add(describeLabels(cd, record.labels));
+ }
+ if (record.requirements != null) {
+ record
+ .requirements
+ .stream()
+ .map(SubmitRequirement::fallbackText)
+ .forEach(blockingConditions::add);
+ }
+ return Joiner.on("; ").join(blockingConditions);
+ }
+
private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels)
throws OrmException {
List<String> labelResults = new ArrayList<>();
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index cccf51b..bd9df30 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -15,6 +15,7 @@
package com.google.gerrit.truth;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
@@ -46,7 +47,7 @@
isNotNull();
List<E> list = getActualList();
if (index >= list.size()) {
- fail("has an element at index " + index);
+ failWithoutActual(fact("expected to have element at index", index));
}
return elementAssertThatFunction.apply(list.get(index));
}
diff --git a/java/com/google/gerrit/truth/OptionalSubject.java b/java/com/google/gerrit/truth/OptionalSubject.java
index f24b5da..d91f07b 100644
--- a/java/com/google/gerrit/truth/OptionalSubject.java
+++ b/java/com/google/gerrit/truth/OptionalSubject.java
@@ -14,6 +14,7 @@
package com.google.gerrit.truth;
+import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.DefaultSubject;
@@ -57,7 +58,7 @@
isNotNull();
Optional<T> optional = actual();
if (!optional.isPresent()) {
- fail("has a value");
+ failWithoutActual(fact("expected to have", "value"));
}
}
@@ -65,7 +66,7 @@
isNotNull();
Optional<T> optional = actual();
if (optional.isPresent()) {
- fail("does not have a value");
+ failWithoutActual(fact("expected not to have", "value"));
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
index a86a739..6563de3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
@@ -144,10 +144,7 @@
ImmutableList.of(
RestCall.get("/projects/%s/commits/%s"),
RestCall.get("/projects/%s/commits/%s/in"),
- RestCall.builder(GET, "/projects/%s/commits/%s/files")
- // GET /projects/<project>/branches/<branch>/files is not implemented
- .expectedResponseCode(SC_NOT_FOUND)
- .build(),
+ RestCall.get("/projects/%s/commits/%s/files"),
RestCall.post("/projects/%s/commits/%s/cherrypick"));
/**
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index d0b01b7..790b884 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -11,26 +11,42 @@
// 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.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
+import static com.google.gerrit.server.restapi.change.DeleteChangeMessage.createNewChangeMessage;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.toSet;
+import static org.eclipse.jgit.util.RawParseUtils.decode;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.TestTimeUtil;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.RawParseUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -114,14 +130,85 @@
public void getOneChangeMessage() throws Exception {
int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
List<ChangeMessageInfo> messages = new ArrayList<>(gApi.changes().id(changeNum).get().messages);
-
for (ChangeMessageInfo messageInfo : messages) {
String id = messageInfo.id;
assertThat(gApi.changes().id(changeNum).message(id).get()).isEqualTo(messageInfo);
}
}
+ @Test
+ public void deleteCannotBeAppliedWithoutAdministrateServerCapability() throws Exception {
+ int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
+ setApiUser(user);
+
+ try {
+ deleteOneChangeMessage(changeNum, 0, user, "spam");
+ fail("expected AuthException");
+ } catch (AuthException e) {
+ assertThat(e.getMessage()).isEqualTo("administrate server not permitted");
+ }
+ }
+
+ @Test
+ public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+ int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
+ setApiUser(user);
+ deleteOneChangeMessage(changeNum, 0, user, "spam");
+ }
+
+ @Test
+ public void deleteCannotBeAppliedWithEmptyChangeMessageUuid() throws Exception {
+ String changeId = createChange().getChangeId();
+
+ try {
+ gApi.changes().id(changeId).message("").delete(new DeleteChangeMessageInput("spam"));
+ fail("expected ResourceNotFoundException");
+ } catch (ResourceNotFoundException e) {
+ assertThat(e.getMessage()).isEqualTo("change message not found");
+ }
+ }
+
+ @Test
+ public void deleteCannotBeAppliedWithNonExistingChangeMessageUuid() throws Exception {
+ String changeId = createChange().getChangeId();
+ DeleteChangeMessageInput input = new DeleteChangeMessageInput();
+ String id = "8473b95934b5732ac55d26311a706c9c2bde9941";
+ input.reason = "spam";
+
+ try {
+ gApi.changes().id(changeId).message(id).delete(input);
+ fail("expected ResourceNotFoundException");
+ } catch (ResourceNotFoundException e) {
+ assertThat(e.getMessage()).isEqualTo(String.format("change message %s not found", id));
+ }
+ }
+
+ @Test
+ public void deleteCanBeAppliedWithoutProvidingReason() throws Exception {
+ int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
+ deleteOneChangeMessage(changeNum, 2, admin, "");
+ }
+
+ @Test
+ public void deleteOneChangeMessageTwice() throws Exception {
+ int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
+ // Deletes the second change message twice.
+ deleteOneChangeMessage(changeNum, 1, admin, "reason 1");
+ deleteOneChangeMessage(changeNum, 1, admin, "reason 2");
+ }
+
+ @Test
+ public void deleteMultipleChangeMessages() throws Exception {
+ int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
+ for (int i = 0; i < 7; ++i) {
+ deleteOneChangeMessage(changeNum, i, admin, "reason " + i);
+ }
+ }
+
private int createOneChangeWithMultipleChangeMessagesInHistory() throws Exception {
+ // Creates the following commit history on the meta branch of the test change.
+
setApiUser(user);
// Commit 1: create a change.
PushOneCommit.Result result = createChange();
@@ -158,6 +245,150 @@
gApi.changes().id(changeId).current().review(reviewInput);
}
+ private void deleteOneChangeMessage(
+ int changeNum, int deletedMessageIndex, TestAccount deletedBy, String reason)
+ throws Exception {
+ List<ChangeMessageInfo> messagesBeforeDeletion = gApi.changes().id(changeNum).messages();
+
+ List<CommentInfo> commentsBefore = getChangeSortedComments(changeNum);
+ List<RevCommit> commitsBefore = new ArrayList<>();
+ if (notesMigration.readChanges()) {
+ commitsBefore = getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
+ }
+
+ String id = messagesBeforeDeletion.get(deletedMessageIndex).id;
+ DeleteChangeMessageInput input = new DeleteChangeMessageInput(reason);
+ ChangeMessageInfo info = gApi.changes().id(changeNum).message(id).delete(input);
+
+ // Verify the return change message info is as expect.
+ assertThat(info.message).isEqualTo(createNewChangeMessage(deletedBy.fullName, reason));
+ List<ChangeMessageInfo> messagesAfterDeletion = gApi.changes().id(changeNum).messages();
+ assertMessagesAfterDeletion(
+ messagesBeforeDeletion, messagesAfterDeletion, deletedMessageIndex, deletedBy, reason);
+ assertCommentsAfterDeletion(changeNum, commentsBefore);
+
+ // Verify change index is updated after deletion.
+ List<ChangeInfo> changes = gApi.changes().query("message removed").get();
+ assertThat(changes.stream().map(c -> c._number).collect(toSet())).contains(changeNum);
+
+ // Verifies states of commits if NoteDb is on.
+ if (notesMigration.readChanges()) {
+ assertMetaCommitsAfterDeletion(
+ commitsBefore, changeNum, deletedMessageIndex, deletedBy, reason);
+ }
+ }
+
+ private void assertMessagesAfterDeletion(
+ List<ChangeMessageInfo> messagesBeforeDeletion,
+ List<ChangeMessageInfo> messagesAfterDeletion,
+ int deletedMessageIndex,
+ TestAccount deletedBy,
+ String deleteReason) {
+ assertThat(messagesAfterDeletion)
+ .named("after: %s; before: %s", messagesAfterDeletion, messagesBeforeDeletion)
+ .hasSize(messagesBeforeDeletion.size());
+
+ for (int i = 0; i < messagesAfterDeletion.size(); ++i) {
+ ChangeMessageInfo before = messagesBeforeDeletion.get(i);
+ ChangeMessageInfo after = messagesAfterDeletion.get(i);
+
+ if (i < deletedMessageIndex) {
+ // The uuid of a commit message will be updated after rewriting.
+ assertThat(after.id).isEqualTo(before.id);
+ }
+
+ assertThat(after.tag).isEqualTo(before.tag);
+ assertThat(after.author).isEqualTo(before.author);
+ assertThat(after.realAuthor).isEqualTo(before.realAuthor);
+ assertThat(after._revisionNumber).isEqualTo(before._revisionNumber);
+
+ if (i == deletedMessageIndex) {
+ assertThat(after.message)
+ .isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+ } else {
+ assertThat(after.message).isEqualTo(before.message);
+ }
+ }
+ }
+
+ private void assertMetaCommitsAfterDeletion(
+ List<RevCommit> commitsBeforeDeletion,
+ int changeNum,
+ int deletedMessageIndex,
+ TestAccount deletedBy,
+ String deleteReason)
+ throws Exception {
+ List<RevCommit> commitsAfterDeletion =
+ getChangeMetaCommitsInReverseOrder(new Change.Id(changeNum));
+ assertThat(commitsAfterDeletion).hasSize(commitsBeforeDeletion.size());
+
+ for (int i = 0; i < commitsBeforeDeletion.size(); i++) {
+ RevCommit commitBefore = commitsBeforeDeletion.get(i);
+ RevCommit commitAfter = commitsAfterDeletion.get(i);
+ if (i == deletedMessageIndex) {
+ byte[] rawBefore = commitBefore.getRawBuffer();
+ byte[] rawAfter = commitAfter.getRawBuffer();
+ Charset encodingBefore = RawParseUtils.parseEncoding(rawBefore);
+ Charset encodingAfter = RawParseUtils.parseEncoding(rawAfter);
+ Optional<ChangeNoteUtil.CommitMessageRange> rangeBefore =
+ parseCommitMessageRange(commitBefore);
+ Optional<ChangeNoteUtil.CommitMessageRange> rangeAfter =
+ parseCommitMessageRange(commitAfter);
+ assertThat(rangeBefore.isPresent()).isTrue();
+ assertThat(rangeAfter.isPresent()).isTrue();
+
+ String subjectBefore =
+ decode(
+ encodingBefore,
+ rawBefore,
+ rangeBefore.get().subjectStart(),
+ rangeBefore.get().subjectEnd());
+ String subjectAfter =
+ decode(
+ encodingAfter,
+ rawAfter,
+ rangeAfter.get().subjectStart(),
+ rangeAfter.get().subjectEnd());
+ assertThat(subjectBefore).isEqualTo(subjectAfter);
+
+ String footersBefore =
+ decode(
+ encodingBefore,
+ rawBefore,
+ rangeBefore.get().changeMessageEnd() + 1,
+ rawBefore.length);
+ String footersAfter =
+ decode(
+ encodingAfter, rawAfter, rangeAfter.get().changeMessageEnd() + 1, rawAfter.length);
+ assertThat(footersBefore).isEqualTo(footersAfter);
+
+ String message =
+ decode(
+ encodingAfter,
+ rawAfter,
+ rangeAfter.get().changeMessageStart(),
+ rangeAfter.get().changeMessageEnd() + 1);
+ assertThat(message).isEqualTo(createNewChangeMessage(deletedBy.fullName, deleteReason));
+ } else {
+ assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage());
+ }
+
+ assertThat(commitAfter.getCommitterIdent().getName())
+ .isEqualTo(commitBefore.getCommitterIdent().getName());
+ assertThat(commitAfter.getAuthorIdent().getName())
+ .isEqualTo(commitBefore.getAuthorIdent().getName());
+ assertThat(commitAfter.getEncoding()).isEqualTo(commitBefore.getEncoding());
+ assertThat(commitAfter.getEncodingName()).isEqualTo(commitBefore.getEncodingName());
+ }
+ }
+
+ /** Verifies comments are not changed after deleting change message(s). */
+ private void assertCommentsAfterDeletion(int changeNum, List<CommentInfo> commentsBeforeDeletion)
+ throws Exception {
+ List<CommentInfo> commentsAfterDeletion = getChangeSortedComments(changeNum);
+ assertThat(commentsAfterDeletion).containsExactlyElementsIn(commentsBeforeDeletion).inOrder();
+ }
+
private static void assertMessage(String expected, String actual) {
assertThat(actual).isEqualTo("Patch Set 1:\n\n" + expected);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListCommitFilesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListCommitFilesIT.java
new file mode 100644
index 0000000..3ac2d10
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListCommitFilesIT.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2018 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.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.Map;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class ListCommitFilesIT extends AbstractDaemonTest {
+ private static String SUBJECT_1 = "subject 1";
+ private static String SUBJECT_2 = "subject 2";
+ private static String FILE_A = "a.txt";
+ private static String FILE_B = "b.txt";
+
+ @Test
+ public void listCommitFiles() throws Exception {
+ commitBuilder().add(FILE_B, "2").message(SUBJECT_1).create();
+ pushHead(testRepo, "refs/heads/master", false);
+
+ RevCommit a = commitBuilder().add(FILE_A, "1").rm(FILE_B).message(SUBJECT_2).create();
+ String id = getChangeId(testRepo, a).get();
+ pushHead(testRepo, "refs/for/master", false);
+
+ RestResponse r =
+ userRestSession.get("/projects/" + project.get() + "/commits/" + a.name() + "/files/");
+ r.assertOK();
+ Type type = new TypeToken<Map<String, FileInfo>>() {}.getType();
+ Map<String, FileInfo> files1 = newGson().fromJson(r.getReader(), type);
+ r.consume();
+
+ r = userRestSession.get("/changes/" + id + "/revisions/" + a.name() + "/files");
+ r.assertOK();
+ Map<String, FileInfo> files2 = newGson().fromJson(r.getReader(), type);
+ r.consume();
+
+ assertThat(files1).isEqualTo(files2);
+ }
+
+ @Test
+ public void listMergeCommitFiles() throws Exception {
+ PushOneCommit.Result result = createMergeCommitChange("refs/for/master");
+
+ RestResponse r =
+ userRestSession.get(
+ "/projects/"
+ + project.get()
+ + "/commits/"
+ + result.getCommit().name()
+ + "/files/?parent=2");
+ r.assertOK();
+ Type type = new TypeToken<Map<String, FileInfo>>() {}.getType();
+ Map<String, FileInfo> files1 = newGson().fromJson(r.getReader(), type);
+ r.consume();
+
+ r =
+ userRestSession.get(
+ "/changes/"
+ + result.getChangeId()
+ + "/revisions/"
+ + result.getCommit().name()
+ + "/files/?parent=2");
+ r.assertOK();
+ Map<String, FileInfo> files2 = newGson().fromJson(r.getReader(), type);
+ r.consume();
+
+ assertThat(files1).isEqualTo(files2);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 211af59..8b1f4bc 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -47,7 +47,6 @@
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -58,26 +57,21 @@
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;
@@ -838,7 +832,7 @@
CommentInput c9 = newComment("b.txt", "comment 9");
addComments(changeId, ps2, c9);
- List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(changeId);
+ List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
assertThat(commentsBeforeDelete).hasSize(9);
// PS1 has comments [c1, c2, c3, c4, c5].
assertThat(getRevisionComments(changeId, ps1)).hasSize(5);
@@ -853,7 +847,7 @@
for (int i = 0; i < commentsBeforeDelete.size(); i++) {
List<RevCommit> commitsBeforeDelete = new ArrayList<>();
if (notesMigration.commitChangeWrites()) {
- commitsBeforeDelete = getCommits(id);
+ commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
}
CommentInfo comment = commentsBeforeDelete.get(i);
@@ -879,7 +873,7 @@
comment.message = expectedMsg;
commentsBeforeDelete.set(i, comment);
- List<CommentInfo> commentsAfterDelete = getChangeSortedComments(changeId);
+ List<CommentInfo> commentsAfterDelete = getChangeSortedComments(id.get());
assertThat(commentsAfterDelete).isEqualTo(commentsBeforeDelete);
}
@@ -893,7 +887,7 @@
addComments(changeId, ps3, c12);
addComments(changeId, ps4, c13);
- assertThat(getChangeSortedComments(changeId)).hasSize(13);
+ assertThat(getChangeSortedComments(id.get())).hasSize(13);
assertThat(getRevisionComments(changeId, ps1)).hasSize(6);
assertThat(getRevisionComments(changeId, ps2)).hasSize(3);
assertThat(getRevisionComments(changeId, ps3)).hasSize(1);
@@ -914,7 +908,7 @@
addComments(changeId, ps1, c2);
addComments(changeId, ps1, c3);
- List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(changeId);
+ List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
assertThat(commentsBeforeDelete).hasSize(3);
Optional<CommentInfo> targetComment =
commentsBeforeDelete.stream().filter(c -> c.message.equals("comment 2")).findFirst();
@@ -924,7 +918,7 @@
List<RevCommit> commitsBeforeDelete = new ArrayList<>();
if (notesMigration.commitChangeWrites()) {
- commitsBeforeDelete = getCommits(id);
+ commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
}
setApiUser(admin);
@@ -944,7 +938,7 @@
if (notesMigration.commitChangeWrites()) {
assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
}
- assertThat(getChangeSortedComments(changeId)).hasSize(3);
+ assertThat(getChangeSortedComments(id.get())).hasSize(3);
}
@Test
@@ -962,19 +956,6 @@
assertThat(comment.legacyFormat).isFalse();
}
- private List<CommentInfo> getChangeSortedComments(String changeId) throws Exception {
- List<CommentInfo> comments = new ArrayList<>();
- Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId);
- for (Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) {
- for (CommentInfo c : e.getValue()) {
- c.path = e.getKey(); // Set the comment's path field.
- comments.add(c);
- }
- }
- comments.sort(Comparator.comparing(c -> c.id));
- return comments;
- }
-
private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
return getPublishedComments(changeId, revId)
.values()
@@ -998,15 +979,6 @@
gApi.changes().id(changeId).revision(revision).review(input);
}
- private List<RevCommit> getCommits(Change.Id changeId) throws IOException {
- try (Repository repo = repoManager.openRepository(project);
- RevWalk revWalk = new RevWalk(repo)) {
- Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId));
- revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId()));
- return Lists.newArrayList(revWalk);
- }
- }
-
/**
* All the commits, which contain the target comment before, should still contain the comment with
* the updated message. All the other metas of the commits should be exactly the same.
@@ -1017,7 +989,7 @@
String targetCommentUuid,
String expectedMessage)
throws Exception {
- List<RevCommit> afterDelete = getCommits(changeId);
+ List<RevCommit> afterDelete = getChangeMetaCommitsInReverseOrder(changeId);
assertThat(afterDelete).hasSize(beforeDelete.size());
try (Repository repo = repoManager.openRepository(project);
@@ -1119,10 +1091,6 @@
return gApi.changes().id(changeId).revision(revId).drafts();
}
- private Map<String, List<CommentInfo>> getPublishedComments(String changeId) throws Exception {
- return gApi.changes().id(changeId).comments();
- }
-
private CommentInfo getDraftComment(String changeId, String revId, String uuid) throws Exception {
return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 988b051..a0170d2 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.mail.EmailHeader;
import com.google.gerrit.mail.MailProcessingUtil;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.TestTimeUtil;
import java.sql.Timestamp;
@@ -63,7 +64,7 @@
assertThat(emails).hasSize(1);
FakeEmailSender.Message message = emails.get(0);
- String changeURL = "<" + canonicalWebUrl.get() + newChange.getChange().getId().get() + ">";
+ String changeURL = "<" + getChangeUrl(newChange.getChange()) + ">";
Map<String, Object> expectedHeaders = new HashMap<>();
expectedHeaders.put("Gerrit-PatchSet", "1");
@@ -100,7 +101,7 @@
assertThat(emails).hasSize(1);
FakeEmailSender.Message message = emails.get(0);
- String changeURL = "<" + canonicalWebUrl.get() + newChange.getChange().getId().get() + ">";
+ String changeURL = "<" + getChangeUrl(newChange.getChange()) + ">";
Map<String, Object> expectedHeaders = new HashMap<>();
expectedHeaders.put("Gerrit-PatchSet", "1");
expectedHeaders.put(
@@ -159,4 +160,12 @@
}
}
}
+
+ private String getChangeUrl(ChangeData changeData) {
+ return canonicalWebUrl.get()
+ + "c/"
+ + changeData.project().get()
+ + "/+/"
+ + changeData.getId().get();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index b9f30d6..9ff2c05 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -46,12 +46,7 @@
// Build Message
MailMessage.Builder b = messageBuilderWithDefaultFields();
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- "Test Message",
- null,
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
mailProcessor.process(b.build());
@@ -74,12 +69,7 @@
// Build Message
MailMessage.Builder b = messageBuilderWithDefaultFields();
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- null,
- "Some Inline Comment",
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, "Some Inline Comment", null, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
mailProcessor.process(b.build());
@@ -111,11 +101,7 @@
MailMessage.Builder b = messageBuilderWithDefaultFields();
String txt =
newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- null,
- null,
- "Some Comment on File 1",
- null);
+ getChangeUrl(changeInfo) + "/1", null, null, "Some Comment on File 1", null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
mailProcessor.process(b.build());
@@ -147,12 +133,7 @@
// Build Message
MailMessage.Builder b = messageBuilderWithDefaultFields();
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- null,
- "Some Inline Comment",
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, "Some Inline Comment", null, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
mailProcessor.process(b.build());
@@ -178,12 +159,7 @@
// Build Message
MailMessage.Builder b = messageBuilderWithDefaultFields();
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- null,
- "Some Inline Comment",
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, "Some Inline Comment", null, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
// Set account state to inactive
@@ -211,12 +187,7 @@
// Build Message
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- "Test Message",
- null,
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
MailMessage.Builder b =
messageBuilderWithDefaultFields()
.from(user.emailAddress)
@@ -238,12 +209,7 @@
// Build Message
String txt =
- newPlaintextBody(
- canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
- "Test Message",
- null,
- null,
- null);
+ newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null, null);
MailMessage.Builder b =
messageBuilderWithDefaultFields()
.from(user.emailAddress)
@@ -257,4 +223,8 @@
assertThat(message.body()).contains("was unable to parse your email");
assertThat(message.headers()).containsKey("Subject");
}
+
+ private String getChangeUrl(ChangeInfo changeInfo) {
+ return canonicalWebUrl.get() + "c/" + changeInfo.project + "/+/" + changeInfo._number;
+ }
}
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
index b5b942d..2fc06a6 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import com.google.common.collect.Iterators;
import com.google.gerrit.gpg.testing.TestKey;
import java.util.ArrayList;
import java.util.Arrays;
@@ -163,6 +164,8 @@
TestKey key5 = validKeyWithSecondUserId();
PGPPublicKeyRing keyRing = key5.getPublicKeyRing();
PGPPublicKey key = keyRing.getPublicKey();
+ PGPPublicKey subKey =
+ keyRing.getPublicKey(Iterators.get(keyRing.getPublicKeys(), 1).getKeyID());
store.add(keyRing);
assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
@@ -171,9 +174,11 @@
"Testuser Five <test5@example.com>",
"foo:myId");
+ keyRing = PGPPublicKeyRing.removePublicKey(keyRing, subKey);
keyRing = PGPPublicKeyRing.removePublicKey(keyRing, key);
key = PGPPublicKey.removeCertification(key, "foo:myId");
keyRing = PGPPublicKeyRing.insertPublicKey(keyRing, key);
+ keyRing = PGPPublicKeyRing.insertPublicKey(keyRing, subKey);
store.add(keyRing);
assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index 5219ce8..6ab4ca2 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -26,7 +26,8 @@
@Ignore
public class AbstractParserTest {
- protected static final String CHANGE_URL = "https://gerrit-review.googlesource.com/#/changes/123";
+ protected static final String CHANGE_URL =
+ "https://gerrit-review.googlesource.com/c/project/+/123";
protected static void assertChangeMessage(String message, MailComment comment) {
assertThat(comment.fileName).isNull();
diff --git a/javatests/com/google/gerrit/mail/TextParserTest.java b/javatests/com/google/gerrit/mail/TextParserTest.java
index e11321a..a5c2152 100644
--- a/javatests/com/google/gerrit/mail/TextParserTest.java
+++ b/javatests/com/google/gerrit/mail/TextParserTest.java
@@ -23,7 +23,7 @@
public class TextParserTest extends AbstractParserTest {
private static final String quotedFooter =
""
- + "> To view, visit https://gerrit-review.googlesource.com/123\n"
+ + "> To view, visit https://gerrit-review.googlesource.com/c/project/+/123\n"
+ "> To unsubscribe, visit https://gerrit-review.googlesource.com\n"
+ "> \n"
+ "> Gerrit-MessageType: comment\n"
diff --git a/lib/truth/BUILD b/lib/truth/BUILD
index 82cd98a..db5bc48 100644
--- a/lib/truth/BUILD
+++ b/lib/truth/BUILD
@@ -4,6 +4,7 @@
visibility = ["//visibility:public"],
exports = ["@truth//jar"],
runtime_deps = [
+ ":diffutils",
"//lib:guava",
"//lib:junit",
],
@@ -33,6 +34,13 @@
)
java_library(
+ name = "diffutils",
+ data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+ visibility = ["//visibility:private"],
+ exports = ["@diffutils//jar"],
+)
+
+java_library(
name = "truth-proto-extension",
data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
visibility = ["//visibility:public"],
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
index d18e442..ff9b54a 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
@@ -30,7 +30,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index dc98b59..4d6913b 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -63,7 +63,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
@@ -74,7 +74,7 @@
'Subject',
'Status',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Size',
];
@@ -83,13 +83,13 @@
});
test('isColumnHidden', () => {
- const columnToCheck = 'Project';
+ const columnToCheck = 'Repo';
let columnsToDisplay = [
'Subject',
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index eab1b01..6890d53 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -169,21 +169,21 @@
<span class="placeholder">--</span>
</template>
</td>
- <td class="cell project"
- hidden$="[[isColumnHidden('Project', visibleChangeTableColumns)]]">
- <a class="fullProject" href$="[[_computeProjectURL(change)]]">
- [[_computeProjectDisplay(change)]]
+ <td class="cell repo"
+ hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]">
+ <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
+ [[_computeRepoDisplay(change)]]
</a>
<a
- class="truncatedProject"
- href$="[[_computeProjectURL(change)]]"
- title$="[[_computeProjectDisplay(change)]]">
- [[_computeProjectDisplay(change, 'true')]]
+ class="truncatedRepo"
+ href$="[[_computeRepoUrl(change)]]"
+ title$="[[_computeRepoDisplay(change)]]">
+ [[_computeRepoDisplay(change, 'true')]]
</a>
</td>
<td class="cell branch"
hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
- <a href$="[[_computeProjectBranchURL(change)]]">
+ <a href$="[[_computeRepoBranchURL(change)]]">
[[change.branch]]
</a>
<template is="dom-if" if="[[change.topic]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 3f5a402..65fe9ea 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -131,12 +131,12 @@
return '';
},
- _computeProjectURL(change) {
+ _computeRepoUrl(change) {
return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
change.internalHost);
},
- _computeProjectBranchURL(change) {
+ _computeRepoBranchURL(change) {
return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
change.internalHost);
},
@@ -155,7 +155,7 @@
* truncated. If this value is truthy, the name will be truncated.
* @return {string}
*/
- _computeProjectDisplay(change, truncate) {
+ _computeRepoDisplay(change, truncate) {
if (!change || !change.project) { return ''; }
let str = '';
if (change.internalHost) { str += change.internalHost + '/'; }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index f4c66c7..aaad362 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -125,7 +125,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
@@ -135,31 +135,13 @@
for (const column of element.columnNames) {
const elementClass = '.' + column.toLowerCase();
+ assert.isOk(element.$$(elementClass),
+ `Expect ${elementClass} element to be found`);
assert.isFalse(element.$$(elementClass).hidden);
}
});
- test('no hidden columns', () => {
- element.visibleChangeTableColumns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Project',
- 'Branch',
- 'Updated',
- 'Size',
- ];
-
- flushAsynchronousOperations();
-
- for (const column of element.columnNames) {
- const elementClass = '.' + column.toLowerCase();
- assert.isFalse(element.$$(elementClass).hidden);
- }
- });
-
- test('project column hidden', () => {
+ test('repo column hidden', () => {
element.visibleChangeTableColumns = [
'Subject',
'Status',
@@ -174,7 +156,7 @@
for (const column of element.columnNames) {
const elementClass = '.' + column.toLowerCase();
- if (column === 'Project') {
+ if (column === 'Repo') {
assert.isTrue(element.$$(elementClass).hidden);
} else {
assert.isFalse(element.$$(elementClass).hidden);
@@ -274,17 +256,17 @@
[change.topic, change.internalHost]);
});
- test('_computeProjectDisplay', () => {
+ test('_computeRepoDisplay', () => {
const change = {
project: 'a/test/repo',
internalHost: 'host',
};
- assert.equal(element._computeProjectDisplay(change), 'host/a/test/repo');
- assert.equal(element._computeProjectDisplay(change, true),
+ assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
'host/…/test/repo');
delete change.internalHost;
- assert.equal(element._computeProjectDisplay(change), 'a/test/repo');
- assert.equal(element._computeProjectDisplay(change, true),
+ assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
'…/test/repo');
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index ec581b6..3cf5f9a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -310,7 +310,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
@@ -349,10 +349,10 @@
flushAsynchronousOperations();
});
- test('all columns except project visible', () => {
+ test('all columns except repo visible', () => {
for (const column of element.changeTableColumns) {
const elementClass = '.' + column.toLowerCase();
- if (column === 'Project') {
+ if (column === 'Repo') {
assert.isTrue(element.$$(elementClass).hidden);
} else {
assert.isFalse(element.$$(elementClass).hidden);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 2801a7c..8413388 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -192,7 +192,7 @@
</section>
</template>
<section>
- <span class="title">Project</span>
+ <span class="title">Repo</span>
<span class="value">
<a href$="[[_computeProjectURL(change.project)]]">
<gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index e635d23..8ec00dd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -65,7 +65,8 @@
if (change.requirements) {
for (const requirement of change.requirements) {
requirement.satisfied = requirement.status === 'OK';
- requirement.style = this._computeRequirementClass(requirement);
+ requirement.style =
+ this._computeRequirementClass(requirement.satisfied);
_requirements.push(requirement);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index 93b2c36..3f35158 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -170,5 +170,53 @@
assert.equal(requirement.querySelector('.name').text,
'Resolve all comments');
});
+
+ test('satisfied class is applied with OK', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.$$('.requirement');
+ assert.ok(requirement);
+ assert.ok(requirement.querySelector('.approved'));
+ });
+
+ test('satisfied class is not applied with NOT_READY', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'NOT_READY',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.$$('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
+
+ test('satisfied class is not applied with RULE_ERROR', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'RULE_ERROR',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.$$('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index a3fc06f..7d8bcf5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -39,8 +39,8 @@
const defaultTheme = config.default_theme;
const pluginsPending = []
.concat(jsPlugins, htmlPlugins, defaultTheme || [])
- .filter(p => !Gerrit._isPluginPreloaded(p))
- .map(p => this._urlFor(p));
+ .map(p => this._urlFor(p))
+ .filter(p => !Gerrit._isPluginPreloaded(p));
Gerrit._setPluginsPending(pluginsPending);
if (defaultTheme) {
// Make theme first to be first to load.
@@ -96,8 +96,9 @@
},
_urlFor(pathOrUrl) {
- if (pathOrUrl.startsWith('http')) {
- // Plugins are loaded from another domain.
+ if (pathOrUrl.startsWith('preloaded:') ||
+ pathOrUrl.startsWith('http')) {
+ // Plugins are loaded from another domain or preloaded.
return pathOrUrl;
}
if (!pathOrUrl.startsWith('/')) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index d9def78..a5b3c3e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -190,8 +190,8 @@
test('skips preloaded plugins', () => {
sandbox.stub(Gerrit, '_isPluginPreloaded')
- .withArgs('plugins/foo/bar').returns(true)
- .withArgs('plugins/42').returns(true);
+ .withArgs(url + '/plugins/foo/bar').returns(true)
+ .withArgs(url + '/plugins/42').returns(true);
sandbox.stub(Gerrit, '_setPluginsCount');
sandbox.stub(Gerrit, '_setPluginsPending');
element.config = {
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index daf9437..587cc3b 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -47,7 +47,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
];
@@ -90,7 +90,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
]);
@@ -151,7 +151,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
]);
@@ -163,7 +163,7 @@
'Status',
'Owner',
'Assignee',
- 'Project',
+ 'Repo',
'Branch',
'Updated',
'Size',
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 52649db..7846b7a 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -49,7 +49,7 @@
<table id="watchedProjects">
<thead>
<tr>
- <th class="projectHeader">Project</th>
+ <th>Repo</th>
<template is="dom-repeat" items="[[_getTypes()]]">
<th class="notifType">[[item.name]]</th>
</template>
@@ -98,7 +98,7 @@
id="newProject"
query="[[_query]]"
threshold="1"
- placeholder="Project"></gr-autocomplete>
+ placeholder="Repo"></gr-autocomplete>
</th>
<th colspan$="[[_getTypeCount()]]">
<input
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index bc41b3e..c1d0936 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -412,6 +412,14 @@
element.EventType.ADMIN_MENU_LINKS);
});
+ test('Gerrit._isPluginPreloaded', () => {
+ Gerrit._preloadedPlugins = {baz: ()=>{}};
+ assert.isFalse(Gerrit._isPluginPreloaded('plugins/foo/bar'));
+ assert.isFalse(Gerrit._isPluginPreloaded('http://a.com/42'));
+ assert.isTrue(Gerrit._isPluginPreloaded('preloaded:baz'));
+ Gerrit._preloadedPlugins = null;
+ });
+
test('preloaded plugins are installed', () => {
const installStub = sandbox.stub();
Gerrit._preloadedPlugins = {foo: installStub};
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 6d7469b..37e8bdc 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -116,7 +116,7 @@
.updated,
.size,
.status,
- .project {
+ .repo {
white-space: nowrap;
}
.star {
@@ -136,7 +136,7 @@
.topHeader .label {
border: none;
}
- .truncatedProject {
+ .truncatedRepo {
display: none;
}
@media only screen and (max-width: 150em) {
@@ -147,10 +147,10 @@
max-width: 18rem;
text-overflow: ellipsis;
}
- .truncatedProject {
+ .truncatedRepo {
display: inline-block;
}
- .fullProject {
+ .fullRepo {
display: none;
}
}
@@ -186,7 +186,7 @@
.topHeader,
.leftPadding,
.status,
- .project,
+ .repo,
.branch,
.updated,
.label,