Merge "doc: add option and example section to ssh index command"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ef164bc..91bbbab 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -753,9 +753,6 @@
Deletes the topic of a change.
-The request body does not need to include a link:#topic-input[
-TopicInput] entity if no review comment is added.
-
Please note that some proxies prohibit request bodies for DELETE
requests. In this case, if you want to specify a commit message, use
link:#set-topic[PUT] to delete the topic.
@@ -2820,6 +2817,73 @@
A review cannot be set on a change edit. Trying to post a review for a
change edit fails with `409 Conflict`.
+It is also possible to add one or more reviewers to a change simultaneously
+with a review.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "message": "Looks good to me, but Jane and John should also take a look.",
+ "labels": {
+ "Code-Review": 1
+ },
+ "reviewers": [
+ {
+ "reviewer": "jane.roe@example.com"
+ },
+ {
+ "reviewer": "john.doe@example.com"
+ }
+ ]
+ }
+----
+
+Each element of the `reviewers` list is an instance of
+link:#reviewer-input[ReviewerInput]. The corresponding result of
+adding each reviewer will be returned in a list of
+link:#add-reviewer-result[AddReviewerResult]. If there are any
+errors returned for reviewers, the entire review request will
+be rejected.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "labels": {
+ "Code-Review": 1
+ },
+ "reviewers": [
+ {
+ "input": "jane.roe@example.com",
+ "approvals": {
+ "Verified": " 0",
+ "Code-Review": " 0"
+ },
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com"
+ },
+ {
+ "input": "john.doe@example.com",
+ "approvals": {
+ "Verified": " 0",
+ "Code-Review": " 0"
+ },
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ }
+ ]
+ }
+----
+
[[rebase-revision]]
=== Rebase Revision
--
@@ -3518,6 +3582,11 @@
in the path name. This is useful to implement suggestion services
finding a file by partial name.
+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.
+
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0
@@ -3763,6 +3832,11 @@
The `base` parameter can be specified to control the base patch set from which the diff should
be generated.
+The integer-valued request parameter `parent` can be specified to control the
+parent commit number against which the diff should be generated. 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.
+
[[weblinks-only]]
If the `weblinks-only` parameter is specified, only the diff web links are returned.
@@ -4060,6 +4134,11 @@
|`reviewers` |optional|
The newly added reviewers as a list of link:#reviewer-info[
ReviewerInfo] entities.
+|`ccs` |optional|
+The newly CCed accounts as a list of link:#reviewer-info[
+ReviewerInfo] entities. This field will only appear if the requested
+`state` for the reviewer was `CC` *and* NoteDb is enabled on the
+server.
|`error` |optional|
Error message explaining why the reviewer could not be added. +
If a group was specified in the input and an error is returned, it
@@ -4312,6 +4391,9 @@
The side on which the comment was added. +
Allowed values are `REVISION` and `PARENT`. +
If not set, the default is `REVISION`.
+|`parent` |optional|
+The 1-based parent number. Used only for merge commits when `side == PARENT`.
+When not set the comment is for the auto-merge tree.
|`line` |optional|
The number of the line for which the comment was done. +
If range is set, this equals the end line of the range. +
@@ -4992,6 +5074,9 @@
ID] of one group for which all members should be added as reviewers. +
If an ID identifies both an account and a group, only the account is
added as reviewer to the change.
+|`state` |optional|
+Add reviewer in this state. Possible reviewer states are `REVIEWER`
+and `CC`. If not given, defaults to `REVIEWER`.
|`confirmed` |optional|
Whether adding the reviewer is confirmed. +
The Gerrit server may be configured to
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 14ca11d..b32fccc 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -18,10 +18,13 @@
import static com.google.gerrit.acceptance.GitUtil.initSsh;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.primitives.Chars;
@@ -77,9 +80,9 @@
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.TempFileUtil;
import com.google.gerrit.testutil.TestNotesMigration;
-import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -497,6 +500,29 @@
return result;
}
+ protected PushOneCommit.Result createMergeCommitChange(String ref)
+ throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+ PushOneCommit.Result p1 = pushFactory.create(db, admin.getIdent(),
+ testRepo, "parent 1", ImmutableMap.of("foo", "foo-1", "bar", "bar-1"))
+ .to(ref);
+
+ // reset HEAD in order to create a sibling of the first change
+ testRepo.reset(initial);
+
+ PushOneCommit.Result p2 = pushFactory.create(db, admin.getIdent(),
+ testRepo, "parent 2", ImmutableMap.of("foo", "foo-2", "bar", "bar-2"))
+ .to(ref);
+
+ PushOneCommit m = pushFactory.create(db, admin.getIdent(), testRepo, "merge",
+ ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+ m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
+ PushOneCommit.Result result = m.to(ref);
+ result.assertOkStatus();
+ return result;
+ }
+
protected PushOneCommit.Result createDraftChange() throws Exception {
return pushTo("refs/drafts/master");
}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index a55acf2..c892877 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
@@ -44,6 +45,7 @@
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import java.util.List;
+import java.util.Map;
public class PushOneCommit {
public static final String SUBJECT = "test commit";
@@ -91,6 +93,13 @@
ReviewDb db,
PersonIdent i,
TestRepository<?> testRepo,
+ @Assisted String subject,
+ @Assisted Map<String, String> files);
+
+ PushOneCommit create(
+ ReviewDb db,
+ PersonIdent i,
+ TestRepository<?> testRepo,
@Assisted("subject") String subject,
@Assisted("fileName") String fileName,
@Assisted("content") String content,
@@ -123,8 +132,7 @@
private final TestRepository<?> testRepo;
private final String subject;
- private final String fileName;
- private final String content;
+ private final Map<String, String> files;
private String changeId;
private Tag tag;
private boolean force;
@@ -175,18 +183,43 @@
@Assisted ReviewDb db,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
+ @Assisted String subject,
+ @Assisted Map<String, String> files) throws Exception {
+ this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo,
+ subject, files, null);
+ }
+
+ @AssistedInject
+ PushOneCommit(ChangeNotes.Factory notesFactory,
+ ApprovalsUtil approvalsUtil,
+ Provider<InternalChangeQuery> queryProvider,
+ @Assisted ReviewDb db,
+ @Assisted PersonIdent i,
+ @Assisted TestRepository<?> testRepo,
@Assisted("subject") String subject,
@Assisted("fileName") String fileName,
@Assisted("content") String content,
@Nullable @Assisted("changeId") String changeId) throws Exception {
+ this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo,
+ subject, ImmutableMap.of(fileName, content), changeId);
+ }
+
+ private PushOneCommit(ChangeNotes.Factory notesFactory,
+ ApprovalsUtil approvalsUtil,
+ Provider<InternalChangeQuery> queryProvider,
+ ReviewDb db,
+ PersonIdent i,
+ TestRepository<?> testRepo,
+ String subject,
+ Map<String, String> files,
+ String changeId) throws Exception {
this.db = db;
this.testRepo = testRepo;
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.queryProvider = queryProvider;
this.subject = subject;
- this.fileName = fileName;
- this.content = content;
+ this.files = files;
this.changeId = changeId;
if (changeId != null) {
commitBuilder = testRepo.amendRef("HEAD")
@@ -212,12 +245,16 @@
}
public Result to(String ref) throws Exception {
- commitBuilder.add(fileName, content);
+ for (Map.Entry<String, String> e : files.entrySet()) {
+ commitBuilder.add(e.getKey(), e.getValue());
+ }
return execute(ref);
}
public Result rm(String ref) throws Exception {
- commitBuilder.rm(fileName);
+ for (String fileName : files.keySet()) {
+ commitBuilder.rm(fileName);
+ }
return execute(ref);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 24cbac4..9533f3f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -526,6 +526,35 @@
}
@Test
+ public void filesOnMergeCommitChange() throws Exception {
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+
+ // list files against auto-merge
+ assertThat(gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .files()
+ .keySet()
+ ).containsExactly(Patch.COMMIT_MSG, "foo", "bar");
+
+ // list files against parent 1
+ assertThat(gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .files(1)
+ .keySet()
+ ).containsExactly(Patch.COMMIT_MSG, "bar");
+
+ // list files against parent 2
+ assertThat(gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .files(2)
+ .keySet()
+ ).containsExactly(Patch.COMMIT_MSG, "foo");
+ }
+
+ @Test
public void diff() throws Exception {
PushOneCommit.Result r = createChange();
DiffInfo diff = gApi.changes()
@@ -538,6 +567,48 @@
}
@Test
+ public void diffOnMergeCommitChange() throws Exception {
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+
+ DiffInfo diff;
+
+ // automerge
+ diff = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .file("foo")
+ .diff();
+ assertThat(diff.metaA.lines).isEqualTo(5);
+ assertThat(diff.metaB.lines).isEqualTo(1);
+
+ diff = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .file("bar")
+ .diff();
+ assertThat(diff.metaA.lines).isEqualTo(5);
+ assertThat(diff.metaB.lines).isEqualTo(1);
+
+ // parent 1
+ diff = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .file("bar")
+ .diff(1);
+ assertThat(diff.metaA.lines).isEqualTo(1);
+ assertThat(diff.metaB.lines).isEqualTo(1);
+
+ // parent 2
+ diff = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .file("foo")
+ .diff(2);
+ assertThat(diff.metaA.lines).isEqualTo(1);
+ assertThat(diff.metaB.lines).isEqualTo(1);
+ }
+
+ @Test
public void content() throws Exception {
PushOneCommit.Result r = createChange();
BinaryResult bin = gApi.changes()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index e821608..af13c5d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -64,7 +64,7 @@
String pushedRef = ref;
if (!topic.isEmpty()) {
- pushedRef += "/" + topic;
+ pushedRef += "/" + name(topic);
}
String refspec = "HEAD:" + pushedRef;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 0bd70ef..49fb102 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -15,6 +15,8 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -22,12 +24,20 @@
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gson.stream.JsonReader;
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
public class ChangeReviewersIT extends AbstractDaemonTest {
@@ -40,17 +50,14 @@
int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
- List<TestAccount> users = new ArrayList<>(largeGroupSize);
+ List<TestAccount> users =
+ createAccounts(largeGroupSize, "addGroupAsReviewer");
List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
- List<String> mediumGroupUsernames = new ArrayList<>(mediumGroupSize);
- for (int i = 0; i < largeGroupSize; i++) {
- users.add(accounts.create(name("u" + i), name("u" + i + "@example.com"),
- "Full Name " + i));
- largeGroupUsernames.add(users.get(i).username);
- if (i < mediumGroupSize) {
- mediumGroupUsernames.add(users.get(i).username);
- }
+ for (TestAccount u : users) {
+ largeGroupUsernames.add(u.username);
}
+ List<String> mediumGroupUsernames =
+ largeGroupUsernames.subList(0, mediumGroupSize);
gApi.groups().id(largeGroup).addMembers(
largeGroupUsernames.toArray(new String[largeGroupSize]));
gApi.groups().id(mediumGroup).addMembers(
@@ -84,6 +91,311 @@
assertThat(result.confirm).isNull();
assertThat(result.error).isNull();
assertThat(result.reviewers).hasSize(mediumGroupSize);
+
+ // Verify that group members were added as reviewers.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, notesMigration.readChanges() ? REVIEWER : CC,
+ users.subList(0, mediumGroupSize));
+ }
+
+ @Test
+ public void addCcAccount() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ in.state = CC;
+ AddReviewerResult result = addReviewer(changeId, in);
+
+ assertThat(result.input).isEqualTo(user.email);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertThat(result.reviewers).isNull();
+ assertThat(result.ccs).hasSize(1);
+ AccountInfo ai = result.ccs.get(0);
+ assertThat(ai._accountId).isEqualTo(user.id.get());
+ assertReviewers(c, CC, user);
+ } else {
+ assertThat(result.ccs).isNull();
+ assertThat(result.reviewers).hasSize(1);
+ AccountInfo ai = result.reviewers.get(0);
+ assertThat(ai._accountId).isEqualTo(user.id.get());
+ assertReviewers(c, CC, user);
+ }
+
+ // Verify email was sent to CCed account.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress);
+ if (notesMigration.readChanges()) {
+ assertThat(m.body())
+ .contains(admin.fullName + " has uploaded a new change for review.");
+ } else {
+ assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+ assertThat(m.body()).contains("I'd like you to do a code review.");
+ }
+ }
+
+ @Test
+ public void addCcGroup() throws Exception {
+ List<TestAccount> users = createAccounts(6, "addCcGroup");
+ List<String> usernames = new ArrayList<>(6);
+ for (TestAccount u : users) {
+ usernames.add(u.username);
+ }
+
+ List<TestAccount> firstUsers = users.subList(0, 3);
+ List<String> firstUsernames = usernames.subList(0, 3);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = createGroup("cc1");
+ in.state = CC;
+ gApi.groups().id(in.reviewer)
+ .addMembers(firstUsernames.toArray(new String[firstUsernames.size()]));
+ AddReviewerResult result = addReviewer(changeId, in);
+
+ assertThat(result.input).isEqualTo(in.reviewer);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ if (notesMigration.readChanges()) {
+ assertThat(result.reviewers).isNull();
+ } else {
+ assertThat(result.ccs).isNull();
+ }
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, CC, firstUsers);
+
+ // Verify emails were sent to each of the group's accounts.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
+ for (TestAccount u : firstUsers) {
+ expectedAddresses.add(u.emailAddress);
+ }
+ assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+
+ // CC a group that overlaps with some existing reviewers and CCed accounts.
+ TestAccount reviewer = accounts.create(name("reviewer"),
+ "addCcGroup-reviewer@example.com", "Reviewer");
+ result = addReviewer(changeId, reviewer.username);
+ assertThat(result.error).isNull();
+ sender.clear();
+ in.reviewer = createGroup("cc2");
+ gApi.groups().id(in.reviewer)
+ .addMembers(usernames.toArray(new String[usernames.size()]));
+ gApi.groups().id(in.reviewer).addMembers(reviewer.username);
+ result = addReviewer(changeId, in);
+ assertThat(result.input).isEqualTo(in.reviewer);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertThat(result.ccs).hasSize(3);
+ assertThat(result.reviewers).isNull();
+ assertReviewers(c, REVIEWER, reviewer);
+ assertReviewers(c, CC, users);
+ } else {
+ assertThat(result.ccs).isNull();
+ assertThat(result.reviewers).hasSize(3);
+ List<TestAccount> expectedUsers = new ArrayList<>(users.size() + 2);
+ expectedUsers.addAll(users);
+ expectedUsers.add(reviewer);
+ assertReviewers(c, CC, expectedUsers);
+ }
+
+ messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ m = messages.get(0);
+ expectedAddresses = new ArrayList<>(4);
+ for (int i = 0; i < 3; i++) {
+ expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
+ }
+ if (notesMigration.readChanges()) {
+ expectedAddresses.add(reviewer.emailAddress);
+ }
+ assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+ }
+
+ @Test
+ public void transitionCcToReviewer() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ in.state = CC;
+ addReviewer(changeId, in);
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+
+ in.state = REVIEWER;
+ addReviewer(changeId, in);
+ c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ } else {
+ // If NoteDb not enabled, should have had no effect.
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+ }
+ }
+
+ @Test
+ public void reviewAndAddReviewers() throws Exception {
+ TestAccount observer = accounts.user2();
+ PushOneCommit.Result r = createChange();
+ ReviewInput input = ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(observer.email, CC, false);
+
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNotNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+
+ // Verify reviewer and CC were added. If not in NoteDb read mode, both
+ // parties will be returned as CCed.
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC, observer);
+ } else {
+ // In legacy mode, change owner should be the only reviewer.
+ assertReviewers(c, REVIEWER, admin);
+ assertReviewers(c, CC, user, observer);
+ }
+
+ // Verify emails were sent to added reviewers.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(3);
+ // First email to user.
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress);
+ assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+ assertThat(m.body()).contains("I'd like you to do a code review.");
+ assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+ // Second email to reviewer and observer.
+ m = messages.get(1);
+ if (notesMigration.readChanges()) {
+ assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+ assertThat(m.body()).contains(admin.fullName + " has uploaded a new change for review.");
+ } else {
+ assertThat(m.rcpt()).containsExactly(observer.emailAddress);
+ assertThat(m.body()).contains("Hello " + observer.fullName + ",\n");
+ assertThat(m.body()).contains("I'd like you to do a code review.");
+ }
+
+ // Third email is review to user and observer.
+ m = messages.get(2);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+ assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
+ assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+ assertThat(m.body()).contains("Patch Set 1: Code-Review+2\n");
+ }
+
+ @Test
+ public void reviewAndAddGroupReviewers() throws Exception {
+ int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ List<TestAccount> users =
+ createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
+ List<String> usernames = new ArrayList<>(largeGroupSize);
+ for (TestAccount u : users) {
+ usernames.add(u.username);
+ }
+
+ String largeGroup = createGroup("largeGroup");
+ String mediumGroup = createGroup("mediumGroup");
+ gApi.groups().id(largeGroup).addMembers(
+ usernames.toArray(new String[largeGroupSize]));
+ gApi.groups().id(mediumGroup).addMembers(
+ usernames.subList(0, mediumGroupSize)
+ .toArray(new String[mediumGroupSize]));
+
+ TestAccount observer = accounts.user2();
+ PushOneCommit.Result r = createChange();
+
+ // Attempt to add overly large group as reviewers.
+ ReviewInput input = ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(observer.email, CC, false)
+ .reviewer(largeGroup);
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(3);
+ AddReviewerResult reviewerResult = result.reviewers.get(largeGroup);
+ assertThat(reviewerResult).isNotNull();
+ assertThat(reviewerResult.confirm).isNull();
+ assertThat(reviewerResult.error).isNotNull();
+ assertThat(reviewerResult.error).contains("has too many members to add them all as reviewers");
+
+ // No labels should have changed, and no reviewers/CCs should have been added.
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(c.messages).hasSize(1);
+ assertThat(c.reviewers.get(REVIEWER)).isNull();
+ assertThat(c.reviewers.get(CC)).isNull();
+
+ // Attempt to add group large enough to require confirmation, without
+ // confirmation, as reviewers.
+ input = ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(observer.email, CC, false)
+ .reviewer(mediumGroup);
+ result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(3);
+ reviewerResult = result.reviewers.get(mediumGroup);
+ assertThat(reviewerResult).isNotNull();
+ assertThat(reviewerResult.confirm).isTrue();
+ assertThat(reviewerResult.error)
+ .contains("has " + mediumGroupSize + " members. Do you want to add them all"
+ + " as reviewers?");
+
+ // No labels should have changed, and no reviewers/CCs should have been added.
+ c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(c.messages).hasSize(1);
+ assertThat(c.reviewers.get(REVIEWER)).isNull();
+ assertThat(c.reviewers.get(CC)).isNull();
+
+ // Retrying with confirmation should successfully approve and add reviewers/CCs.
+ input = ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(mediumGroup, CC, true);
+ result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNotNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+
+ c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(c.messages).hasSize(2);
+
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC, users.subList(0, mediumGroupSize));
+ } else {
+ // If not in NoteDb mode, then user is returned with the CC group.
+ assertReviewers(c, REVIEWER, admin);
+ List<TestAccount> expectedCC = users.subList(0, mediumGroupSize);
+ expectedCC.add(user);
+ assertReviewers(c, CC, expectedCC);
+ }
}
private AddReviewerResult addReviewer(String changeId, String reviewer)
@@ -100,6 +412,12 @@
return readContentFromJson(resp, AddReviewerResult.class);
}
+ private ReviewResult review(String changeId, String revisionId, ReviewInput in) throws Exception {
+ RestResponse resp = adminRestSession.post(
+ "/changes/" + changeId + "/revisions/" + revisionId + "/review", in);
+ return readContentFromJson(resp, ReviewResult.class);
+ }
+
private static <T> T readContentFromJson(RestResponse r, Class<T> clazz)
throws Exception {
r.assertOK();
@@ -107,4 +425,42 @@
jsonReader.setLenient(true);
return newGson().fromJson(jsonReader, clazz);
}
+
+ private static void assertReviewers(ChangeInfo c, ReviewerState reviewerState,
+ TestAccount... accounts) throws Exception {
+ List<TestAccount> accountList = new ArrayList<>(accounts.length);
+ for (TestAccount a : accounts) {
+ accountList.add(a);
+ }
+ assertReviewers(c, reviewerState, accountList);
+ }
+
+ private static void assertReviewers(ChangeInfo c, ReviewerState reviewerState,
+ Iterable<TestAccount> accounts) throws Exception {
+ Collection<AccountInfo> actualAccounts = c.reviewers.get(reviewerState);
+ if (actualAccounts == null) {
+ assertThat(accounts.iterator().hasNext()).isFalse();
+ return;
+ }
+ assertThat(actualAccounts).isNotNull();
+ List<Integer> actualAccountIds = new ArrayList<>(actualAccounts.size());
+ for (AccountInfo account : actualAccounts) {
+ actualAccountIds.add(account._accountId);
+ }
+ List<Integer> expectedAccountIds = new ArrayList<>();
+ for (TestAccount account : accounts) {
+ expectedAccountIds.add(account.getId().get());
+ }
+ assertThat(actualAccountIds).containsExactlyElementsIn(expectedAccountIds);
+ }
+
+ private List<TestAccount> createAccounts(int n, String emailPrefix)
+ throws Exception {
+ List<TestAccount> result = new ArrayList<>(n);
+ for (int i = 0; i < n; i++) {
+ result.add(accounts.create(name("u" + i),
+ emailPrefix + "-" + i + "@example.com", "Full Name " + i));
+ }
+ return result;
+ }
}
\ No newline at end of file
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
new file mode 100644
index 0000000..f52fccd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
+
+import org.junit.Test;
+
+public class TopicIT extends AbstractDaemonTest {
+ @Test
+ public void topic() throws Exception {
+ Result result = createChange();
+ String endpoint = "/changes/" + result.getChangeId() + "/topic";
+ RestResponse response = adminRestSession.put(endpoint, "topic");
+ response.assertOK();
+
+ response = adminRestSession.delete(endpoint);
+ response.assertNoContent();
+
+ response = adminRestSession.put(endpoint, "topic");
+ response.assertOK();
+
+ response = adminRestSession.put(endpoint, "");
+ response.assertNoContent();
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 96a672f..d9f1a5c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -56,6 +57,7 @@
@NoHttpd
public class CommentsIT extends AbstractDaemonTest {
+
@Inject
private Provider<ChangesCollection> changes;
@@ -87,12 +89,35 @@
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
- DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
+ String path = "file1";
+ DraftInput comment = newDraft(path, Side.REVISION, line, "comment 1");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
assertThat(result).hasSize(1);
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
- assertCommentInfo(comment, actual);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+ }
+ }
+
+ @Test
+ public void createDraftOnMergeCommitChange() throws Exception {
+ for (Integer line : lines) {
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String path = "file1";
+ DraftInput c1 = newDraft(path, Side.REVISION, line, "ps-1");
+ DraftInput c2 = newDraft(path, Side.PARENT, line, "auto-merge of ps-1");
+ DraftInput c3 = newDraftOnParent(path, 1, line, "parent-1 of ps-1");
+ DraftInput c4 = newDraftOnParent(path, 2, line, "parent-2 of ps-1");
+ addDraft(changeId, revId, c1);
+ addDraft(changeId, revId, c2);
+ addDraft(changeId, revId, c3);
+ addDraft(changeId, revId, c4);
+ Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+ assertThat(result).hasSize(1);
+ assertThat(Lists.transform(result.get(path), infoToDraft(path)))
+ .containsExactly(c1, c2, c3, c4);
}
}
@@ -114,8 +139,31 @@
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
- assertCommentInfo(comment, actual);
- assertCommentInfo(actual, getPublishedComment(changeId, revId, actual.id));
+ assertThat(comment).isEqualTo(infoToInput(file).apply(actual));
+ assertThat(comment).isEqualTo(infoToInput(file).apply(
+ getPublishedComment(changeId, revId, actual.id)));
+ }
+ }
+
+ @Test
+ public void postCommentOnMergeCommitChange() throws Exception {
+ for (Integer line : lines) {
+ final String file = "/COMMIT_MSG";
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ ReviewInput input = new ReviewInput();
+ CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1");
+ CommentInput c2 = newComment(file, Side.PARENT, line, "auto-merge of ps-1");
+ CommentInput c3 = newCommentOnParent(file, 1, line, "parent-1 of ps-1");
+ CommentInput c4 = newCommentOnParent(file, 2, line, "parent-2 of ps-1");
+ input.comments = new HashMap<>();
+ input.comments.put(file, ImmutableList.of(c1, c2, c3, c4));
+ revision(r).review(input);
+ Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+ assertThat(result).isNotEmpty();
+ assertThat(Lists.transform(result.get(file), infoToInput(file)))
+ .containsExactly(c1, c2, c3, c4);
}
}
@@ -129,7 +177,7 @@
String revId = r.getCommit().getName();
assertThat(getPublishedComments(changeId, revId)).isEmpty();
- List<Comment> expectedComments = new ArrayList<>();
+ List<CommentInput> expectedComments = new ArrayList<>();
for (Integer line : lines) {
ReviewInput input = new ReviewInput();
CommentInput comment = newComment(file, Side.REVISION, line, "comment " + line);
@@ -142,10 +190,8 @@
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
List<CommentInfo> actualComments = result.get(file);
- assertThat(actualComments).hasSize(expectedComments.size());
- for (int i = 0; i < actualComments.size(); i++) {
- assertCommentInfo(expectedComments.get(i), actualComments.get(i));
- }
+ assertThat(Lists.transform(actualComments, infoToInput(file)))
+ .containsExactlyElementsIn(expectedComments);
}
@Test
@@ -155,17 +201,18 @@
Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
- DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
+ String path = "file1";
+ DraftInput comment = newDraft(path, Side.REVISION, line, "comment 1");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
- assertCommentInfo(comment, actual);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
String uuid = actual.id;
comment.message = "updated comment 1";
updateDraft(changeId, revId, comment, uuid);
result = getDraftComments(changeId, revId);
actual = Iterables.getOnlyElement(result.get(comment.path));
- assertCommentInfo(comment, actual);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
// Posting a draft comment doesn't cause lastUpdatedOn to change.
assertThat(r.getChange().change().getLastUpdatedOn())
@@ -181,7 +228,7 @@
String revId = r.getCommit().getName();
assertThat(getDraftComments(changeId, revId)).isEmpty();
- List<Comment> expectedDrafts = new ArrayList<>();
+ List<DraftInput> expectedDrafts = new ArrayList<>();
for (Integer line : lines) {
DraftInput comment = newDraft(file, Side.REVISION, line, "comment " + line);
expectedDrafts.add(comment);
@@ -191,10 +238,8 @@
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
assertThat(result).isNotEmpty();
List<CommentInfo> actualComments = result.get(file);
- assertThat(actualComments).hasSize(expectedDrafts.size());
- for (int i = 0; i < actualComments.size(); i++) {
- assertCommentInfo(expectedDrafts.get(i), actualComments.get(i));
- }
+ assertThat(Lists.transform(actualComments, infoToDraft(file)))
+ .containsExactlyElementsIn(expectedDrafts);
}
@Test
@@ -203,11 +248,12 @@
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
+ String path = "file1";
DraftInput comment = newDraft(
- "file1", Side.REVISION, line, "comment 1");
+ path, Side.REVISION, line, "comment 1");
CommentInfo returned = addDraft(changeId, revId, comment);
CommentInfo actual = getDraftComment(changeId, revId, returned.id);
- assertCommentInfo(comment, actual);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
}
}
@@ -257,7 +303,9 @@
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
- assertCommentInfo(comment, actual);
+ CommentInput ci = infoToInput(file).apply(actual);
+ ci.updated = comment.updated;
+ assertThat(comment).isEqualTo(ci);
assertThat(actual.updated)
.isEqualTo(gApi.changes().id(r.getChangeId()).info().created);
@@ -597,45 +645,35 @@
return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
}
- private static void assertCommentInfo(Comment expected, CommentInfo actual) {
- assertThat(actual.line).isEqualTo(expected.line);
- assertThat(actual.message).isEqualTo(expected.message);
- assertThat(actual.inReplyTo).isEqualTo(expected.inReplyTo);
- assertCommentRange(expected.range, actual.range);
- if (actual.side == null && expected.side != null) {
- assertThat(Side.REVISION).isEqualTo(expected.side);
- }
- }
-
- private static void assertCommentRange(Comment.Range expected,
- Comment.Range actual) {
- if (expected == null) {
- assertThat(actual).isNull();
- } else {
- assertThat(actual).isNotNull();
- assertThat(actual.startLine).isEqualTo(expected.startLine);
- assertThat(actual.startCharacter).isEqualTo(expected.startCharacter);
- assertThat(actual.endLine).isEqualTo(expected.endLine);
- assertThat(actual.endCharacter).isEqualTo(expected.endCharacter);
- }
- }
-
private static CommentInput newComment(String path, Side side, int line,
String message) {
CommentInput c = new CommentInput();
- return populate(c, path, side, line, message);
+ return populate(c, path, side, null, line, message);
+ }
+
+ private static CommentInput newCommentOnParent(String path, int parent,
+ int line, String message) {
+ CommentInput c = new CommentInput();
+ return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message);
}
private DraftInput newDraft(String path, Side side, int line,
String message) {
DraftInput d = new DraftInput();
- return populate(d, path, side, line, message);
+ return populate(d, path, side, null, line, message);
+ }
+
+ private DraftInput newDraftOnParent(String path, int parent, int line,
+ String message) {
+ DraftInput d = new DraftInput();
+ return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message);
}
private static <C extends Comment> C populate(C c, String path, Side side,
- int line, String message) {
+ Integer parent, int line, String message) {
c.path = path;
c.side = side;
+ c.parent = parent;
c.line = line != 0 ? line : null;
c.message = message;
if (line != 0) {
@@ -648,4 +686,38 @@
}
return c;
}
+
+ private static Function<CommentInfo, CommentInput> infoToInput(
+ final String path) {
+ return new Function<CommentInfo, CommentInput>() {
+ @Override
+ public CommentInput apply(CommentInfo info) {
+ CommentInput ci = new CommentInput();
+ ci.path = path;
+ copy(info, ci);
+ return ci;
+ }
+ };
+ }
+
+ private static Function<CommentInfo, DraftInput> infoToDraft(
+ final String path) {
+ return new Function<CommentInfo, DraftInput>() {
+ @Override
+ public DraftInput apply(CommentInfo info) {
+ DraftInput di = new DraftInput();
+ di.path = path;
+ copy(info, di);
+ return di;
+ }
+ };
+ }
+
+ private static void copy(Comment from, Comment to) {
+ to.side = from.side == null ? Side.REVISION : from.side;
+ to.parent = from.parent;
+ to.line = from.line;
+ to.message = from.message;
+ to.range = from.range;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index f846151..b88278e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -14,19 +14,22 @@
package com.google.gerrit.acceptance.server.change;
-import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.testutil.TestChanges.newChange;
+import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIXED;
+import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIX_FAILED;
import static com.google.gerrit.testutil.TestChanges.newPatchSet;
import static java.util.Collections.singleton;
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
@@ -34,22 +37,39 @@
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;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ConsistencyChecker;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.TestChanges;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
import org.junit.Before;
import org.junit.Test;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
@NoHttpd
@@ -58,25 +78,36 @@
private ChangeControl.GenericFactory changeControlFactory;
@Inject
- private ChangeUpdate.Factory changeUpdateFactory;
-
- @Inject
private Provider<ConsistencyChecker> checkerProvider;
@Inject
private IdentifiedUser.GenericFactory userFactory;
+ @Inject
+ private BatchUpdate.Factory updateFactory;
+
+ @Inject
+ private ChangeInserter.Factory changeInserterFactory;
+
+ @Inject
+ private PatchSetInserter.Factory patchSetInserterFactory;
+
+ @Inject
+ private ChangeNoteUtil noteUtil;
+
+ @Inject
+ @AnonymousCowardName
+ private String anonymousCowardName;
+
+ @Inject
+ private Sequences sequences;
+
private RevCommit tip;
private Account.Id adminId;
private ConsistencyChecker checker;
@Before
public void setUp() throws Exception {
- // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
- // Note that we *do* want to enable these tests with GERRIT_CHECK_NOTEDB, as
- // we need to be able to convert old, corrupt changes. However, those tests
- // don't necessarily need to pass.
- assume().that(notesMigration.enabled()).isFalse();
// Ignore client clone of project; repurpose as server-side TestRepository.
testRepo = new TestRepository<>(
(InMemoryRepository) repoManager.openRepository(project));
@@ -88,62 +119,56 @@
@Test
public void validNewChange() throws Exception {
- Change c = insertChange();
- insertPatchSet(c);
- incrementPatchSet(c);
- insertPatchSet(c);
- assertProblems(c);
+ assertNoProblems(insertChange(), null);
}
@Test
public void validMergedChange() throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- insertPatchSet(c);
- incrementPatchSet(c);
-
- incrementPatchSet(c);
- RevCommit commit2 = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).create();
- PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, adminId);
- db.patchSets().insert(singleton(ps2));
-
- testRepo.branch(c.getDest().get()).update(commit2);
- assertProblems(c);
+ ChangeControl ctl = mergeChange(incrementPatchSet(insertChange()));
+ assertNoProblems(ctl, null);
}
@Test
public void missingOwner() throws Exception {
- Change c = newChange(project, new Account.Id(2));
- db.changes().insert(singleton(c));
- RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- db.patchSets().insert(singleton(ps));
+ TestAccount owner = accounts.create("missing");
+ ChangeControl ctl = insertChange(owner);
+ db.accounts().deleteKeys(singleton(owner.getId()));
- assertProblems(c, "Missing change owner: 2");
+ assertProblems(ctl, null,
+ problem("Missing change owner: " + owner.getId()));
}
@Test
public void missingRepo() throws Exception {
- Change c = newChange(new Project.NameKey("otherproject"), adminId);
- db.changes().insert(singleton(c));
- insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
- assertProblems(c, "Destination repository not found: otherproject");
+ // NoteDb can't have a change without a repo.
+ assume().that(notesMigration.enabled()).isFalse();
+
+ ChangeControl ctl = insertChange();
+ Project.NameKey name = ctl.getProject().getNameKey();
+ ((InMemoryRepositoryManager) repoManager).deleteRepository(name);
+
+ assertProblems(
+ ctl, null,
+ problem("Destination repository not found: " + name));
}
@Test
public void invalidRevision() throws Exception {
- Change c = insertChange();
+ // NoteDb always parses the revision when inserting a patch set, so we can't
+ // create an invalid patch set.
+ assume().that(notesMigration.enabled()).isFalse();
- db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
- "fooooooooooooooooooooooooooooooooooooooo", adminId)));
- incrementPatchSet(c);
- insertPatchSet(c);
+ ChangeControl ctl = insertChange();
+ PatchSet ps = newPatchSet(
+ ctl.getChange().currentPatchSetId(),
+ "fooooooooooooooooooooooooooooooooooooooo",
+ adminId);
+ db.patchSets().update(singleton(ps));
- assertProblems(c,
- "Invalid revision on patch set 1:"
- + " fooooooooooooooooooooooooooooooooooooooo");
+ assertProblems(
+ ctl, null,
+ problem("Invalid revision on patch set 1:"
+ + " fooooooooooooooooooooooooooooooooooooooo"));
}
// No test for ref existing but object missing; InMemoryRepository won't let
@@ -151,414 +176,429 @@
@Test
public void patchSetObjectAndRefMissing() throws Exception {
- Change c = insertChange();
- PatchSet ps = newPatchSet(c.currentPatchSetId(),
- ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
- db.patchSets().insert(singleton(ps));
-
- assertProblems(c,
- "Ref missing: " + ps.getId().toRefName(),
- "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ ChangeControl ctl = insertChange();
+ PatchSet ps = insertMissingPatchSet(ctl, rev);
+ ctl = reload(ctl);
+ assertProblems(
+ ctl, null,
+ problem("Ref missing: " + ps.getId().toRefName()),
+ problem(
+ "Object missing: patch set 2:"
+ + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
}
@Test
public void patchSetObjectAndRefMissingWithFix() throws Exception {
- Change c = insertChange();
- PatchSet ps = newPatchSet(c.currentPatchSetId(),
- ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
- db.patchSets().insert(singleton(ps));
+ String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ ChangeControl ctl = insertChange();
+ PatchSet ps = insertMissingPatchSet(ctl, rev);
+ ctl = reload(ctl);
String refName = ps.getId().toRefName();
- List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo("Ref missing: " + refName);
- assertThat(p.status).isNull();
+ assertProblems(
+ ctl, new FixInput(),
+ problem("Ref missing: " + refName),
+ problem("Object missing: patch set 2: " + rev));
}
@Test
public void patchSetRefMissing() throws Exception {
- Change c = insertChange();
- PatchSet ps = insertPatchSet(c);
- String refName = ps.getId().toRefName();
- testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+ ChangeControl ctl = insertChange();
+ testRepo.update(
+ "refs/other/foo",
+ ObjectId.fromString(
+ psUtil.current(db, ctl.getNotes()).getRevision().get()));
+ String refName = ctl.getChange().currentPatchSetId().toRefName();
deleteRef(refName);
- assertProblems(c, "Ref missing: " + refName);
+ assertProblems(ctl, null, problem("Ref missing: " + refName));
}
@Test
public void patchSetRefMissingWithFix() throws Exception {
- Change c = insertChange();
- PatchSet ps = insertPatchSet(c);
- String refName = ps.getId().toRefName();
- testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ testRepo.update("refs/other/foo", ObjectId.fromString(rev));
+ String refName = ctl.getChange().currentPatchSetId().toRefName();
deleteRef(refName);
- List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo("Ref missing: " + refName);
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Repaired patch set ref");
-
+ assertProblems(
+ ctl, new FixInput(),
+ problem("Ref missing: " + refName, FIXED, "Repaired patch set ref"));
assertThat(testRepo.getRepository().exactRef(refName).getObjectId().name())
- .isEqualTo(ps.getRevision().get());
+ .isEqualTo(rev);
}
@Test
public void patchSetObjectAndRefMissingWithDeletingPatchSet()
throws Exception {
- Change c = insertChange();
- PatchSet ps1 = insertPatchSet(c);
- incrementPatchSet(c);
- PatchSet ps2 = insertMissingPatchSet(c,
- "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ ChangeControl ctl = insertChange();
+ PatchSet ps1 = psUtil.current(db, ctl.getNotes());
+
+ String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ PatchSet ps2 = insertMissingPatchSet(ctl, rev2);
+ ctl = reload(ctl);
FixInput fix = new FixInput();
fix.deletePatchSetIfCommitMissing = true;
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(2);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
- assertThat(p.status).isNull();
- p = problems.get(1);
- assertThat(p.message).isEqualTo(
- "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Deleted patch set");
+ assertProblems(
+ ctl, fix,
+ problem("Ref missing: " + ps2.getId().toRefName()),
+ problem("Object missing: patch set 2: " + rev2,
+ FIXED, "Deleted patch set"));
- c = notesFactory.createChecked(db, c).getChange();
- assertThat(c.currentPatchSetId().get()).isEqualTo(1);
- assertThat(getPatchSet(ps1.getId())).isNotNull();
- assertThat(getPatchSet(ps2.getId())).isNull();
+ ctl = reload(ctl);
+ assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1);
+ assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull();
+ assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull();
}
@Test
public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
throws Exception {
- Change c = insertChange();
- PatchSet ps1 = insertPatchSet(c);
+ ChangeControl ctl = insertChange();
+ PatchSet ps1 = psUtil.current(db, ctl.getNotes());
- incrementPatchSet(c);
- PatchSet ps2 = insertMissingPatchSet(c,
- "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ PatchSet ps2 = insertMissingPatchSet(ctl, rev2);
- incrementPatchSet(c);
- PatchSet ps3 = insertPatchSet(c);
+ ctl = incrementPatchSet(reload(ctl));
+ PatchSet ps3 = psUtil.current(db, ctl.getNotes());
- incrementPatchSet(c);
- PatchSet ps4 = insertMissingPatchSet(c,
- "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+ String rev4 = "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee";
+ PatchSet ps4 = insertMissingPatchSet(ctl, rev4);
+ ctl = reload(ctl);
FixInput fix = new FixInput();
fix.deletePatchSetIfCommitMissing = true;
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(4);
+ assertProblems(
+ ctl, fix,
+ problem("Ref missing: " + ps2.getId().toRefName()),
+ problem("Object missing: patch set 2: " + rev2,
+ FIXED, "Deleted patch set"),
+ problem("Ref missing: " + ps4.getId().toRefName()),
+ problem("Object missing: patch set 4: " + rev4,
+ FIXED, "Deleted patch set"));
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
- assertThat(p.status).isNull();
-
- p = problems.get(1);
- assertThat(p.message).isEqualTo(
- "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Deleted patch set");
-
- p = problems.get(2);
- assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
- assertThat(p.status).isNull();
-
- p = problems.get(3);
- assertThat(p.message).isEqualTo(
- "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Deleted patch set");
-
- c = notesFactory.createChecked(db, c).getChange();
- assertThat(c.currentPatchSetId().get()).isEqualTo(3);
- assertThat(getPatchSet(ps1.getId())).isNotNull();
- assertThat(getPatchSet(ps2.getId())).isNull();
- assertThat(getPatchSet(ps3.getId())).isNotNull();
- assertThat(getPatchSet(ps4.getId())).isNull();
+ ctl = reload(ctl);
+ assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(3);
+ assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull();
+ assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull();
+ assertThat(psUtil.get(db, ctl.getNotes(), ps3.getId())).isNotNull();
+ assertThat(psUtil.get(db, ctl.getNotes(), ps4.getId())).isNull();
}
@Test
public void onlyPatchSetObjectMissingWithFix() throws Exception {
- Change c = insertChange();
- PatchSet ps1 = insertMissingPatchSet(c,
- "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ Change c = TestChanges.newChange(
+ project, admin.getId(), sequences.nextChangeId());
+ PatchSet.Id psId = c.currentPatchSetId();
+ String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ PatchSet ps = newPatchSet(psId, rev, adminId);
+
+ db.changes().insert(singleton(c));
+ db.patchSets().insert(singleton(ps));
+ addNoteDbCommit(
+ c.getId(),
+ "Create change\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Branch: " + c.getDest().get() + "\n"
+ + "Change-id: " + c.getKey().get() + "\n"
+ + "Subject: Bogus subject\n"
+ + "Commit: " + rev + "\n"
+ + "Groups: " + rev + "\n");
+ indexer.index(db, c.getProject(), c.getId());
+ IdentifiedUser user = userFactory.create(admin.getId());
+ ChangeControl ctl = changeControlFactory.controlFor(
+ db, c.getProject(), c.getId(), user);
FixInput fix = new FixInput();
fix.deletePatchSetIfCommitMissing = true;
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(2);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
- assertThat(p.status).isNull();
- p = problems.get(1);
- assertThat(p.message).isEqualTo(
- "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
- assertThat(p.outcome)
- .isEqualTo("Cannot delete patch set; no patch sets would remain");
+ assertProblems(
+ ctl, fix,
+ problem("Ref missing: " + ps.getId().toRefName()),
+ problem("Object missing: patch set 1: " + rev,
+ FIX_FAILED, "Cannot delete patch set; no patch sets would remain"));
- c = notesFactory.createChecked(db, c).getChange();
- assertThat(c.currentPatchSetId().get()).isEqualTo(1);
- assertThat(getPatchSet(ps1.getId())).isNotNull();
+ ctl = reload(ctl);
+ assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1);
+ assertThat(psUtil.current(db, ctl.getNotes())).isNotNull();
}
@Test
public void currentPatchSetMissing() throws Exception {
- Change c = insertChange();
- assertProblems(c, "Current patch set 1 not found");
+ // NoteDb can't create a change without a patch set.
+ assume().that(notesMigration.enabled()).isFalse();
+
+ ChangeControl ctl = insertChange();
+ db.patchSets().deleteKeys(singleton(ctl.getChange().currentPatchSetId()));
+ assertProblems(ctl, null, problem("Current patch set 1 not found"));
}
@Test
public void duplicatePatchSetRevisions() throws Exception {
- Change c = insertChange();
- PatchSet ps1 = insertPatchSet(c);
+ ChangeControl ctl = insertChange();
+ PatchSet ps1 = psUtil.current(db, ctl.getNotes());
String rev = ps1.getRevision().get();
- incrementPatchSet(c);
- PatchSet ps2 = insertMissingPatchSet(c, rev);
- updatePatchSetRef(ps2);
- assertProblems(c,
- "Multiple patch sets pointing to " + rev + ": [1, 2]");
+ ctl = incrementPatchSet(
+ ctl, testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
+
+ assertProblems(
+ ctl, null,
+ problem("Multiple patch sets pointing to " + rev + ": [1, 2]"));
}
@Test
public void missingDestRef() throws Exception {
+ ChangeControl ctl = insertChange();
+
String ref = "refs/heads/master";
// Detach head so we're allowed to delete ref.
testRepo.reset(testRepo.getRepository().exactRef(ref).getObjectId());
RefUpdate ru = testRepo.getRepository().updateRef(ref);
ru.setForceUpdate(true);
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
- Change c = insertChange();
- RevCommit commit = testRepo.commit().create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- updatePatchSetRef(ps);
- db.patchSets().insert(singleton(ps));
- assertProblems(c, "Destination ref not found (may be new branch): " + ref);
+ assertProblems(
+ ctl, null,
+ problem("Destination ref not found (may be new branch): " + ref));
}
@Test
public void mergedChangeIsNotMerged() throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- PatchSet ps = insertPatchSet(c);
- String rev = ps.getRevision().get();
+ ChangeControl ctl = insertChange();
- assertProblems(c,
- "Patch set 1 (" + rev + ") is not merged into destination ref"
- + " refs/heads/master (" + tip.name()
- + "), but change status is MERGED");
+ try (BatchUpdate bu = newUpdate(adminId)) {
+ bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException {
+ ctx.getChange().setStatus(Change.Status.MERGED);
+ ctx.getUpdate(ctx.getChange().currentPatchSetId())
+ .fixStatus(Change.Status.MERGED);
+ return true;
+ }
+ });
+ bu.execute();
+ }
+ ctl = reload(ctl);
+
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ ObjectId tip = getDestRef(ctl);
+ assertProblems(
+ ctl, null,
+ problem(
+ "Patch set 1 (" + rev + ") is not merged into destination ref"
+ + " refs/heads/master (" + tip.name()
+ + "), but change status is MERGED"));
}
@Test
public void newChangeIsMerged() throws Exception {
- Change c = insertChange();
- RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- db.patchSets().insert(singleton(ps));
- testRepo.branch(c.getDest().get()).update(commit);
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ testRepo.branch(ctl.getChange().getDest().get())
+ .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
- assertProblems(c,
- "Patch set 1 (" + commit.name() + ") is merged into destination ref"
- + " refs/heads/master (" + commit.name()
- + "), but change status is NEW");
+ assertProblems(
+ ctl, null,
+ problem(
+ "Patch set 1 (" + rev + ") is merged into destination ref"
+ + " refs/heads/master (" + rev
+ + "), but change status is NEW"));
}
@Test
public void newChangeIsMergedWithFix() throws Exception {
- Change c = insertChange();
- RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- db.patchSets().insert(singleton(ps));
- testRepo.branch(c.getDest().get()).update(commit);
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ testRepo.branch(ctl.getChange().getDest().get())
+ .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
- List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "Patch set 1 (" + commit.name() + ") is merged into destination ref"
- + " refs/heads/master (" + commit.name()
- + "), but change status is NEW");
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Marked change as merged");
+ assertProblems(
+ ctl, new FixInput(),
+ problem(
+ "Patch set 1 (" + rev + ") is merged into destination ref"
+ + " refs/heads/master (" + rev
+ + "), but change status is NEW",
+ FIXED, "Marked change as merged"));
- c = notesFactory.createChecked(db, c).getChange();
- assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
- assertProblems(c);
+ ctl = reload(ctl);
+ assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+ assertNoProblems(ctl, null);
}
@Test
public void extensionApiReturnsUpdatedValueAfterFix() throws Exception {
- Change c = insertChange();
- RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- db.patchSets().insert(singleton(ps));
- testRepo.branch(c.getDest().get()).update(commit);
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ testRepo.branch(ctl.getChange().getDest().get())
+ .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
ChangeInfo info = gApi.changes()
- .id(c.getChangeId())
+ .id(ctl.getId().get())
.info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
info = gApi.changes()
- .id(c.getChangeId())
+ .id(ctl.getId().get())
.check(new FixInput());
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void expectedMergedCommitIsLatestPatchSet() throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- PatchSet ps = insertPatchSet(c);
- RevCommit commit = parseCommit(ps);
- testRepo.update(c.getDest().get(), commit);
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ testRepo.branch(ctl.getChange().getDest().get())
+ .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)));
FixInput fix = new FixInput();
- fix.expectMergedAs = commit.name();
- assertThat(checker.check(c, fix).problems()).isEmpty();
+ fix.expectMergedAs = rev;
+ assertProblems(
+ ctl, fix,
+ problem(
+ "Patch set 1 (" + rev + ") is merged into destination ref"
+ + " refs/heads/master (" + rev + "), but change status is NEW",
+ FIXED, "Marked change as merged"));
+
+ ctl = reload(ctl);
+ assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+ assertNoProblems(ctl, null);
}
@Test
public void expectedMergedCommitNotMergedIntoDestination() throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- PatchSet ps = insertPatchSet(c);
- RevCommit commit = parseCommit(ps);
- testRepo.update(c.getDest().get(), commit);
+ ChangeControl ctl = insertChange();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ RevCommit commit =
+ testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+ testRepo.branch(ctl.getChange().getDest().get()).update(commit);
FixInput fix = new FixInput();
RevCommit other =
testRepo.commit().message(commit.getFullMessage()).create();
fix.expectMergedAs = other.name();
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "Expected merged commit " + other.name()
- + " is not merged into destination ref refs/heads/master"
- + " (" + commit.name() + ")");
+ assertProblems(
+ ctl, fix,
+ problem(
+ "Expected merged commit " + other.name()
+ + " is not merged into destination ref refs/heads/master"
+ + " (" + commit.name() + ")"));
}
@Test
public void createNewPatchSetForExpectedMergeCommitWithNoChangeId()
throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- RevCommit parent =
- testRepo.branch(c.getDest().get()).commit().message("parent").create();
- PatchSet ps = insertPatchSet(c);
- RevCommit commit = parseCommit(ps);
+ ChangeControl ctl = insertChange();
+ String dest = ctl.getChange().getDest().get();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ RevCommit commit =
+ testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
- RevCommit mergedAs = testRepo.commit().parent(parent)
+ RevCommit mergedAs = testRepo.commit().parent(commit.getParent(0))
.message(commit.getShortMessage()).create();
testRepo.getRevWalk().parseBody(mergedAs);
assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).isEmpty();
- testRepo.update(c.getDest().get(), mergedAs);
+ testRepo.update(dest, mergedAs);
- assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
- + " destination ref refs/heads/master (" + mergedAs.name()
- + "), but change status is MERGED");
+ assertNoProblems(ctl, null);
FixInput fix = new FixInput();
fix.expectMergedAs = mergedAs.name();
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "No patch set found for merged commit " + mergedAs.name());
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+ assertProblems(
+ ctl, fix,
+ problem(
+ "No patch set found for merged commit " + mergedAs.name(),
+ FIXED, "Inserted as patch set 2"),
+ problem(
+ "Expected merged commit " + mergedAs.name()
+ + " has no associated patch set",
+ FIXED, "Marked change as merged"));
- c = notesFactory.createChecked(db, c).getChange();
- PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
- assertThat(c.currentPatchSetId()).isEqualTo(psId2);
- assertThat(getPatchSet(psId2).getRevision().get())
+ ctl = reload(ctl);
+ PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+ assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2);
+ assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get())
.isEqualTo(mergedAs.name());
- assertProblems(c);
+ assertNoProblems(ctl, null);
}
@Test
public void createNewPatchSetForExpectedMergeCommitWithChangeId()
throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- RevCommit parent =
- testRepo.branch(c.getDest().get()).commit().message("parent").create();
- PatchSet ps = insertPatchSet(c);
- RevCommit commit = parseCommit(ps);
+ ChangeControl ctl = insertChange();
+ String dest = ctl.getChange().getDest().get();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ RevCommit commit =
+ testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
- RevCommit mergedAs = testRepo.commit().parent(parent)
+ RevCommit mergedAs = testRepo.commit().parent(commit.getParent(0))
.message(commit.getShortMessage() + "\n"
+ "\n"
- + "Change-Id: " + c.getKey().get() + "\n").create();
+ + "Change-Id: " + ctl.getChange().getKey().get() + "\n").create();
testRepo.getRevWalk().parseBody(mergedAs);
assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID))
- .containsExactly(c.getKey().get());
- testRepo.update(c.getDest().get(), mergedAs);
+ .containsExactly(ctl.getChange().getKey().get());
+ testRepo.update(dest, mergedAs);
- assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
- + " destination ref refs/heads/master (" + mergedAs.name()
- + "), but change status is MERGED");
+ assertNoProblems(ctl, null);
FixInput fix = new FixInput();
fix.expectMergedAs = mergedAs.name();
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "No patch set found for merged commit " + mergedAs.name());
- assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
- assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+ assertProblems(
+ ctl, fix,
+ problem(
+ "No patch set found for merged commit " + mergedAs.name(),
+ FIXED, "Inserted as patch set 2"),
+ problem(
+ "Expected merged commit " + mergedAs.name()
+ + " has no associated patch set",
+ FIXED, "Marked change as merged"));
- c = notesFactory.createChecked(db, c).getChange();
- PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
- assertThat(c.currentPatchSetId()).isEqualTo(psId2);
- assertThat(getPatchSet(psId2).getRevision().get())
+ ctl = reload(ctl);
+ PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2);
+ assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2);
+ assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get())
.isEqualTo(mergedAs.name());
- assertProblems(c);
+ assertNoProblems(ctl, null);
}
@Test
public void expectedMergedCommitIsOldPatchSetOfSameChange()
throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
- PatchSet ps1 = insertPatchSet(c);
+ ChangeControl ctl = insertChange();
+ PatchSet ps1 = psUtil.current(db, ctl.getNotes());
String rev1 = ps1.getRevision().get();
- incrementPatchSet(c);
- PatchSet ps2 = insertPatchSet(c);
- testRepo.branch(c.getDest().get()).update(parseCommit(ps1));
+ ctl = incrementPatchSet(ctl);
+ PatchSet ps2 = psUtil.current(db, ctl.getNotes());
+ testRepo.branch(ctl.getChange().getDest().get())
+ .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev1)));
FixInput fix = new FixInput();
fix.expectMergedAs = rev1;
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "Expected merged commit " + rev1 + " corresponds to patch set "
- + ps1.getId() + ", which is not the current patch set " + ps2.getId());
+ assertProblems(
+ ctl, fix,
+ problem(
+ "Expected merged commit " + rev1 + " corresponds to patch set "
+ + ps1.getId() + ", which is not the current patch set "
+ + ps2.getId()));
}
@Test
public void expectedMergedCommitWithMismatchedChangeId() throws Exception {
- Change c = insertChange();
- c.setStatus(Change.Status.MERGED);
+ ChangeControl ctl = insertChange();
+ String dest = ctl.getChange().getDest().get();
RevCommit parent =
- testRepo.branch(c.getDest().get()).commit().message("parent").create();
- PatchSet ps = insertPatchSet(c);
- RevCommit commit = parseCommit(ps);
+ testRepo.branch(dest).commit().message("parent").create();
+ String rev = psUtil.current(db, ctl.getNotes()).getRevision().get();
+ RevCommit commit =
+ testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+ testRepo.branch(dest).update(commit);
String badId = "I0000000000000000000000000000000000000000";
RevCommit mergedAs = testRepo.commit().parent(parent)
@@ -567,85 +607,140 @@
+ "Change-Id: " + badId + "\n")
.create();
testRepo.getRevWalk().parseBody(mergedAs);
- testRepo.update(c.getDest().get(), mergedAs);
+ assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID))
+ .containsExactly(badId);
+ testRepo.update(dest, mergedAs);
+
+ assertNoProblems(ctl, null);
FixInput fix = new FixInput();
fix.expectMergedAs = mergedAs.name();
- List<ProblemInfo> problems = checker.check(c, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "Expected merged commit " + mergedAs.name() + " has Change-Id: "
- + badId + ", but expected " + c.getKey().get());
+ assertProblems(
+ ctl, fix,
+ problem(
+ "Expected merged commit " + mergedAs.name() + " has Change-Id: "
+ + badId + ", but expected " + ctl.getChange().getKey().get()));
}
@Test
public void expectedMergedCommitMatchesMultiplePatchSets()
throws Exception {
- Change c1 = insertChange();
- c1.setStatus(Change.Status.MERGED);
- insertPatchSet(c1);
+ ChangeControl ctl1 = insertChange();
+ PatchSet.Id psId1 = psUtil.current(db, ctl1.getNotes()).getId();
+ String dest = ctl1.getChange().getDest().get();
+ String rev = psUtil.current(db, ctl1.getNotes()).getRevision().get();
+ RevCommit commit =
+ testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev));
+ testRepo.branch(dest).update(commit);
- RevCommit commit = testRepo.branch(c1.getDest().get()).commit().create();
- Change c2 = insertChange();
- PatchSet ps2 = newPatchSet(c2.currentPatchSetId(), commit, adminId);
- updatePatchSetRef(ps2);
- db.patchSets().insert(singleton(ps2));
+ ChangeControl ctl2 = insertChange();
+ ctl2 = incrementPatchSet(ctl2, commit);
+ PatchSet.Id psId2 = psUtil.current(db, ctl2.getNotes()).getId();
- Change c3 = insertChange();
- PatchSet ps3 = newPatchSet(c3.currentPatchSetId(), commit, adminId);
- updatePatchSetRef(ps3);
- db.patchSets().insert(singleton(ps3));
+ ChangeControl ctl3 = insertChange();
+ ctl3 = incrementPatchSet(ctl3, commit);
+ PatchSet.Id psId3 = psUtil.current(db, ctl3.getNotes()).getId();
FixInput fix = new FixInput();
fix.expectMergedAs = commit.name();
- List<ProblemInfo> problems = checker.check(c1, fix).problems();
- assertThat(problems).hasSize(1);
- ProblemInfo p = problems.get(0);
- assertThat(p.message).isEqualTo(
- "Multiple patch sets for expected merged commit " + commit.name()
- + ": [" + ps2.getId() + ", " + ps3.getId() + "]");
+ assertProblems(
+ ctl1, fix,
+ problem(
+ "Multiple patch sets for expected merged commit " + commit.name()
+ + ": [" + psId1 + ", " + psId2 + ", " + psId3 + "]"));
}
- private Change insertChange() throws Exception {
- Change c = newChange(project, adminId);
- db.changes().insert(singleton(c));
- indexer.index(db, c);
-
- ChangeUpdate u = changeUpdateFactory.create(
- changeControlFactory.controlFor(db, c, userFactory.create(adminId)));
- u.setBranch(c.getDest().get());
- u.setChangeId(c.getKey().get());
- u.commit();
-
- return c;
+ private BatchUpdate newUpdate(Account.Id owner) {
+ return updateFactory.create(
+ db, project, userFactory.create(owner), TimeUtil.nowTs());
}
- private void incrementPatchSet(Change c) throws Exception {
- TestChanges.incrementPatchSet(c);
- db.changes().upsert(singleton(c));
+ private ChangeControl insertChange() throws Exception {
+ return insertChange(admin);
}
- private PatchSet insertPatchSet(Change c) throws Exception {
- db.changes().upsert(singleton(c));
- RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
- .parent(tip).message("Change " + c.getId().get()).create();
- PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
- updatePatchSetRef(ps);
+
+ private ChangeControl insertChange(TestAccount owner) throws Exception {
+ return insertChange(owner, "refs/heads/master");
+ }
+
+ private ChangeControl insertChange(TestAccount owner, String dest)
+ throws Exception {
+ Change.Id id = new Change.Id(sequences.nextChangeId());
+ ChangeInserter ins;
+ try (BatchUpdate bu = newUpdate(owner.getId())) {
+ RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+ ins = changeInserterFactory
+ .create(id, commit, dest)
+ .setValidatePolicy(CommitValidators.Policy.NONE)
+ .setNotify(NotifyHandling.NONE)
+ .setFireRevisionCreated(false)
+ .setSendMail(false);
+ bu.insertChange(ins).execute();
+ }
+ // Return control for admin regardless of owner.
+ return changeControlFactory.controlFor(
+ db, ins.getChange(), userFactory.create(adminId));
+ }
+
+ private PatchSet.Id nextPatchSetId(ChangeControl ctl) {
+ return ChangeUtil.nextPatchSetId(ctl.getChange().currentPatchSetId());
+ }
+
+ private ChangeControl incrementPatchSet(ChangeControl ctl) throws Exception {
+ return incrementPatchSet(ctl, patchSetCommit(nextPatchSetId(ctl)));
+ }
+
+ private ChangeControl incrementPatchSet(ChangeControl ctl,
+ RevCommit commit) throws Exception {
+ PatchSetInserter ins;
+ try (BatchUpdate bu = newUpdate(ctl.getChange().getOwner())) {
+ ins = patchSetInserterFactory.create(ctl, nextPatchSetId(ctl), commit)
+ .setValidatePolicy(CommitValidators.Policy.NONE)
+ .setFireRevisionCreated(false)
+ .setSendMail(false);
+ bu.addOp(ctl.getId(), ins).execute();
+ }
+ return reload(ctl);
+ }
+
+ private ChangeControl reload(ChangeControl ctl) throws Exception {
+ return changeControlFactory.controlFor(
+ db, ctl.getChange().getProject(), ctl.getId(), ctl.getUser());
+ }
+
+ private RevCommit patchSetCommit(PatchSet.Id psId) throws Exception {
+ RevCommit c = testRepo
+ .commit()
+ .parent(tip)
+ .message("Change " + psId)
+ .create();
+ return testRepo.parseBody(c);
+ }
+
+ private PatchSet insertMissingPatchSet(ChangeControl ctl, String rev)
+ throws Exception {
+ // Don't use BatchUpdate since we're manually updating the meta ref rather
+ // than using ChangeUpdate.
+ String subject = "Subject for missing commit";
+ Change c = new Change(ctl.getChange());
+ PatchSet.Id psId = nextPatchSetId(ctl);
+ c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
+
+ PatchSet ps = newPatchSet(psId, rev, adminId);
db.patchSets().insert(singleton(ps));
- return ps;
- }
+ db.changes().update(singleton(c));
- private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
- PatchSet ps = newPatchSet(c.currentPatchSetId(),
- ObjectId.fromString(id), adminId);
- db.patchSets().insert(singleton(ps));
- return ps;
- }
+ addNoteDbCommit(
+ c.getId(),
+ "Update patch set " + psId.get() + "\n"
+ + "\n"
+ + "Patch-set: " + psId.get() + "\n"
+ + "Commit: " + rev + "\n"
+ + "Subject: " + subject + "\n");
+ indexer.index(db, c.getProject(), c.getId());
- private void updatePatchSetRef(PatchSet ps) throws Exception {
- testRepo.update(ps.getId().toRefName(),
- ObjectId.fromString(ps.getRevision().get()));
+ return ps;
}
private void deleteRef(String refName) throws Exception {
@@ -654,22 +749,82 @@
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
- private RevCommit parseCommit(PatchSet ps) throws Exception {
- RevCommit commit = testRepo.getRevWalk()
- .parseCommit(ObjectId.fromString(ps.getRevision().get()));
- testRepo.getRevWalk().parseBody(commit);
- return commit;
+ private void addNoteDbCommit(Change.Id id, String commitMessage)
+ throws Exception {
+ if (!notesMigration.commitChangeWrites()) {
+ return;
+ }
+ PersonIdent committer = serverIdent.get();
+ PersonIdent author = noteUtil.newIdent(
+ accountCache.get(admin.getId()).getAccount(),
+ committer.getWhen(),
+ committer,
+ anonymousCowardName);
+ testRepo.branch(RefNames.changeMetaRef(id))
+ .commit()
+ .author(author)
+ .committer(committer)
+ .message(commitMessage)
+ .create();
}
- private void assertProblems(Change c, String... expected) {
- assertThat(Lists.transform(checker.check(c).problems(),
- new Function<ProblemInfo, String>() {
- @Override
- public String apply(ProblemInfo in) {
- checkArgument(in.status == null,
- "Status is not null: " + in.message);
- return in.message;
- }
- })).containsExactly((Object[]) expected);
+ private ObjectId getDestRef(ChangeControl ctl) throws Exception {
+ return testRepo.getRepository()
+ .exactRef(ctl.getChange().getDest().get())
+ .getObjectId();
+ }
+
+ private ChangeControl mergeChange(ChangeControl ctl) throws Exception {
+ final ObjectId oldId = getDestRef(ctl);
+ final ObjectId newId = ObjectId.fromString(
+ psUtil.current(db, ctl.getNotes()).getRevision().get());
+ final String dest = ctl.getChange().getDest().get();
+
+ try (BatchUpdate bu = newUpdate(adminId)) {
+ bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+ @Override
+ public void updateRepo(RepoContext ctx) throws IOException {
+ ctx.addRefUpdate(new ReceiveCommand(oldId, newId, dest));
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException {
+ ctx.getChange().setStatus(Change.Status.MERGED);
+ ctx.getUpdate(ctx.getChange().currentPatchSetId())
+ .fixStatus(Change.Status.MERGED);
+ return true;
+ }
+ });
+ bu.execute();
+ }
+ return reload(ctl);
+ }
+
+ private static ProblemInfo problem(String message) {
+ ProblemInfo p = new ProblemInfo();
+ p.message = message;
+ return p;
+ }
+
+ private static ProblemInfo problem(String message,
+ ProblemInfo.Status status, String outcome) {
+ ProblemInfo p = problem(message);
+ p.status = checkNotNull(status);
+ p.outcome = checkNotNull(outcome);
+ return p;
+ }
+
+ private void assertProblems(ChangeControl ctl, @Nullable FixInput fix,
+ ProblemInfo first, ProblemInfo... rest) {
+ List<ProblemInfo> expected = new ArrayList<>(1 + rest.length);
+ expected.add(first);
+ expected.addAll(Arrays.asList(rest));
+ assertThat(checker.check(ctl, fix).problems())
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ }
+
+ private void assertNoProblems(ChangeControl ctl, @Nullable FixInput fix) {
+ assertThat(checker.check(ctl, fix).problems()).isEmpty();
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
index 30a23bf..ca61b1d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
@@ -14,15 +14,22 @@
package com.google.gerrit.extensions.api.changes;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.DefaultInput;
public class AddReviewerInput {
@DefaultInput
public String reviewer;
public Boolean confirmed;
+ public ReviewerState state;
public boolean confirmed() {
return (confirmed != null) ? confirmed : false;
}
-}
+ public ReviewerState state() {
+ return (state != null) ? state : REVIEWER;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
index c29a635..10f74ff84 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.AccountInfo;
import java.util.List;
@@ -49,6 +50,14 @@
public List<ReviewerInfo> reviewers;
/**
+ * List of accounts CCed on the change. The size of this list may be
+ * greater than one (e.g. when a group is CCed). Null if no accounts were CCed
+ * or if reviewers is non-null.
+ */
+ @Nullable
+ public List<AccountInfo> ccs;
+
+ /**
* Constructs a partially initialized result for the given reviewer.
*
* @param input String identifier of an account or group, from user request
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
index 8b626b7..9d94f50 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
@@ -16,6 +16,22 @@
import com.google.gerrit.extensions.client.Comment;
+import java.util.Objects;
+
public class DraftInput extends Comment {
public String tag;
+
+ @Override
+ public boolean equals(Object o) {
+ if (super.equals(o)) {
+ DraftInput di = (DraftInput) o;
+ return Objects.equals(tag, di.tag);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), tag);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 3641ac5..2536c46 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -35,6 +35,11 @@
DiffInfo diff(String base) throws RestApiException;
/**
+ * @param parent 1-based parent number to diff against
+ */
+ DiffInfo diff(int parent) throws RestApiException;
+
+ /**
* Creates a request to retrieve the diff. On the returned request formatting
* options for the diff can be set.
*/
@@ -106,6 +111,11 @@
}
@Override
+ public DiffInfo diff(int parent) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public DiffRequest diffRequest() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 9749e18..cbe16ed 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -14,9 +14,13 @@
package com.google.gerrit.extensions.api.changes;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.DefaultInput;
+import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -68,6 +72,11 @@
*/
public String onBehalfOf;
+ /**
+ * Reviewers that should be added to this change.
+ */
+ public List<AddReviewerInput> reviewers;
+
public enum DraftHandling {
/** Delete pending drafts on this revision only. */
DELETE,
@@ -112,6 +121,23 @@
return label(name, (short) 1);
}
+ public ReviewInput reviewer(String reviewer) {
+ return reviewer(reviewer, REVIEWER, false);
+ }
+
+ public ReviewInput reviewer(String reviewer, ReviewerState state,
+ boolean confirmed) {
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = reviewer;
+ input.state = state;
+ input.confirmed = confirmed;
+ if (reviewers == null) {
+ reviewers = new ArrayList<>();
+ }
+ reviewers.add(input);
+ return this;
+ }
+
public static ReviewInput recommend() {
return new ReviewInput().label("Code-Review", 1);
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
new file mode 100644
index 0000000..b9de2e1
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+
+import java.util.Map;
+
+/**
+ * Result object representing the outcome of a review request.
+ */
+public class ReviewResult {
+ /**
+ * Map of labels to values after the review was posted. Null if any
+ * reviewer additions were rejected.
+ */
+ @Nullable
+ public Map<String, Short> labels;
+
+ /**
+ * Map of account or group identifier to outcome of adding as a reviewer.
+ * Null if no reviewer additions were requested.
+ */
+ @Nullable
+ public Map<String, AddReviewerResult> reviewers;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index b23c7f9..2731476 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -46,6 +46,7 @@
Map<String, FileInfo> files() throws RestApiException;
Map<String, FileInfo> files(String base) throws RestApiException;
+ Map<String, FileInfo> files(int parentNum) throws RestApiException;
FileApi file(String path);
MergeableInfo mergeable() throws RestApiException;
MergeableInfo mergeableOtherBranches() throws RestApiException;
@@ -147,6 +148,11 @@
}
@Override
+ public Map<String, FileInfo> files(int parentNum) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public Map<String, FileInfo> files() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index b9863d7..7c8a3e8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.client;
import java.sql.Timestamp;
+import java.util.Objects;
public abstract class Comment {
/**
@@ -27,6 +28,7 @@
public String id;
public String path;
public Side side;
+ public Integer parent;
public Integer line;
public Range range;
public String inReplyTo;
@@ -38,5 +40,49 @@
public int startCharacter;
public int endLine;
public int endCharacter;
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Range) {
+ Range r = (Range) o;
+ return Objects.equals(startLine, r.startLine)
+ && Objects.equals(startCharacter, r.startCharacter)
+ && Objects.equals(endLine, r.endLine)
+ && Objects.equals(endCharacter, r.endCharacter);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(startLine, startCharacter, endLine, endCharacter);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o != null && getClass() == o.getClass()) {
+ Comment c = (Comment) o;
+ return Objects.equals(patchSet, c.patchSet)
+ && Objects.equals(id, c.id)
+ && Objects.equals(path, c.path)
+ && Objects.equals(side, c.side)
+ && Objects.equals(parent, c.parent)
+ && Objects.equals(line, c.line)
+ && Objects.equals(range, c.range)
+ && Objects.equals(inReplyTo, c.inReplyTo)
+ && Objects.equals(updated, c.updated)
+ && Objects.equals(message, c.message);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(patchSet, id, path, side, parent, line, range,
+ inReplyTo, updated, message);
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
index 3485b8b..e077df2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
@@ -19,11 +19,10 @@
REVISION;
public static Side fromShort(short s) {
- switch (s) {
- case 0:
- return PARENT;
- case 1:
- return REVISION;
+ if (s <= 0) {
+ return PARENT;
+ } else if (s == 1) {
+ return REVISION;
}
return null;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
index b7535e1..166aaa2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -16,7 +16,24 @@
import com.google.gerrit.extensions.client.Comment;
+import java.util.Objects;
+
public class CommentInfo extends Comment {
public AccountInfo author;
public String tag;
+
+ @Override
+ public boolean equals(Object o) {
+ if (super.equals(o)) {
+ CommentInfo ci = (CommentInfo) o;
+ return Objects.equals(author, ci.author)
+ && Objects.equals(tag, ci.tag);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), author, tag);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index 4dd910d..ff04fdc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.common;
+import java.util.Objects;
+
public class ProblemInfo {
public enum Status {
FIXED, FIX_FAILED
@@ -24,6 +26,22 @@
public String outcome;
@Override
+ public int hashCode() {
+ return Objects.hash(message, status, outcome);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ProblemInfo)) {
+ return false;
+ }
+ ProblemInfo p = (ProblemInfo) o;
+ return Objects.equals(message, p.message)
+ && Objects.equals(status, p.status)
+ && Objects.equals(outcome, p.outcome);
+ }
+
+ @Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getSimpleName())
.append('[').append(message);
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index c8694d4..cd172a7 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -39,6 +39,7 @@
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
@@ -46,7 +47,10 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.mail.AddKeySender;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -87,6 +91,8 @@
private final GerritPublicKeyChecker.Factory checkerFactory;
private final AddKeySender.Factory addKeyFactory;
private final AccountCache accountCache;
+ private final AccountIndexCollection accountIndexes;
+ private final Provider<InternalAccountQuery> accountQueryProvider;
@Inject
PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
@@ -95,7 +101,9 @@
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory,
- AccountCache accountCache) {
+ AccountCache accountCache,
+ AccountIndexCollection accountIndexes,
+ Provider<InternalAccountQuery> accountQueryProvider) {
this.serverIdent = serverIdent;
this.db = db;
this.self = self;
@@ -103,6 +111,8 @@
this.checkerFactory = checkerFactory;
this.addKeyFactory = addKeyFactory;
this.accountCache = accountCache;
+ this.accountIndexes = accountIndexes;
+ this.accountQueryProvider = accountQueryProvider;
}
@Override
@@ -122,15 +132,28 @@
for (PGPPublicKeyRing keyRing : newKeys) {
PGPPublicKey key = keyRing.getPublicKey();
AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
- AccountExternalId existing = db.get().accountExternalIds().get(extIdKey);
- if (existing != null) {
- if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) {
- throw new ResourceConflictException(
- "GPG key already associated with another account");
+ if (accountIndexes.getSearchIndex() != null) {
+ Account account = getAccountByExternalId(extIdKey.get());
+ if (account != null) {
+ if (!account.getId().equals(rsrc.getUser().getAccountId())) {
+ throw new ResourceConflictException(
+ "GPG key already associated with another account");
+ }
+ } else {
+ newExtIds.add(
+ new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
}
} else {
- newExtIds.add(
- new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
+ AccountExternalId existing = db.get().accountExternalIds().get(extIdKey);
+ if (existing != null) {
+ if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) {
+ throw new ResourceConflictException(
+ "GPG key already associated with another account");
+ }
+ } else {
+ newExtIds.add(
+ new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
+ }
}
}
@@ -257,6 +280,33 @@
BaseEncoding.base16().encode(fp));
}
+ private Account getAccountByExternalId(String externalId)
+ throws OrmException {
+ List<AccountState> accountStates =
+ accountQueryProvider.get().byExternalId(externalId);
+
+ if (accountStates.isEmpty()) {
+ return null;
+ }
+
+ if (accountStates.size() > 1) {
+ StringBuilder msg = new StringBuilder();
+ msg.append("GPG key ").append(externalId)
+ .append(" associated with multiple accounts: ");
+ Joiner.on(", ").appendTo(msg,
+ Lists.transform(accountStates, new Function<AccountState, String>() {
+ @Override
+ public String apply(AccountState accountState) {
+ return accountState.getAccount().getId().toString();
+ }
+ }));
+ log.error(msg.toString());
+ throw new IllegalStateException(msg.toString());
+ }
+
+ return accountStates.get(0).getAccount();
+ }
+
private Map<String, GpgKeyInfo> toJson(
Collection<PGPPublicKeyRing> keys,
Set<Fingerprint> deleted, PublicKeyStore store, IdentifiedUser user)
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 053b8c5..9eea93e 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -333,12 +333,22 @@
revisionInfo.takeFromEdit(edit);
return revisionInfo;
}
+ public static RevisionInfo forParent(int number, CommitInfo commit) {
+ RevisionInfo revisionInfo = createObject().cast();
+ revisionInfo.takeFromParent(number, commit);
+ return revisionInfo;
+ }
private native void takeFromEdit(EditInfo edit) /*-{
this._number = 0;
this.name = edit.name;
this.commit = edit.commit;
this.edit_base = edit.base_revision;
}-*/;
+ private native void takeFromParent(int number, CommitInfo commit) /*-{
+ this._number = number;
+ this.commit = commit;
+ this.name = this._number;
+ }-*/;
public final native int _number() /*-{ return this._number; }-*/;
public final native String name() /*-{ return this.name; }-*/;
public final native boolean draft() /*-{ return this.draft || false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 340460f..8da949c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -981,24 +981,25 @@
final List<NativeMap<JsArray<CommentInfo>>> comments,
final List<NativeMap<JsArray<CommentInfo>>> drafts) {
DiffApi.list(changeId.get(),
- base != null ? base.name() : null,
- rev.name(),
- group.add(new AsyncCallback<NativeMap<FileInfo>>() {
- @Override
- public void onSuccess(NativeMap<FileInfo> m) {
- files.set(
- base != null ? new PatchSet.Id(changeId, base._number()) : null,
- new PatchSet.Id(changeId, rev._number()),
- style, reply, fileTableMode, edit != null);
- files.setValue(m, myLastReply,
- comments != null ? comments.get(0) : null,
- drafts != null ? drafts.get(0) : null);
- }
+ rev.name(),
+ base,
+ group.add(
+ new AsyncCallback<NativeMap<FileInfo>>() {
+ @Override
+ public void onSuccess(NativeMap<FileInfo> m) {
+ files.set(
+ base != null ? new PatchSet.Id(changeId, base._number()) : null,
+ new PatchSet.Id(changeId, rev._number()),
+ style, reply, fileTableMode, edit != null);
+ files.setValue(m, myLastReply,
+ comments != null ? comments.get(0) : null,
+ drafts != null ? drafts.get(0) : null);
+ }
- @Override
- public void onFailure(Throwable caught) {
- }
- }));
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
}
private List<NativeMap<JsArray<CommentInfo>>> loadComments(
@@ -1117,7 +1118,6 @@
}
/**
- *
* Resolve a revision or patch set id string to RevisionInfo.
* When this view is created from the changes table, revision
* is passed as a real revision.
@@ -1131,8 +1131,17 @@
*/
private RevisionInfo resolveRevisionOrPatchSetId(ChangeInfo info,
String revOrId, String defaultValue) {
+ int parentNum;
if (revOrId == null) {
revOrId = defaultValue;
+ } else if ((parentNum = toParentNum(revOrId)) > 0) {
+ CommitInfo commitInfo = info.revision(revision).commit();
+ if (commitInfo != null) {
+ JsArray<CommitInfo> parents = commitInfo.parents();
+ if (parents.length() >= parentNum) {
+ return RevisionInfo.forParent(-parentNum, parents.get(parentNum - 1));
+ }
+ }
} else if (!info.revisions().containsKey(revOrId)) {
JsArray<RevisionInfo> list = info.revisions().values();
for (int i = 0; i < list.length(); i++) {
@@ -1389,9 +1398,20 @@
RevisionInfo rev = info.revisions().get(revision);
JsArray<CommitInfo> parents = rev.commit().parents();
- diffBase.addItem(
- parents.length() > 1 ? Util.C.autoMerge() : Util.C.baseDiffItem(),
- "");
+ if (parents.length() > 1) {
+ diffBase.addItem(Util.C.autoMerge(), "");
+ for (int i = 0; i < parents.length(); i++) {
+ int parentNum = i + 1;
+ diffBase.addItem(Util.M.diffBaseParent(parentNum),
+ String.valueOf(-parentNum));
+ }
+ int parentNum = toParentNum(base);
+ if (parentNum > 0) {
+ selectedIdx = list.length() + parentNum;
+ }
+ } else {
+ diffBase.addItem(Util.C.baseDiffItem(), "");
+ }
diffBase.setSelectedIndex(selectedIdx);
}
@@ -1443,4 +1463,22 @@
private static String normalize(String r) {
return r != null && !r.isEmpty() ? r : null;
}
+
+ /**
+ * @param parentToken
+ * @return 1-based parentNum if parentToken is a String which can be parsed as
+ * a negative integer i.e. "-1", "-2", etc. If parentToken cannot be
+ * parsed as a negative integer, return zero.
+ */
+ private static int toParentNum(String parentToken) {
+ try {
+ int n = Integer.parseInt(parentToken);
+ if (n < 0) {
+ return -n;
+ }
+ return 0;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 60d66e0..f0a7ce3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -36,6 +36,7 @@
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -712,8 +713,8 @@
}
private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
- JsArray<CommentInfo> cList = get(info.path(), comments);
- JsArray<CommentInfo> dList = get(info.path(), drafts);
+ JsArray<CommentInfo> cList = filterForParent(get(info.path(), comments));
+ JsArray<CommentInfo> dList = filterForParent(get(info.path(), drafts));
sb.openTd().setStyleName(R.css().draftColumn());
if (dList.length() > 0) {
@@ -747,6 +748,20 @@
sb.closeTd();
}
+ private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
+ JsArray<CommentInfo> result = JsArray.createArray().cast();
+ for (CommentInfo c : Natives.asList(list)) {
+ if (c.side() == Side.REVISION) {
+ result.push(c);
+ } else if (base == null && !c.hasParent()) {
+ result.push(c);
+ } else if (base != null && c.parent() == -base.get()) {
+ result.push(c);
+ }
+ }
+ return result;
+ }
+
private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
JsArray<CommentInfo> r = null;
if (m != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index c5397ee..b192bd5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -42,4 +42,6 @@
String changeQueryPageTitle(String query);
String insertionsAndDeletions(int insertions, int deletions);
+
+ String diffBaseParent(int parentNum);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index f0d7e59..2b68492 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -23,3 +23,5 @@
changeQueryPageTitle = Search for {0}
insertionsAndDeletions = +{0}, -{1}
+
+diffBaseParent = Parent {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 8e73f73..d42c344 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -25,9 +25,15 @@
public class CommentInfo extends JavaScriptObject {
public static CommentInfo create(String path, Side side,
int line, CommentRange range) {
+ return create(path, side, 0, line, range);
+ }
+
+ public static CommentInfo create(String path, Side side, int parent,
+ int line, CommentRange range) {
CommentInfo n = createObject().cast();
n.path(path);
n.side(side);
+ n.parent(parent);
if (range != null) {
n.line(range.endLine());
n.range(range);
@@ -41,6 +47,7 @@
CommentInfo n = createObject().cast();
n.path(r.path());
n.side(r.side());
+ n.parent(r.parent());
n.inReplyTo(r.id());
if (r.hasRange()) {
n.line(r.range().endLine());
@@ -55,6 +62,7 @@
CommentInfo n = createObject().cast();
n.path(s.path());
n.side(s.side());
+ n.parent(s.parent());
n.id(s.id());
n.inReplyTo(s.inReplyTo());
n.message(s.message());
@@ -78,6 +86,8 @@
sideRaw(side.toString());
}
private native void sideRaw(String s) /*-{ this.side = s }-*/;
+ public final native void parent(int n) /*-{ this.parent = n }-*/;
+ public final native boolean hasParent() /*-{ return this.hasOwnProperty('parent') }-*/;
public final native String path() /*-{ return this.path }-*/;
public final native String id() /*-{ return this.id }-*/;
@@ -91,6 +101,7 @@
: Side.REVISION;
}
private native String sideRaw() /*-{ return this.side }-*/;
+ public final native int parent() /*-{ return this.parent }-*/;
public final Timestamp updated() {
Timestamp r = updatedTimestamp();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index a26b1ce..2f3ead3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -129,16 +129,29 @@
}
Side getStoredSideFromDisplaySide(DisplaySide side) {
- return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
+ if (side == DisplaySide.A && (base == null || base.get() < 0)) {
+ return Side.PARENT;
+ }
+ return Side.REVISION;
+ }
+
+ int getParentNumFromDisplaySide(DisplaySide side) {
+ if (side == DisplaySide.A && base != null && base.get() < 0) {
+ return -base.get();
+ }
+ return 0;
}
PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
- return side == DisplaySide.A && base != null ? base : revision;
+ if (side == DisplaySide.A && base != null && base.get() >= 0) {
+ return base;
+ }
+ return revision;
}
DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
if (info.side() == Side.PARENT) {
- return base == null ? DisplaySide.A : null;
+ return (base == null || base.get() < 0) ? DisplaySide.A : null;
}
return forSide;
}
@@ -179,6 +192,7 @@
addDraftBox(side, CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(side),
+ getParentNumFromDisplaySide(side),
line,
null)).setEdit(true);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index 83f74a3..ce1d294 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -20,6 +20,7 @@
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -46,13 +47,13 @@
}
void load(CallbackGroup group) {
- if (base != null) {
+ if (base != null && base.get() > 0) {
CommentApi.comments(base, group.add(publishedBase()));
}
CommentApi.comments(revision, group.add(publishedRevision()));
if (Gerrit.isSignedIn()) {
- if (base != null) {
+ if (base != null && base.get() > 0) {
CommentApi.drafts(base, group.add(draftsBase()));
}
CommentApi.drafts(revision, group.add(draftsRevision()));
@@ -60,7 +61,7 @@
}
boolean hasCommentForPath(String filePath) {
- if (base != null) {
+ if (base != null && base.get() > 0) {
JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
if (forBase != null && forBase.length() > 0) {
return true;
@@ -91,6 +92,9 @@
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+ for (String k : result.keySet()) {
+ result.put(k, filterForParent(result.get(k)));
+ }
publishedRevisionAll = result;
publishedRevision = sort(result.get(path));
}
@@ -101,6 +105,20 @@
};
}
+ private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
+ JsArray<CommentInfo> result = JsArray.createArray().cast();
+ for (CommentInfo c : Natives.asList(list)) {
+ if (c.side() == Side.REVISION) {
+ result.push(c);
+ } else if (base == null && !c.hasParent()) {
+ result.push(c);
+ } else if (base != null && c.parent() == -base.get()) {
+ result.push(c);
+ }
+ }
+ return result;
+ }
+
private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
@@ -118,6 +136,9 @@
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+ for (String k : result.keySet()) {
+ result.put(k, filterForParent(result.get(k)));
+ }
draftsRevision = sort(result.get(path));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index bc5a305..e3720cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
@@ -25,11 +26,15 @@
import com.google.gwt.user.client.rpc.AsyncCallback;
public class DiffApi {
- public static void list(int id, String base, String revision,
+ public static void list(int id, String revision, RevisionInfo base,
AsyncCallback<NativeMap<FileInfo>> cb) {
RestApi api = ChangeApi.revision(id, revision).view("files");
if (base != null) {
- api.addParameter("base", base);
+ if (base._number() < 0) {
+ api.addParameter("parent", -base._number());
+ } else {
+ api.addParameter("base", base.name());
+ }
}
api.get(NativeMap.copyKeysIntoChildren("path", cb));
}
@@ -38,7 +43,11 @@
AsyncCallback<NativeMap<FileInfo>> cb) {
RestApi api = ChangeApi.revision(id).view("files");
if (base != null) {
- api.addParameter("base", base.get());
+ if (base.get() < 0) {
+ api.addParameter("parent", -base.get());
+ } else {
+ api.addParameter("base", base.get());
+ }
}
api.get(NativeMap.copyKeysIntoChildren("path", cb));
}
@@ -57,7 +66,11 @@
public DiffApi base(PatchSet.Id id) {
if (id != null) {
- call.addParameter("base", id.get());
+ if (id.get() < 0) {
+ call.addParameter("parent", -id.get());
+ } else {
+ call.addParameter("base", id.get());
+ }
}
return this;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index 2264871..8935e36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -27,6 +27,7 @@
import com.google.gerrit.client.diff.DiffInfo.FileMeta;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
@@ -116,6 +117,7 @@
private List<HandlerRegistration> handlers;
private PreferencesAction prefsAction;
private int reloadVersionId;
+ private int parents;
@UiField(provided = true)
Header header;
@@ -213,6 +215,8 @@
new CommentsCollections(base, revision, path);
comments.load(group2);
+ countParents(group2);
+
RestApi call = ChangeApi.detail(changeId.get());
ChangeList.addOptions(call, EnumSet.of(
ListChangesOption.ALL_REVISIONS));
@@ -231,7 +235,7 @@
revision.get() == info.revision(currentRevision)._number();
JsArray<RevisionInfo> list = info.revisions().values();
RevisionInfo.sortRevisionInfoByNumber(list);
- getDiffTable().set(prefs, list, diff, edit != null, current,
+ getDiffTable().set(prefs, list, parents, diff, edit != null, current,
changeStatus.isOpen(), diff.binary());
header.setChangeInfo(info);
}
@@ -245,6 +249,22 @@
getScreenLoadCallback(comments)));
}
+ private void countParents(CallbackGroup cbg) {
+ ChangeApi.revision(changeId.get(), revision.getId())
+ .view("commit")
+ .get(cbg.add(new AsyncCallback<CommitInfo>() {
+ @Override
+ public void onSuccess(CommitInfo info) {
+ parents = info.parents().length();
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ parents = 0;
+ }
+ }));
+ }
+
@Override
public void onShowView() {
super.onShowView();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 4374986..392ad2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -108,12 +108,12 @@
patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
}
- void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
+ void set(DiffPreferences prefs, JsArray<RevisionInfo> list, int parents, DiffInfo info,
boolean editExists, boolean current, boolean open, boolean binary) {
this.changeType = info.changeType();
- patchSetSelectBoxA.setUpPatchSetNav(list, info.metaA(), editExists,
+ patchSetSelectBoxA.setUpPatchSetNav(list, parents, info.metaA(), editExists,
current, open, binary);
- patchSetSelectBoxB.setUpPatchSetNav(list, info.metaB(), editExists,
+ patchSetSelectBoxB.setUpPatchSetNav(list, parents, info.metaB(), editExists,
current, open, binary);
JsArrayString hdr = info.diffHeader();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index 39b85cf..bc37abb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -18,6 +18,7 @@
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.blame.BlameInfo;
import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.WebLinkInfo;
import com.google.gerrit.client.patches.PatchUtil;
@@ -87,13 +88,29 @@
this.path = path;
}
- void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta,
+ void setUpPatchSetNav(JsArray<RevisionInfo> list, int parents, DiffInfo.FileMeta meta,
boolean editExists, boolean current, boolean open, boolean binary) {
- InlineHyperlink baseLink = null;
InlineHyperlink selectedLink = null;
if (sideA) {
- baseLink = createLink(PatchUtil.C.patchBase(), null);
- linkPanel.add(baseLink);
+ if (parents <= 1) {
+ InlineHyperlink link = createLink(PatchUtil.C.patchBase(), null);
+ linkPanel.add(link);
+ selectedLink = link;
+ } else {
+ for (int i = parents; i > 0; i--) {
+ PatchSet.Id id = new PatchSet.Id(changeId, -i);
+ InlineHyperlink link = createLink(Util.M.diffBaseParent(i), id);
+ linkPanel.add(link);
+ if (revision != null && id.equals(revision)) {
+ selectedLink = link;
+ }
+ }
+ InlineHyperlink link = createLink(Util.C.autoMerge(), null);
+ linkPanel.add(link);
+ if (selectedLink == null) {
+ selectedLink = link;
+ }
+ }
}
for (int i = 0; i < list.length(); i++) {
RevisionInfo r = list.get(i);
@@ -106,8 +123,6 @@
}
if (selectedLink != null) {
selectedLink.setStyleName(style.selected());
- } else if (sideA) {
- baseLink.setStyleName(style.selected());
}
if (meta == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
index 2b83b71..bcb7dac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -84,6 +84,7 @@
addDraftBox(cm.side(), CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(cm.side()),
+ getParentNumFromDisplaySide(cm.side()),
line,
CommentRange.create(fromTo))).setEdit(true);
cm.setCursor(fromTo.to());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 4dd21ae..f80cc49 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -66,7 +66,7 @@
CmdLineParser clp = parserFactory.create(param);
try {
clp.parseOptionMap(in);
- } catch (CmdLineException e) {
+ } catch (CmdLineException | NumberFormatException e) {
if (!clp.wasHelpRequestedByOption()) {
replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
return false;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 77c466e..b9aab3a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -55,9 +55,11 @@
@Override
public void run() throws IOException {
- ui.header("Index");
-
- IndexType type = index.select("Type", "type", IndexType.LUCENE);
+ IndexType type = IndexType.LUCENE;
+ if (IndexType.values().length > 1) {
+ ui.header("Index");
+ type = index.select("Type", "type", type);
+ }
for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
AbstractLuceneIndex.setReady(
site, def.getName(), def.getLatest().getVersion(), true);
@@ -65,7 +67,10 @@
if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
// Do nothing
} else {
- final String message = String.format(
+ if (IndexType.values().length <= 1) {
+ ui.header("Index");
+ }
+ String message = String.format(
"\nThe index must be %sbuilt before starting Gerrit:\n"
+ " java -jar gerrit.war reindex -d site_path\n",
site.isNew ? "" : "re");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index 1c72600..6739ce0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -19,48 +19,29 @@
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AuthorizedKeys;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.VersionedMetaData;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.FS;
-import java.io.File;
import java.io.IOException;
-import java.nio.file.Path;
import java.util.List;
-public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
+public class VersionedAuthorizedKeysOnInit extends VersionedMetaDataOnInit {
public interface Factory {
VersionedAuthorizedKeysOnInit create(Account.Id accountId);
}
private final Account.Id accountId;
- private final String ref;
- private final String project;
- private final SitePaths site;
- private final InitFlags flags;
-
private List<Optional<AccountSshKey>> keys;
- private ObjectId revision;
@Inject
public VersionedAuthorizedKeysOnInit(
@@ -68,41 +49,19 @@
SitePaths site,
InitFlags flags,
@Assisted Account.Id accountId) {
-
- this.project = allUsers.get();
- this.site = site;
- this.flags = flags;
+ super(flags, site, allUsers.get(), RefNames.refsUsers(accountId));
this.accountId = accountId;
- this.ref = RefNames.refsUsers(accountId);
}
@Override
- protected String getRefName() {
- return ref;
- }
-
public VersionedAuthorizedKeysOnInit load()
throws IOException, ConfigInvalidException {
- File path = getPath();
- if (path != null) {
- try (Repository repo = new FileRepository(path)) {
- load(repo);
- }
- }
+ super.load();
return this;
}
- private File getPath() {
- Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
- if (basePath == null) {
- throw new IllegalStateException("gerrit.basePath must be configured");
- }
- return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
- }
-
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
- revision = getRevision();
keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
}
@@ -116,50 +75,6 @@
return key;
}
- public void save(String message) throws IOException {
- save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
- }
-
- private void save(PersonIdent ident, String msg) throws IOException {
- File path = getPath();
- if (path == null) {
- throw new IOException(project + " does not exist.");
- }
-
- try (Repository repo = new FileRepository(path);
- ObjectInserter i = repo.newObjectInserter();
- ObjectReader r = repo.newObjectReader();
- RevWalk rw = new RevWalk(reader)) {
- inserter = i;
- reader = r;
-
- RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
- newTree = readTree(srcTree);
-
- CommitBuilder commit = new CommitBuilder();
- commit.setAuthor(ident);
- commit.setCommitter(ident);
- commit.setMessage(msg);
-
- onSave(commit);
- ObjectId res = newTree.writeTree(inserter);
- if (res.equals(srcTree)) {
- return;
- }
-
- commit.setTreeId(res);
- if (revision != null) {
- commit.addParentId(revision);
- }
- ObjectId newRevision = inserter.insert(commit);
- updateRef(repo, ident, newRevision, "commit: " + msg);
- revision = newRevision;
- } finally {
- inserter = null;
- reader = null;
- }
- }
-
@Override
protected boolean onSave(CommitBuilder commit) throws IOException {
if (Strings.isNullOrEmpty(commit.getMessage())) {
@@ -169,30 +84,4 @@
saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
return true;
}
-
- private void updateRef(Repository repo, PersonIdent ident,
- ObjectId newRevision, String refLogMsg) throws IOException {
- RefUpdate ru = repo.updateRef(getRefName());
- ru.setRefLogIdent(ident);
- ru.setNewObjectId(newRevision);
- ru.setExpectedOldObjectId(revision);
- ru.setRefLogMessage(refLogMsg, false);
- RefUpdate.Result r = ru.update();
- switch(r) {
- case FAST_FORWARD:
- case NEW:
- case NO_CHANGE:
- break;
- case FORCED:
- case IO_FAILURE:
- case LOCK_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- default:
- throw new IOException("Failed to update " + getRefName() + " of "
- + project + ": " + r.name());
- }
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index f7d9d4a..a7ebd33 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -18,73 +18,32 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GroupList;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.VersionedMetaData;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
import java.io.IOException;
-import java.nio.file.Path;
-public class AllProjectsConfig extends VersionedMetaData {
+public class AllProjectsConfig extends VersionedMetaDataOnInit {
private static final Logger log = LoggerFactory.getLogger(AllProjectsConfig.class);
- private final String project;
- private final SitePaths site;
- private final InitFlags flags;
-
private Config cfg;
- private ObjectId revision;
private GroupList groupList;
@Inject
AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site,
InitFlags flags) {
- this.project = allProjects.get();
- this.site = site;
- this.flags = flags;
+ super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
}
- @Override
- protected String getRefName() {
- return RefNames.REFS_CONFIG;
- }
-
- private File getPath() {
- Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
- if (basePath == null) {
- throw new IllegalStateException("gerrit.basePath must be configured");
- }
- return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
- }
-
- public AllProjectsConfig load() throws IOException, ConfigInvalidException {
- File path = getPath();
- if (path != null) {
- try (Repository repo = new FileRepository(path)) {
- load(repo);
- }
- }
- return this;
- }
-
public Config getConfig() {
return cfg;
}
@@ -94,10 +53,16 @@
}
@Override
+ public AllProjectsConfig load()
+ throws IOException, ConfigInvalidException {
+ super.load();
+ return this;
+ }
+
+ @Override
protected void onLoad() throws IOException, ConfigInvalidException {
groupList = readGroupList();
cfg = readConfig(ProjectConfig.PROJECT_CONFIG);
- revision = getRevision();
}
private GroupList readGroupList() throws IOException {
@@ -105,96 +70,31 @@
GroupList.createLoggerSink(GroupList.FILE_NAME, log));
}
- @Override
- protected boolean onSave(CommitBuilder commit) throws IOException,
- ConfigInvalidException {
- throw new UnsupportedOperationException();
- }
-
- public void save(String message) throws IOException {
- save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
- }
-
- public void save(String pluginName, String message) throws IOException {
+ public void save(String pluginName, String message)
+ throws IOException, ConfigInvalidException {
save(new PersonIdent(pluginName, pluginName + "@gerrit"),
"Update from plugin " + pluginName + ": " + message);
}
- private void save(PersonIdent ident, String msg) throws IOException {
- File path = getPath();
- if (path == null) {
- throw new IOException("All-Projects does not exist.");
- }
-
- try (Repository repo = new FileRepository(path)) {
- inserter = repo.newObjectInserter();
- reader = repo.newObjectReader();
- try (RevWalk rw = new RevWalk(reader)) {
- RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
- newTree = readTree(srcTree);
- saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
- saveGroupList();
- ObjectId res = newTree.writeTree(inserter);
- if (res.equals(srcTree)) {
- // If there are no changes to the content, don't create the commit.
- return;
- }
-
- CommitBuilder commit = new CommitBuilder();
- commit.setAuthor(ident);
- commit.setCommitter(ident);
- commit.setMessage(msg);
- commit.setTreeId(res);
- if (revision != null) {
- commit.addParentId(revision);
- }
- ObjectId newRevision = inserter.insert(commit);
- updateRef(repo, ident, newRevision, "commit: " + msg);
- revision = newRevision;
- } finally {
- if (inserter != null) {
- inserter.close();
- inserter = null;
- }
- if (reader != null) {
- reader.close();
- reader = null;
- }
- }
- }
+ @Override
+ protected void save(PersonIdent ident, String msg)
+ throws IOException, ConfigInvalidException {
+ super.save(ident, msg);
// we need to invalidate the JGit cache if the group list is invalidated in
// an unattended init step
RepositoryCache.clear();
}
- private void saveGroupList() throws IOException {
- saveUTF8(GroupList.FILE_NAME, groupList.asText());
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException,
+ ConfigInvalidException {
+ saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
+ saveGroupList();
+ return true;
}
- private void updateRef(Repository repo, PersonIdent ident,
- ObjectId newRevision, String refLogMsg) throws IOException {
- RefUpdate ru = repo.updateRef(getRefName());
- ru.setRefLogIdent(ident);
- ru.setNewObjectId(newRevision);
- ru.setExpectedOldObjectId(revision);
- ru.setRefLogMessage(refLogMsg, false);
- RefUpdate.Result r = ru.update();
- switch (r) {
- case FAST_FORWARD:
- case NEW:
- case NO_CHANGE:
- break;
- case FORCED:
- case IO_FAILURE:
- case LOCK_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- default:
- throw new IOException("Failed to update " + getRefName() + " of "
- + project + ": " + r.name());
- }
+ private void saveGroupList() throws IOException {
+ saveUTF8(GroupList.FILE_NAME, groupList.asText());
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
new file mode 100644
index 0000000..b953a0b
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init.api;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public abstract class VersionedMetaDataOnInit extends VersionedMetaData {
+
+ private final InitFlags flags;
+ private final SitePaths site;
+ private final String project;
+ private final String ref;
+
+ protected VersionedMetaDataOnInit(InitFlags flags, SitePaths site,
+ String project, String ref) {
+ this.flags = flags;
+ this.site = site;
+ this.project = project;
+ this.ref = ref;
+ }
+
+ @Override
+ protected String getRefName() {
+ return ref;
+ }
+
+ public VersionedMetaDataOnInit load()
+ throws IOException, ConfigInvalidException {
+ File path = getPath();
+ if (path != null) {
+ try (Repository repo = new FileRepository(path)) {
+ load(repo);
+ }
+ }
+ return this;
+ }
+
+ public void save(String message) throws IOException, ConfigInvalidException {
+ save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
+ }
+
+ protected void save(PersonIdent ident, String msg)
+ throws IOException, ConfigInvalidException {
+ File path = getPath();
+ if (path == null) {
+ throw new IOException(project + " does not exist.");
+ }
+
+ try (Repository repo = new FileRepository(path);
+ ObjectInserter i = repo.newObjectInserter();
+ ObjectReader r = repo.newObjectReader();
+ RevWalk rw = new RevWalk(r)) {
+ inserter = i;
+ reader = r;
+
+ RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
+ newTree = readTree(srcTree);
+
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setMessage(msg);
+
+ onSave(commit);
+
+ ObjectId res = newTree.writeTree(inserter);
+ if (res.equals(srcTree)) {
+ return;
+ }
+ commit.setTreeId(res);
+
+ if (revision != null) {
+ commit.addParentId(revision);
+ }
+ ObjectId newRevision = inserter.insert(commit);
+ updateRef(repo, ident, newRevision, "commit: " + msg);
+ revision = rw.parseCommit(newRevision);
+ } finally {
+ inserter = null;
+ reader = null;
+ }
+ }
+
+ private void updateRef(Repository repo, PersonIdent ident,
+ ObjectId newRevision, String refLogMsg) throws IOException {
+ RefUpdate ru = repo.updateRef(getRefName());
+ ru.setRefLogIdent(ident);
+ ru.setNewObjectId(newRevision);
+ ru.setExpectedOldObjectId(revision);
+ ru.setRefLogMessage(refLogMsg, false);
+ RefUpdate.Result r = ru.update();
+ switch(r) {
+ case FAST_FORWARD:
+ case NEW:
+ case NO_CHANGE:
+ break;
+ case FORCED:
+ case IO_FAILURE:
+ case LOCK_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ default:
+ throw new IOException("Failed to update " + getRefName() + " of "
+ + project + ": " + r.name());
+ }
+ }
+
+ private File getPath() {
+ Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+ if (basePath == null) {
+ throw new IllegalStateException("gerrit.basePath must be configured");
+ }
+ return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index a573625..eebf8e0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -27,6 +27,8 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.FakeRealm;
import com.google.gerrit.server.account.GroupCacheImpl;
@@ -157,5 +159,8 @@
bind(ChangeJson.Factory.class).toProvider(
Providers.<ChangeJson.Factory>of(null));
+ bind(AccountVisibility.class)
+ .toProvider(AccountVisibilityProvider.class)
+ .in(SINGLETON);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 9034e47..c493ccd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -100,10 +100,9 @@
PatchListCache plCache = env.getArgs().getPatchListCache();
Change change = getChange(engine);
Project.NameKey project = change.getProject();
- ObjectId a = null;
ObjectId b = ObjectId.fromString(ps.getRevision().get());
Whitespace ws = Whitespace.IGNORE_NONE;
- PatchListKey plKey = new PatchListKey(a, b, ws);
+ PatchListKey plKey = PatchListKey.againstDefaultBase(b, ws);
PatchList patchList;
try {
patchList = plCache.get(plKey, project);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 847d559..902a51c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import com.google.common.annotations.VisibleForTesting;
@@ -51,6 +52,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -122,7 +124,7 @@
}
/**
- * Get all reviewers for a change.
+ * Get all reviewers and CCed accounts for a change.
*
* @param allApprovals all approvals to consider; must all belong to the same
* change.
@@ -151,8 +153,16 @@
ChangeUpdate update, LabelTypes labelTypes, Change change,
Iterable<Account.Id> wantReviewers) throws OrmException {
PatchSet.Id psId = change.currentPatchSetId();
+ Collection<Account.Id> existingReviewers;
+ if (migration.readChanges()) {
+ // If using NoteDB, we only want reviewers in the REVIEWER state.
+ existingReviewers = notes.load().getReviewers().byState(REVIEWER);
+ } else {
+ // Prior to NoteDB, we gather all reviewers regardless of state.
+ existingReviewers = getReviewers(db, notes).all();
+ }
return addReviewers(db, update, labelTypes, change, psId, false, null, null,
- wantReviewers, getReviewers(db, notes).all());
+ wantReviewers, existingReviewers);
}
private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update,
@@ -191,6 +201,30 @@
return Collections.unmodifiableList(cells);
}
+ /**
+ * Adds accounts to a change as reviewers in the CC state.
+ *
+ * @param notes change notes.
+ * @param update change update.
+ * @param wantCCs accounts to CC.
+ * @return whether a change was made.
+ * @throws OrmException
+ */
+ public Collection<Account.Id> addCcs(ChangeNotes notes, ChangeUpdate update,
+ Collection<Account.Id> wantCCs) throws OrmException {
+ return addCcs(update, wantCCs, notes.load().getReviewers());
+ }
+
+ private Collection<Account.Id> addCcs(ChangeUpdate update,
+ Collection<Account.Id> wantCCs, ReviewerSet existingReviewers) {
+ Set<Account.Id> need = new LinkedHashSet<>(wantCCs);
+ need.removeAll(existingReviewers.all());
+ for (Account.Id account : need) {
+ update.putReviewer(account, CC);
+ }
+ return need;
+ }
+
public void addApprovals(ReviewDb db, ChangeUpdate update,
LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
Map<String, Short> approvals) throws OrmException {
@@ -317,6 +351,6 @@
.append(LabelVote.create(e.getKey(), e.getValue()).format());
}
}
- return msgs.append('.').toString();
+ return msgs.toString();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index d990115..603f528 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -365,9 +365,17 @@
"cannot set RevId for patch set %s on comment %s", ps.getId(), c);
if (c.getRevId() == null) {
try {
- c.setRevId(Side.fromShort(c.getSide()) == Side.PARENT
- ? new RevId(ObjectId.toString(cache.getOldId(change, ps)))
- : ps.getRevision());
+ if (Side.fromShort(c.getSide()) == Side.PARENT) {
+ if (c.getSide() < 0) {
+ c.setRevId(new RevId(ObjectId.toString(
+ cache.getOldId(change, ps, -c.getSide()))));
+ } else {
+ c.setRevId(new RevId(ObjectId.toString(
+ cache.getOldId(change, ps, null))));
+ }
+ } else {
+ c.setRevId(ps.getRevision());
+ }
} catch (PatchListNotAvailableException e) {
throw new OrmException(e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 57ffd0a..0856616 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -21,9 +21,12 @@
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
@@ -84,10 +87,16 @@
static class Loader extends CacheLoader<String, Set<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
+ private final AccountIndexCollection accountIndexes;
+ private final Provider<InternalAccountQuery> accountQueryProvider;
@Inject
- Loader(final SchemaFactory<ReviewDb> schema) {
+ Loader(SchemaFactory<ReviewDb> schema,
+ AccountIndexCollection accountIndexes,
+ Provider<InternalAccountQuery> accountQueryProvider) {
this.schema = schema;
+ this.accountIndexes = accountIndexes;
+ this.accountQueryProvider = accountQueryProvider;
}
@Override
@@ -97,9 +106,18 @@
for (Account a : db.accounts().byPreferredEmail(email)) {
r.add(a.getId());
}
- for (AccountExternalId a : db.accountExternalIds()
- .byEmailAddress(email)) {
- r.add(a.getAccountId());
+ if (accountIndexes.getSearchIndex() != null) {
+ for (AccountState accountState : accountQueryProvider.get()
+ .byExternalId(
+ (new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO,
+ email)).get())) {
+ r.add(accountState.getAccount().getId());
+ }
+ } else {
+ for (AccountExternalId a : db.accountExternalIds()
+ .byEmailAddress(email)) {
+ r.add(a.getAccountId());
+ }
}
return ImmutableSet.copyOf(r);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 337cd7c..e348e73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -263,20 +263,7 @@
}
rules = capabilities.getPermission(permissionName);
-
- if (rules.isEmpty()) {
- effective.put(permissionName, rules);
- return rules;
- }
-
GroupMembership groups = user.getEffectiveGroups();
- if (rules.size() == 1) {
- if (!match(groups, rules.get(0))) {
- rules = Collections.emptyList();
- }
- effective.put(permissionName, rules);
- return rules;
- }
List<PermissionRule> mine = new ArrayList<>(rules.size());
for (PermissionRule rule : rules) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 3d40373..e6ca18df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -75,6 +75,15 @@
}
@Override
+ public DiffInfo diff(int parent) throws RestApiException {
+ try {
+ return getDiff.setParent(parent).apply(file).value();
+ } catch (OrmException | InvalidChangeOperationException | IOException e) {
+ throw new RestApiException("Cannot retrieve diff", e);
+ }
+ }
+
+ @Override
public DiffRequest diffRequest() {
return new DiffRequest() {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 4c7adea..6b5e83c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -166,7 +166,7 @@
public void review(ReviewInput in) throws RestApiException {
try {
review.apply(revision, in);
- } catch (OrmException | UpdateException e) {
+ } catch (OrmException | UpdateException | IOException e) {
throw new RestApiException("Cannot post review", e);
}
}
@@ -309,6 +309,17 @@
}
}
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map<String, FileInfo> files(int parentNum) throws RestApiException {
+ try {
+ return (Map<String, FileInfo>) listFiles.setParent(parentNum)
+ .apply(revision).value();
+ } catch (OrmException | IOException e) {
+ throw new RestApiException("Cannot retrieve files", e);
+ }
+ }
+
@Override
public FileApi file(String path) {
return fileApi.create(files.parse(revision,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index ce38186..158758b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -355,7 +355,21 @@
}
private ChangeInfo checkOnly(ChangeData cd) {
- ConsistencyChecker.Result result = checkerProvider.get().check(cd, fix);
+ ChangeControl ctl;
+ try {
+ ctl = cd.changeControl().forUser(userProvider.get());
+ } catch (OrmException e) {
+ String msg = "Error loading change";
+ log.warn(msg + " " + cd.getId(), e);
+ ChangeInfo info = new ChangeInfo();
+ info._number = cd.getId().get();
+ ProblemInfo p = new ProblemInfo();
+ p.message = msg;
+ info.problems = Lists.newArrayList(p);
+ return info;
+ }
+
+ ConsistencyChecker.Result result = checkerProvider.get().check(ctl, fix);
ChangeInfo info;
Change c = result.change();
if (c != null) {
@@ -384,9 +398,11 @@
Optional<PatchSet.Id> limitToPsId) throws PatchListNotAvailableException,
GpgException, OrmException, IOException {
ChangeInfo out = new ChangeInfo();
+ CurrentUser user = userProvider.get();
+ ChangeControl ctl = cd.changeControl().forUser(user);
if (has(CHECK)) {
- out.problems = checkerProvider.get().check(cd.change(), fix).problems();
+ out.problems = checkerProvider.get().check(ctl, fix).problems();
// If any problems were fixed, the ChangeData needs to be reloaded.
for (ProblemInfo p : out.problems) {
if (p.status == ProblemInfo.Status.FIXED) {
@@ -397,8 +413,6 @@
}
Change in = cd.change();
- CurrentUser user = userProvider.get();
- ChangeControl ctl = cd.changeControl().forUser(user);
out.project = in.getProject().get();
out.branch = in.getDest().getShortName();
out.topic = in.getTopic();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 465ce95..f4869be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -18,12 +18,10 @@
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -32,27 +30,22 @@
public class Check implements RestReadView<ChangeResource>,
RestModifyView<ChangeResource, FixInput> {
- private final NotesMigration notesMigration;
private final ChangeJson.Factory jsonFactory;
@Inject
- Check(NotesMigration notesMigration,
- ChangeJson.Factory json) {
- this.notesMigration = notesMigration;
+ Check(ChangeJson.Factory json) {
this.jsonFactory = json;
}
@Override
public Response<ChangeInfo> apply(ChangeResource rsrc)
throws RestApiException, OrmException {
- checkEnabled();
return Response.withMustRevalidate(newChangeJson().format(rsrc));
}
@Override
public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
throws RestApiException, OrmException {
- checkEnabled();
ChangeControl ctl = rsrc.getControl();
if (!ctl.isOwner()
&& !ctl.getProjectControl().isOwner()
@@ -65,10 +58,4 @@
private ChangeJson newChangeJson() {
return jsonFactory.create(EnumSet.of(ListChangesOption.CHECK));
}
-
- private void checkEnabled() throws NotImplementedException {
- if (notesMigration.readChanges()) {
- throw new NotImplementedException("check not implemented for NoteDb");
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index 0af5656..d1ce453 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -126,8 +126,11 @@
}
r.id = Url.encode(c.getKey().get());
r.path = c.getKey().getParentKey().getFileName();
- if (c.getSide() == 0) {
+ if (c.getSide() <= 0) {
r.side = Side.PARENT;
+ if (c.getSide() < 0) {
+ r.parent = -c.getSide();
+ }
}
if (c.getLine() > 0) {
r.line = c.getLine();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index c0a92ba..b16d8b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -14,7 +14,8 @@
package com.google.gerrit.server.change;
-import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
@@ -39,23 +40,22 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.PatchSetState;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -78,9 +78,10 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
/**
* Checks changes for various kinds of inconsistency and corruption.
@@ -94,12 +95,10 @@
@AutoValue
public abstract static class Result {
- private static Result create(Change.Id id, List<ProblemInfo> problems) {
- return new AutoValue_ConsistencyChecker_Result(id, null, problems);
- }
-
- private static Result create(Change c, List<ProblemInfo> problems) {
- return new AutoValue_ConsistencyChecker_Result(c.getId(), c, problems);
+ private static Result create(ChangeControl ctl,
+ List<ProblemInfo> problems) {
+ return new AutoValue_ConsistencyChecker_Result(
+ ctl.getId(), ctl.getChange(), problems);
}
public abstract Change.Id id();
@@ -110,22 +109,20 @@
public abstract List<ProblemInfo> problems();
}
- private final Provider<ReviewDb> db;
- private final GitRepositoryManager repoManager;
- private final NotesMigration notesMigration;
- private final Provider<CurrentUser> user;
- private final Provider<PersonIdent> serverIdent;
- private final PatchSetInfoFactory patchSetInfoFactory;
- private final PatchSetInserter.Factory patchSetInserterFactory;
private final BatchUpdate.Factory updateFactory;
- private final ChangeIndexer indexer;
private final ChangeControl.GenericFactory changeControlFactory;
private final ChangeNotes.Factory notesFactory;
- private final ChangeUpdate.Factory changeUpdateFactory;
private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
+ private final GitRepositoryManager repoManager;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
+ private final PatchSetUtil psUtil;
+ private final Provider<CurrentUser> user;
+ private final Provider<PersonIdent> serverIdent;
+ private final Provider<ReviewDb> db;
private FixInput fix;
- private Change change;
+ private ChangeControl ctl;
private Repository repo;
private RevWalk rw;
@@ -137,67 +134,51 @@
private List<ProblemInfo> problems;
@Inject
- ConsistencyChecker(Provider<ReviewDb> db,
- GitRepositoryManager repoManager,
- NotesMigration notesMigration,
- Provider<CurrentUser> user,
+ ConsistencyChecker(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
- PatchSetInfoFactory patchSetInfoFactory,
- PatchSetInserter.Factory patchSetInserterFactory,
BatchUpdate.Factory updateFactory,
- ChangeIndexer indexer,
ChangeControl.GenericFactory changeControlFactory,
ChangeNotes.Factory notesFactory,
- ChangeUpdate.Factory changeUpdateFactory,
- DynamicItem<AccountPatchReviewStore> accountPatchReviewStore) {
+ DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
+ GitRepositoryManager repoManager,
+ PatchSetInfoFactory patchSetInfoFactory,
+ PatchSetInserter.Factory patchSetInserterFactory,
+ PatchSetUtil psUtil,
+ Provider<CurrentUser> user,
+ Provider<ReviewDb> db) {
+ this.accountPatchReviewStore = accountPatchReviewStore;
+ this.changeControlFactory = changeControlFactory;
this.db = db;
- this.notesMigration = notesMigration;
- this.repoManager = repoManager;
- this.user = user;
- this.serverIdent = serverIdent;
+ this.notesFactory = notesFactory;
this.patchSetInfoFactory = patchSetInfoFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
+ this.psUtil = psUtil;
+ this.repoManager = repoManager;
+ this.serverIdent = serverIdent;
this.updateFactory = updateFactory;
- this.indexer = indexer;
- this.changeControlFactory = changeControlFactory;
- this.notesFactory = notesFactory;
- this.changeUpdateFactory = changeUpdateFactory;
- this.accountPatchReviewStore = accountPatchReviewStore;
+ this.user = user;
reset();
}
private void reset() {
- change = null;
+ ctl = null;
repo = null;
rw = null;
problems = new ArrayList<>();
}
- public Result check(ChangeData cd) {
- return check(cd, null);
+ private Change change() {
+ return ctl.getChange();
}
- public Result check(ChangeData cd, @Nullable FixInput f) {
- reset();
+ public Result check(ChangeControl cc, @Nullable FixInput f) {
+ checkNotNull(cc);
try {
- return check(cd.change(), f);
- } catch (OrmException e) {
- error("Error looking up change", e);
- return Result.create(cd.getId(), problems);
- }
- }
-
- public Result check(Change c) {
- return check(c, null);
- }
-
- public Result check(Change c, @Nullable FixInput f) {
- reset();
- fix = f;
- change = c;
- try {
+ reset();
+ ctl = cc;
+ fix = f;
checkImpl();
- return Result.create(c, problems);
+ return result();
} finally {
if (rw != null) {
rw.close();
@@ -209,8 +190,6 @@
}
private void checkImpl() {
- checkState(!notesMigration.readChanges(),
- "ConsistencyChecker for NoteDb not yet implemented");
checkOwner();
checkCurrentPatchSetEntity();
@@ -226,8 +205,8 @@
private void checkOwner() {
try {
- if (db.get().accounts().get(change.getOwner()) == null) {
- problem("Missing change owner: " + change.getOwner());
+ if (db.get().accounts().get(change().getOwner()) == null) {
+ problem("Missing change owner: " + change().getOwner());
}
} catch (OrmException e) {
error("Failed to look up owner", e);
@@ -236,10 +215,10 @@
private void checkCurrentPatchSetEntity() {
try {
- PatchSet.Id psId = change.currentPatchSetId();
- currPs = db.get().patchSets().get(psId);
+ currPs = psUtil.current(db.get(), ctl.getNotes());
if (currPs == null) {
- problem(String.format("Current patch set %d not found", psId.get()));
+ problem(String.format("Current patch set %d not found",
+ change().currentPatchSetId().get()));
}
} catch (OrmException e) {
error("Failed to look up current patch set", e);
@@ -247,7 +226,7 @@
}
private boolean openRepo() {
- Project.NameKey project = change.getDest().getParentKey();
+ Project.NameKey project = change().getDest().getParentKey();
try {
repo = repoManager.openRepository(project);
rw = new RevWalk(repo);
@@ -262,13 +241,11 @@
private boolean checkPatchSets() {
List<PatchSet> all;
try {
- all = Lists.newArrayList(db.get().patchSets().byChange(change.getId()));
+ // Iterate in descending order.
+ all = PS_ID_ORDER.sortedCopy(psUtil.byChange(db.get(), ctl.getNotes()));
} catch (OrmException e) {
return error("Failed to look up patch sets", e);
}
- // Iterate in descending order so deletePatchSet can assume the latest patch
- // set exists.
- Collections.sort(all, PS_ID_ORDER.reverse());
patchSetsBySha = MultimapBuilder.hashKeys(all.size())
.treeSetValues(PS_ID_ORDER)
.build();
@@ -287,6 +264,7 @@
refs = Collections.emptyMap();
}
+ List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>();
for (PatchSet ps : all) {
// Check revision format.
int psNum = ps.getId().get();
@@ -317,17 +295,21 @@
objId, String.format("patch set %d", psNum));
if (psCommit == null) {
if (fix != null && fix.deletePatchSetIfCommitMissing) {
- deletePatchSet(lastProblem(), change.getProject(), ps.getId());
+ deletePatchSetOps.add(
+ new DeletePatchSetFromDbOp(lastProblem(), ps.getId()));
}
continue;
} else if (refProblem != null && fix != null) {
fixPatchSetRef(refProblem, ps);
}
- if (ps.getId().equals(change.currentPatchSetId())) {
+ if (ps.getId().equals(change().currentPatchSetId())) {
currPsCommit = psCommit;
}
}
+ // Delete any bad patch sets found above, in a single update.
+ deletePatchSets(deletePatchSetOps);
+
// Check for duplicates.
for (Map.Entry<ObjectId, Collection<PatchSet>> e
: patchSetsBySha.asMap().entrySet()) {
@@ -342,7 +324,7 @@
}
private void checkMerged() {
- String refName = change.getDest().get();
+ String refName = change().getDest().get();
Ref dest;
try {
dest = repo.getRefDatabase().exactRef(refName);
@@ -375,22 +357,27 @@
}
}
+ private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
+ String refName = change().getDest().get();
+ return problem(String.format(
+ "Patch set %d (%s) is merged into destination ref %s (%s), but change"
+ + " status is %s", psId.get(), commit.name(),
+ refName, tip.name(), change().getStatus()));
+ }
+
private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit,
boolean merged) {
- String refName = change.getDest().get();
- if (merged && change.getStatus() != Change.Status.MERGED) {
- ProblemInfo p = problem(String.format(
- "Patch set %d (%s) is merged into destination ref %s (%s), but change"
- + " status is %s", psId.get(), commit.name(),
- refName, tip.name(), change.getStatus()));
+ String refName = change().getDest().get();
+ if (merged && change().getStatus() != Change.Status.MERGED) {
+ ProblemInfo p = wrongChangeStatus(psId, commit);
if (fix != null) {
fixMerged(p);
}
- } else if (!merged && change.getStatus() == Change.Status.MERGED) {
+ } else if (!merged && change().getStatus() == Change.Status.MERGED) {
problem(String.format("Patch set %d (%s) is not merged into"
+ " destination ref %s (%s), but change status is %s",
currPs.getId().get(), commit.name(), refName, tip.name(),
- change.getStatus()));
+ change().getStatus()));
}
}
@@ -401,16 +388,12 @@
if (commit == null) {
return;
}
- if (Objects.equals(commit, currPsCommit)) {
- // Caller gave us latest patch set SHA-1; verified in checkPatchSets.
- return;
- }
try {
if (!rw.isMergedInto(commit, tip)) {
problem(String.format("Expected merged commit %s is not merged into"
+ " destination ref %s (%s)",
- commit.name(), change.getDest().get(), tip.name()));
+ commit.name(), change().getDest().get(), tip.name()));
return;
}
@@ -425,8 +408,8 @@
}
try {
Change c = notesFactory.createChecked(
- db.get(), change.getProject(), psId.getParentKey()).getChange();
- if (!c.getDest().equals(change.getDest())) {
+ db.get(), change().getProject(), psId.getParentKey()).getChange();
+ if (!c.getDest().equals(change().getDest())) {
continue;
}
} catch (OrmException | NoSuchChangeException e) {
@@ -442,16 +425,17 @@
String changeId = Iterables.getFirst(
commit.getFooterLines(FooterConstants.CHANGE_ID), null);
// Missing Change-Id footer is ok, but mismatched is not.
- if (changeId != null && !changeId.equals(change.getKey().get())) {
+ if (changeId != null && !changeId.equals(change().getKey().get())) {
problem(String.format("Expected merged commit %s has Change-Id: %s,"
+ " but expected %s",
- commit.name(), changeId, change.getKey().get()));
+ commit.name(), changeId, change().getKey().get()));
return;
}
- PatchSet.Id psId = insertPatchSet(commit);
- if (psId != null) {
- checkMergedBitMatchesStatus(psId, commit, true);
- }
+ // TODO(dborowitz): Combine into one op.
+ insertPatchSet(commit);
+ fixMerged(problem(String.format(
+ "Expected merged commit %s has no associated patch set",
+ commit.name())));
break;
case 1:
@@ -460,10 +444,12 @@
// TODO(dborowitz): This could be fixed if it's an older patch set of
// the current change.
PatchSet.Id id = psIds.get(0);
- if (!id.equals(change.currentPatchSetId())) {
+ if (id.equals(change().currentPatchSetId())) {
+ fixMerged(wrongChangeStatus(id, commit));
+ } else {
problem(String.format("Expected merged commit %s corresponds to"
+ " patch set %s, which is not the current patch set %s",
- commit.name(), id, change.currentPatchSetId()));
+ commit.name(), id, change().currentPatchSetId()));
}
break;
@@ -490,17 +476,14 @@
}
try {
- ChangeControl ctl = changeControlFactory
- .controlFor(db.get(), change, user.get());
PatchSet.Id psId =
- ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+ ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
PatchSetInserter inserter =
patchSetInserterFactory.create(ctl, psId, commit);
- try (BatchUpdate bu = updateFactory.create(
- db.get(), change.getProject(), ctl.getUser(), TimeUtil.nowTs());
+ try (BatchUpdate bu = newBatchUpdate();
ObjectInserter oi = repo.newObjectInserter()) {
bu.setRepository(repo, rw, oi);
- bu.addOp(change.getId(), inserter
+ bu.addOp(ctl.getId(), inserter
.setValidatePolicy(CommitValidators.Policy.NONE)
.setFireRevisionCreated(false)
.setSendMail(false)
@@ -509,7 +492,8 @@
"Patch set for merged commit inserted by consistency checker"));
bu.execute();
}
- change = inserter.getChange();
+ ctl = changeControlFactory.controlFor(
+ db.get(), inserter.getChange(), ctl.getUser());
p.status = Status.FIXED;
p.outcome = "Inserted as patch set " + psId.get();
return psId;
@@ -523,30 +507,33 @@
}
private void fixMerged(ProblemInfo p) {
- try {
- change = db.get().changes().atomicUpdate(change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change c) {
- c.setStatus(Change.Status.MERGED);
- return c;
- }
- });
- ChangeUpdate changeUpdate =
- changeUpdateFactory.create(
- changeControlFactory.controlFor(db.get(), change, user.get()));
- changeUpdate.fixStatus(Change.Status.MERGED);
- changeUpdate.commit();
- indexer.index(db.get(), change);
+ try (BatchUpdate bu = newBatchUpdate();
+ ObjectInserter oi = repo.newObjectInserter()) {
+ bu.setRepository(repo, rw, oi);
+ bu.addOp(ctl.getId(), new BatchUpdate.Op() {
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException {
+ ctx.getChange().setStatus(Change.Status.MERGED);
+ ctx.getUpdate(ctx.getChange().currentPatchSetId())
+ .fixStatus(Change.Status.MERGED);
+ return true;
+ }
+ });
+ bu.execute();
p.status = Status.FIXED;
p.outcome = "Marked change as merged";
- } catch (OrmException | IOException | NoSuchChangeException e) {
- log.warn("Error marking " + change.getId() + "as merged", e);
+ } catch (UpdateException | RestApiException e) {
+ log.warn("Error marking " + ctl.getId() + "as merged", e);
p.status = Status.FIX_FAILED;
p.outcome = "Error updating status to merged";
}
}
+ private BatchUpdate newBatchUpdate() {
+ return updateFactory.create(
+ db.get(), change().getProject(), ctl.getUser(), TimeUtil.nowTs());
+ }
+
private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
try {
RefUpdate ru = repo.updateRef(ps.getId().toRefName());
@@ -582,59 +569,108 @@
}
}
- private void deletePatchSet(ProblemInfo p, Project.NameKey project,
- PatchSet.Id psId) {
- ReviewDb db = this.db.get();
- Change.Id cid = psId.getParentKey();
- try {
- db.changes().beginTransaction(cid);
- try {
- ChangeNotes notes = notesFactory.createChecked(db, project, cid);
- Change c = notes.getChange();
- if (psId.equals(c.currentPatchSetId())) {
- List<PatchSet> all = Lists.newArrayList(db.patchSets().byChange(cid));
- if (all.size() == 1 && all.get(0).getId().equals(psId)) {
- p.status = Status.FIX_FAILED;
- p.outcome = "Cannot delete patch set; no patch sets would remain";
- return;
- }
- // If there were multiple missing patch sets, assumes deletePatchSet
- // has been called in decreasing order, so the max remaining PatchSet
- // is the effective current patch set.
- Collections.sort(all, PS_ID_ORDER.reverse());
- PatchSet.Id latest = null;
- for (PatchSet ps : all) {
- latest = ps.getId();
- if (!ps.getId().equals(psId)) {
- break;
- }
- }
- c.setCurrentPatchSet(patchSetInfoFactory.get(db, notes, latest));
- db.changes().update(Collections.singleton(c));
- }
-
- // Delete dangling primary key references. Don't delete ChangeMessages,
- // which don't use patch sets as a primary key, and may provide useful
- // historical information.
- accountPatchReviewStore.get().clearReviewed(psId);
- db.patchSetApprovals().delete(
- db.patchSetApprovals().byPatchSet(psId));
- db.patchComments().delete(
- db.patchComments().byPatchSet(psId));
- db.patchSets().deleteKeys(Collections.singleton(psId));
- db.commit();
-
- p.status = Status.FIXED;
- p.outcome = "Deleted patch set";
- } finally {
- db.rollback();
+ private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) {
+ try (BatchUpdate bu = newBatchUpdate();
+ ObjectInserter oi = repo.newObjectInserter()) {
+ bu.setRepository(repo, rw, oi);
+ for (DeletePatchSetFromDbOp op : ops) {
+ checkArgument(op.psId.getParentKey().equals(ctl.getId()));
+ bu.addOp(ctl.getId(), op);
}
- } catch (PatchSetInfoNotAvailableException | OrmException
- | NoSuchChangeException e) {
+ bu.addOp(ctl.getId(), new UpdateCurrentPatchSetOp(ops));
+ bu.execute();
+ } catch (NoPatchSetsWouldRemainException e) {
+ for (DeletePatchSetFromDbOp op : ops) {
+ op.p.status = Status.FIX_FAILED;
+ op.p.outcome = e.getMessage();
+ }
+ } catch (UpdateException | RestApiException e) {
String msg = "Error deleting patch set";
- log.warn(msg + ' ' + psId, e);
- p.status = Status.FIX_FAILED;
- p.outcome = msg;
+ log.warn(msg + " of change " + ops.get(0).psId.getParentKey(), e);
+ for (DeletePatchSetFromDbOp op : ops) {
+ // Overwrite existing statuses that were set before the transaction was
+ // rolled back.
+ op.p.status = Status.FIX_FAILED;
+ op.p.outcome = msg;
+ }
+ }
+ }
+
+ private class DeletePatchSetFromDbOp extends BatchUpdate.Op {
+ private final ProblemInfo p;
+ private final PatchSet.Id psId;
+
+ private DeletePatchSetFromDbOp(ProblemInfo p, PatchSet.Id psId) {
+ this.p = p;
+ this.psId = psId;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws OrmException, PatchSetInfoNotAvailableException {
+ // Delete dangling key references.
+ ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
+ accountPatchReviewStore.get().clearReviewed(psId);
+ db.changeMessages().delete(
+ db.changeMessages().byChange(psId.getParentKey()));
+ db.patchSetApprovals().delete(
+ db.patchSetApprovals().byPatchSet(psId));
+ db.patchComments().delete(
+ db.patchComments().byPatchSet(psId));
+ db.patchSets().deleteKeys(Collections.singleton(psId));
+
+ // NoteDb requires no additional fiddling; setting the state to deleted is
+ // sufficient to filter everything else out.
+ ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
+
+ p.status = Status.FIXED;
+ p.outcome = "Deleted patch set";
+ return true;
+ }
+ }
+
+ private static class NoPatchSetsWouldRemainException
+ extends RestApiException {
+ private static final long serialVersionUID = 1L;
+
+ private NoPatchSetsWouldRemainException() {
+ super("Cannot delete patch set; no patch sets would remain");
+ }
+ }
+
+ private class UpdateCurrentPatchSetOp extends BatchUpdate.Op {
+ private final Set<PatchSet.Id> toDelete;
+
+ private UpdateCurrentPatchSetOp(List<DeletePatchSetFromDbOp> deleteOps) {
+ toDelete = new HashSet<>();
+ for (DeletePatchSetFromDbOp op : deleteOps) {
+ toDelete.add(op.psId);
+ }
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws OrmException, PatchSetInfoNotAvailableException,
+ NoPatchSetsWouldRemainException {
+ if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
+ return false;
+ }
+ Set<PatchSet.Id> all = new HashSet<>();
+ // Doesn't make any assumptions about the order in which deletes happen
+ // and whether they are seen by this op; we are already given the full set
+ // of patch sets that will eventually be deleted in this update.
+ for (PatchSet ps : psUtil.byChange(ctx.getDb(), ctx.getNotes())) {
+ if (!toDelete.contains(ps.getId())) {
+ all.add(ps.getId());
+ }
+ }
+ if (all.isEmpty()) {
+ throw new NoPatchSetsWouldRemainException();
+ }
+ PatchSet.Id latest = ReviewDbUtil.intKeyOrdering().max(all);
+ ctx.getChange().setCurrentPatchSet(
+ patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), latest));
+ return true;
}
}
@@ -687,6 +723,10 @@
}
private void warn(Throwable t) {
- log.warn("Error in consistency check of change " + change.getId(), t);
+ log.warn("Error in consistency check of change " + ctl.getId(), t);
+ }
+
+ private Result result() {
+ return Result.create(ctl, problems);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 68b3199..7cb2aac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -15,11 +15,11 @@
package com.google.gerrit.server.change;
import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.change.PutDraftComment.side;
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.DraftInput;
-import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -119,7 +119,7 @@
ChangeUtil.messageUUID(ctx.getDb())),
line, ctx.getAccountId(), Url.decode(in.inReplyTo),
ctx.getWhen());
- comment.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+ comment.setSide(side(in));
comment.setMessage(in.message.trim());
comment.setRange(in.range);
comment.setTag(in.tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 45794fb..e0591f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.server.util.GitUtil.getParent;
+
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.common.FileInfo;
@@ -21,6 +23,7 @@
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
@@ -29,17 +32,24 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;
@Singleton
public class FileInfoJson {
private final PatchListCache patchListCache;
+ private final GitRepositoryManager repoManager;
@Inject
- FileInfoJson(PatchListCache patchListCache) {
+ FileInfoJson(
+ PatchListCache patchListCache,
+ GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
this.patchListCache = patchListCache;
}
@@ -54,6 +64,22 @@
? null
: ObjectId.fromString(base.getRevision().get());
ObjectId b = ObjectId.fromString(revision.get());
+ return toFileInfoMap(change, a, b);
+ }
+
+ Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
+ throws RepositoryNotFoundException, IOException,
+ PatchListNotAvailableException {
+ ObjectId b = ObjectId.fromString(revision.get());
+ ObjectId a;
+ try (Repository git = repoManager.openRepository(change.getProject())) {
+ a = getParent(git, b, parent);
+ }
+ return toFileInfoMap(change, a, b);
+ }
+
+ private Map<String, FileInfo> toFileInfoMap(Change change,
+ ObjectId a, ObjectId b) throws PatchListNotAvailableException {
PatchList list = patchListCache.get(
new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 7f74967..35dbec1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -95,6 +95,9 @@
@Option(name = "--base", metaVar = "revision-id")
String base;
+ @Option(name = "--parent", metaVar = "parent-number")
+ int parentNum;
+
@Option(name = "--reviewed")
boolean reviewed;
@@ -145,24 +148,33 @@
return Response.ok(query(resource));
}
- PatchSet basePatchSet = null;
- if (base != null) {
- RevisionResource baseResource = revisions.parse(
- resource.getChangeResource(), IdString.fromDecoded(base));
- basePatchSet = baseResource.getPatchSet();
- }
+ Response<Map<String, FileInfo>> r;
try {
- Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap(
- resource.getChange(),
- resource.getPatchSet().getRevision(),
- basePatchSet));
- if (resource.isCacheable()) {
- r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+ if (base != null) {
+ RevisionResource baseResource = revisions.parse(
+ resource.getChangeResource(), IdString.fromDecoded(base));
+ r = Response.ok(fileInfoJson.toFileInfoMap(
+ resource.getChange(),
+ resource.getPatchSet().getRevision(),
+ baseResource.getPatchSet()));
+ } else if (parentNum > 0) {
+ r = Response.ok(fileInfoJson.toFileInfoMap(
+ resource.getChange(),
+ resource.getPatchSet().getRevision(),
+ parentNum - 1));
+ } else {
+ r = Response.ok(fileInfoJson.toFileInfoMap(
+ resource.getChange(),
+ resource.getPatchSet()));
}
- return r;
} catch (PatchListNotAvailableException e) {
throw new ResourceNotFoundException(e.getMessage());
}
+
+ if (resource.isCacheable()) {
+ r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+ }
+ return r;
}
private void checkOptions() throws BadRequestException {
@@ -170,6 +182,9 @@
if (base != null) {
supplied++;
}
+ if (parentNum > 0) {
+ supplied++;
+ }
if (reviewed) {
supplied++;
}
@@ -177,7 +192,8 @@
supplied++;
}
if (supplied > 1) {
- throw new BadRequestException("cannot combine base, reviewed, query");
+ throw new BadRequestException(
+ "cannot combine base, parent, reviewed, query");
}
}
@@ -306,5 +322,10 @@
this.base = base;
return this;
}
+
+ public ListFiles setParent(int parentNum) {
+ this.parentNum = parentNum;
+ return this;
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 3d02b83..1728c35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -90,6 +90,9 @@
@Option(name = "--base", metaVar = "REVISION")
String base;
+ @Option(name = "--parent", metaVar = "parent-number")
+ int parentNum;
+
@Deprecated
@Option(name = "--ignore-whitespace")
IgnoreWhitespace ignoreWhitespace;
@@ -121,12 +124,6 @@
public Response<DiffInfo> apply(FileResource resource)
throws ResourceConflictException, ResourceNotFoundException,
OrmException, AuthException, InvalidChangeOperationException, IOException {
- PatchSet basePatchSet = null;
- if (base != null) {
- RevisionResource baseResource = revisions.parse(
- resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
- basePatchSet = baseResource.getPatchSet();
- }
DiffPreferencesInfo prefs = new DiffPreferencesInfo();
if (whitespace != null) {
prefs.ignoreWhitespace = whitespace;
@@ -138,13 +135,35 @@
prefs.context = context;
prefs.intralineDifference = intraline;
- try {
- PatchScriptFactory psf = patchScriptFactoryFactory.create(
+ PatchScriptFactory psf;
+ PatchSet basePatchSet = null;
+ if (base != null) {
+ RevisionResource baseResource = revisions.parse(
+ resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
+ basePatchSet = baseResource.getPatchSet();
+ psf = patchScriptFactoryFactory.create(
resource.getRevision().getControl(),
resource.getPatchKey().getFileName(),
- basePatchSet != null ? basePatchSet.getId() : null,
+ basePatchSet.getId(),
resource.getPatchKey().getParentKey(),
prefs);
+ } else if (parentNum > 0) {
+ psf = patchScriptFactoryFactory.create(
+ resource.getRevision().getControl(),
+ resource.getPatchKey().getFileName(),
+ parentNum - 1,
+ resource.getPatchKey().getParentKey(),
+ prefs);
+ } else {
+ psf = patchScriptFactoryFactory.create(
+ resource.getRevision().getControl(),
+ resource.getPatchKey().getFileName(),
+ null,
+ resource.getPatchKey().getParentKey(),
+ prefs);
+ }
+
+ try {
psf.setLoadHistory(false);
psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
PatchScript ps = psf.call();
@@ -272,6 +291,11 @@
return this;
}
+ public GetDiff setParent(int parentNum) {
+ this.parentNum = parentNum;
+ return this;
+ }
+
public GetDiff setContext(int context) {
this.context = context;
return this;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 2fbf552..4e8527a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -17,6 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.change.PutDraftComment.side;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -24,6 +25,8 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.hash.HashCode;
@@ -34,10 +37,13 @@
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -79,6 +85,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
@@ -94,10 +101,6 @@
public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
private static final Logger log = LoggerFactory.getLogger(PostReview.class);
- static class Output {
- Map<String, Short> labels;
- }
-
private final Provider<ReviewDb> db;
private final BatchUpdate.Factory batchUpdateFactory;
private final ChangesCollection changes;
@@ -110,6 +113,7 @@
private final AccountsCollection accounts;
private final EmailReviewComments.Factory email;
private final CommentAdded commentAdded;
+ private final PostReviewers postReviewers;
@Inject
PostReview(Provider<ReviewDb> db,
@@ -123,7 +127,8 @@
PatchListCache patchListCache,
AccountsCollection accounts,
EmailReviewComments.Factory email,
- CommentAdded commentAdded) {
+ CommentAdded commentAdded,
+ PostReviewers postReviewers) {
this.db = db;
this.batchUpdateFactory = batchUpdateFactory;
this.changes = changes;
@@ -136,16 +141,18 @@
this.accounts = accounts;
this.email = email;
this.commentAdded = commentAdded;
+ this.postReviewers = postReviewers;
}
@Override
- public Output apply(RevisionResource revision, ReviewInput input)
- throws RestApiException, UpdateException, OrmException {
+ public ReviewResult apply(RevisionResource revision, ReviewInput input)
+ throws RestApiException, UpdateException, OrmException, IOException {
return apply(revision, input, TimeUtil.nowTs());
}
- public Output apply(RevisionResource revision, ReviewInput input,
- Timestamp ts) throws RestApiException, UpdateException, OrmException {
+ public ReviewResult apply(RevisionResource revision, ReviewInput input,
+ Timestamp ts)
+ throws RestApiException, UpdateException, OrmException, IOException {
// Respect timestamp, but truncate at change created-on time.
ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
if (revision.getEdit().isPresent()) {
@@ -165,15 +172,52 @@
input.notify = NotifyHandling.NONE;
}
+ Map<String, AddReviewerResult> reviewerJsonResults = null;
+ List<PostReviewers.Addition> reviewerResults = Lists.newArrayList();
+ boolean hasError = false;
+ boolean confirm = false;
+ if (input.reviewers != null) {
+ reviewerJsonResults = Maps.newHashMap();
+ for (AddReviewerInput reviewerInput : input.reviewers) {
+ PostReviewers.Addition result = postReviewers.prepareApplication(
+ revision.getChangeResource(), reviewerInput);
+ reviewerJsonResults.put(reviewerInput.reviewer, result.result);
+ if (result.result.error != null) {
+ hasError = true;
+ continue;
+ }
+ if (result.result.confirm != null) {
+ confirm = true;
+ continue;
+ }
+ reviewerResults.add(result);
+ }
+ }
+
+ ReviewResult output = new ReviewResult();
+ output.reviewers = reviewerJsonResults;
+ if (hasError || confirm) {
+ return output;
+ }
+ output.labels = input.labels;
+
try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
revision.getChange().getProject(), revision.getUser(), ts)) {
+ // Apply reviewer changes first. Revision emails should be sent to the
+ // updated set of reviewers.
+ for (PostReviewers.Addition reviewerResult : reviewerResults) {
+ bu.addOp(revision.getChange().getId(), reviewerResult.op);
+ }
bu.addOp(
revision.getChange().getId(),
new Op(revision.getPatchSet().getId(), input));
bu.execute();
+
+ for (PostReviewers.Addition reviewerResult : reviewerResults) {
+ reviewerResult.gatherResults();
+ }
}
- Output output = new Output();
- output.labels = input.labels;
+
return output;
}
@@ -426,7 +470,7 @@
}
e.setStatus(PatchLineComment.Status.PUBLISHED);
e.setWrittenOn(ctx.getWhen());
- e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+ e.setSide(side(c));
setCommentRevId(e, patchListCache, ctx.getChange(), ps);
e.setMessage(c.message);
e.setTag(in.tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 8205beb..399c3ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -14,6 +14,9 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
@@ -23,6 +26,7 @@
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -49,6 +53,7 @@
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gwtorm.server.OrmException;
@@ -62,15 +67,18 @@
import java.io.IOException;
import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Singleton
-public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> {
- private static final Logger log = LoggerFactory
- .getLogger(PostReviewers.class);
+public class PostReviewers
+ implements RestModifyView<ChangeResource, AddReviewerInput> {
+ private static final Logger log =
+ LoggerFactory.getLogger(PostReviewers.class);
public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
public static final int DEFAULT_MAX_REVIEWERS = 20;
@@ -91,6 +99,7 @@
private final AccountCache accountCache;
private final ReviewerJson json;
private final ReviewerAdded reviewerAdded;
+ private final NotesMigration migration;
@Inject
PostReviewers(AccountsCollection accounts,
@@ -108,7 +117,8 @@
@GerritServerConfig Config cfg,
AccountCache accountCache,
ReviewerJson json,
- ReviewerAdded reviewerAdded) {
+ ReviewerAdded reviewerAdded,
+ NotesMigration migration) {
this.accounts = accounts;
this.reviewerFactory = reviewerFactory;
this.approvalsUtil = approvalsUtil;
@@ -125,49 +135,64 @@
this.accountCache = accountCache;
this.json = json;
this.reviewerAdded = reviewerAdded;
+ this.migration = migration;
}
@Override
public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input)
- throws UpdateException, OrmException, RestApiException, IOException {
+ throws IOException, OrmException, RestApiException, UpdateException {
if (input.reviewer == null) {
throw new BadRequestException("missing reviewer field");
}
+ Addition addition = prepareApplication(rsrc, input);
+ if (addition.op == null) {
+ return addition.result;
+ }
+ try (BatchUpdate bu = batchUpdateFactory.create(dbProvider.get(),
+ rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ Change.Id id = rsrc.getChange().getId();
+ bu.addOp(id, addition.op);
+ bu.execute();
+ addition.gatherResults();
+ }
+ return addition.result;
+ }
+
+ public Addition prepareApplication(ChangeResource rsrc, AddReviewerInput input)
+ throws OrmException, RestApiException, IOException {
try {
Account.Id accountId = accounts.parse(input.reviewer).getAccountId();
- return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId));
+ return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId),
+ input.state());
} catch (UnprocessableEntityException e) {
try {
return putGroup(rsrc, input);
} catch (UnprocessableEntityException e2) {
- throw new UnprocessableEntityException(MessageFormat.format(
- ChangeMessages.get().reviewerNotFound,
- input.reviewer));
+ throw new UnprocessableEntityException(MessageFormat
+ .format(ChangeMessages.get().reviewerNotFound, input.reviewer));
}
}
}
- private AddReviewerResult putAccount(String reviewer, ReviewerResource rsrc)
- throws OrmException, UpdateException, RestApiException {
+ private Addition putAccount(String reviewer, ReviewerResource rsrc,
+ ReviewerState state) {
Account member = rsrc.getReviewerUser().getAccount();
ChangeControl control = rsrc.getReviewerControl();
- AddReviewerResult result = new AddReviewerResult(reviewer);
if (isValidReviewer(member, control)) {
- addReviewers(rsrc.getChangeResource(), result,
- ImmutableMap.of(member.getId(), control));
+ return new Addition(reviewer, rsrc.getChangeResource(),
+ ImmutableMap.of(member.getId(), control), state);
}
- return result;
+ return new Addition(reviewer);
}
- private AddReviewerResult putGroup(ChangeResource rsrc, AddReviewerInput input)
- throws UpdateException, RestApiException, OrmException, IOException {
- GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
- AddReviewerResult result = new AddReviewerResult(input.reviewer);
+ private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
+ throws RestApiException, OrmException, IOException {
+ GroupDescription.Basic group =
+ groupsCollection.parseInternal(input.reviewer);
if (!isLegalReviewerGroup(group.getGroupUUID())) {
- result.error = MessageFormat.format(
- ChangeMessages.get().groupIsNotAllowed, group.getName());
- return result;
+ return fail(input.reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed,
+ group.getName()));
}
Map<Account.Id, ChangeControl> reviewers = new HashMap<>();
@@ -187,22 +212,19 @@
int maxAllowed =
cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
if (maxAllowed > 0 && members.size() > maxAllowed) {
- result.error = MessageFormat.format(
- ChangeMessages.get().groupHasTooManyMembers, group.getName());
- return result;
+ return fail(input.reviewer, MessageFormat.format(
+ ChangeMessages.get().groupHasTooManyMembers, group.getName()));
}
// if maxWithoutCheck is set to 0, we never ask for confirmation
- int maxWithoutConfirmation =
- cfg.getInt("addreviewer", "maxWithoutConfirmation",
- DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+ int maxWithoutConfirmation = cfg.getInt("addreviewer",
+ "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
if (!input.confirmed() && maxWithoutConfirmation > 0
&& members.size() > maxWithoutConfirmation) {
- result.confirm = true;
- result.error = MessageFormat.format(
- ChangeMessages.get().groupManyMembersConfirmation,
- group.getName(), members.size());
- return result;
+ return fail(input.reviewer, true,
+ MessageFormat.format(
+ ChangeMessages.get().groupManyMembersConfirmation,
+ group.getName(), members.size()));
}
for (Account member : members) {
@@ -211,8 +233,7 @@
}
}
- addReviewers(rsrc, result, reviewers);
- return result;
+ return new Addition(input.reviewer, rsrc, reviewers, input.state());
}
private boolean isValidReviewer(Account member, ChangeControl control) {
@@ -225,80 +246,127 @@
return false;
}
+ private Addition fail(String reviewer, String error) {
+ return fail(reviewer, false, error);
+ }
- private void addReviewers(
- ChangeResource rsrc, AddReviewerResult result, Map<Account.Id, ChangeControl> reviewers)
- throws OrmException, RestApiException, UpdateException {
- try (BatchUpdate bu = batchUpdateFactory.create(
- dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
- Op op = new Op(rsrc, reviewers);
- Change.Id id = rsrc.getChange().getId();
- bu.addOp(id, op);
- bu.execute();
+ private Addition fail(String reviewer, boolean confirm, String error) {
+ Addition addition = new Addition(reviewer);
+ addition.result.confirm = confirm ? true : null;
+ addition.result.error = error;
+ return addition;
+ }
- result.reviewers = Lists.newArrayListWithCapacity(op.added.size());
- for (PatchSetApproval psa : op.added) {
- // New reviewers have value 0, don't bother normalizing.
- result.reviewers.add(
- json.format(new ReviewerInfo(psa.getAccountId().get()),
- reviewers.get(psa.getAccountId()),
- ImmutableList.of(psa)));
+ class Addition {
+ final AddReviewerResult result;
+ final Op op;
+
+ private final Map<Account.Id, ChangeControl> reviewers;
+
+ protected Addition(String reviewer) {
+ this(reviewer, null, null, REVIEWER);
+ }
+
+ protected Addition(String reviewer, ChangeResource rsrc,
+ Map<Account.Id, ChangeControl> reviewers, ReviewerState state) {
+ result = new AddReviewerResult(reviewer);
+ if (reviewers == null) {
+ this.reviewers = ImmutableMap.of();
+ op = null;
+ return;
}
+ this.reviewers = reviewers;
+ op = new Op(rsrc, reviewers, state);
+ }
- // We don't do this inside Op, since the accounts are in a different
- // table.
- accountLoaderFactory.create(true).fill(result.reviewers);
+ void gatherResults() throws OrmException {
+ // Generate result details and fill AccountLoader. This occurs outside
+ // the Op because the accounts are in a different table.
+ if (migration.readChanges() && op.state == CC) {
+ result.ccs = Lists.newArrayListWithCapacity(op.addedCCs.size());
+ for (Account.Id accountId : op.addedCCs) {
+ result.ccs.add(
+ json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
+ }
+ accountLoaderFactory.create(true).fill(result.ccs);
+ } else {
+ result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size());
+ for (PatchSetApproval psa : op.addedReviewers) {
+ // New reviewers have value 0, don't bother normalizing.
+ result.reviewers.add(
+ json.format(new ReviewerInfo(psa.getAccountId().get()),
+ reviewers.get(psa.getAccountId()),
+ ImmutableList.of(psa)));
+ }
+ accountLoaderFactory.create(true).fill(result.reviewers);
+ }
}
}
- private class Op extends BatchUpdate.Op {
- private final ChangeResource rsrc;
- private final Map<Account.Id, ChangeControl> reviewers;
+ class Op extends BatchUpdate.Op {
+ final Map<Account.Id, ChangeControl> reviewers;
+ final ReviewerState state;
+ List<PatchSetApproval> addedReviewers;
+ Collection<Account.Id> addedCCs;
- private List<PatchSetApproval> added;
+ private final ChangeResource rsrc;
private PatchSet patchSet;
- Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers) {
+ Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers,
+ ReviewerState state) {
this.rsrc = rsrc;
this.reviewers = reviewers;
+ this.state = state;
}
@Override
public boolean updateChange(ChangeContext ctx)
throws RestApiException, OrmException, IOException {
- added =
- approvalsUtil.addReviewers(
- ctx.getDb(),
- ctx.getNotes(),
- ctx.getUpdate(ctx.getChange().currentPatchSetId()),
- rsrc.getControl().getLabelTypes(),
- rsrc.getChange(),
- reviewers.keySet());
-
- if (added.isEmpty()) {
- return false;
+ if (migration.readChanges() && state == CC) {
+ addedCCs = approvalsUtil.addCcs(ctx.getNotes(),
+ ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+ reviewers.keySet());
+ if (addedCCs.isEmpty()) {
+ return false;
+ }
+ } else {
+ addedReviewers = approvalsUtil.addReviewers(ctx.getDb(), ctx.getNotes(),
+ ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+ rsrc.getControl().getLabelTypes(), rsrc.getChange(),
+ reviewers.keySet());
+ if (addedReviewers.isEmpty()) {
+ return false;
+ }
}
+
patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
return true;
}
@Override
public void postUpdate(Context ctx) throws Exception {
- emailReviewers(rsrc.getChange(), added);
-
- if (!added.isEmpty()) {
- for (PatchSetApproval psa : added) {
- Account account = accountCache.get(psa.getAccountId()).getAccount();
- reviewerAdded.fire(rsrc.getChange(), patchSet, account,
- ctx.getAccount(),
- ctx.getWhen());
+ if (addedReviewers != null || addedCCs != null) {
+ if (addedReviewers == null) {
+ addedReviewers = new ArrayList<>();
+ }
+ if (addedCCs == null) {
+ addedCCs = new ArrayList<>();
+ }
+ emailReviewers(rsrc.getChange(), addedReviewers, addedCCs);
+ if (!addedReviewers.isEmpty()) {
+ for (PatchSetApproval psa : addedReviewers) {
+ Account account = accountCache.get(psa.getAccountId()).getAccount();
+ reviewerAdded.fire(rsrc.getChange(), patchSet, account,
+ ctx.getAccount(), ctx.getWhen());
+ }
}
}
}
}
- private void emailReviewers(Change change, List<PatchSetApproval> added) {
- if (added.isEmpty()) {
+ private void emailReviewers(Change change, List<PatchSetApproval> added,
+ Collection<Account.Id> copied) {
+ if (added.isEmpty() && copied.isEmpty()) {
return;
}
@@ -312,18 +380,27 @@
toMail.add(psa.getAccountId());
}
}
- if (!toMail.isEmpty()) {
- try {
- AddReviewerSender cm = addReviewerSenderFactory
- .create(change.getProject(), change.getId());
- cm.setFrom(userId);
- cm.addReviewers(toMail);
- cm.send();
- } catch (Exception err) {
- log.error("Cannot send email to new reviewers of change "
- + change.getId(), err);
+ List<Account.Id> toCopy = Lists.newArrayListWithCapacity(copied.size());
+ for (Account.Id id : copied) {
+ if (!id.equals(userId)) {
+ toCopy.add(id);
}
}
+ if (toMail.isEmpty() && toCopy.isEmpty()) {
+ return;
+ }
+
+ try {
+ AddReviewerSender cm = addReviewerSenderFactory
+ .create(change.getProject(), change.getId());
+ cm.setFrom(userId);
+ cm.addReviewers(toMail);
+ cm.addExtraCC(toCopy);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email to new reviewers of change "
+ + change.getId(), err);
+ }
}
public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 043beaa..655e07d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -19,6 +19,7 @@
import com.google.common.base.Optional;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -163,7 +164,7 @@
private static PatchLineComment update(PatchLineComment e, DraftInput in,
Timestamp when) {
if (in.side != null) {
- e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+ e.setSide(side(in));
}
if (in.inReplyTo != null) {
e.setParentUuid(Url.decode(in.inReplyTo));
@@ -180,4 +181,11 @@
}
return e;
}
+
+ static short side(Comment c) {
+ if (c.side == Side.PARENT) {
+ return (short) (c.parent == null ? 0 : -c.parent.shortValue());
+ }
+ return 1;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 5b1fd25..bd5dbda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -17,6 +17,9 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
@@ -41,6 +44,8 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -59,6 +64,7 @@
import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -276,7 +282,7 @@
}
}
- public static class Op {
+ public static class RepoOnlyOp {
/**
* Override this method to update the repo.
*
@@ -286,6 +292,18 @@
}
/**
+ * Override this method to do something after the update
+ * e.g. send email or run hooks
+ *
+ * @param ctx context
+ */
+ //TODO(dborowitz): Support async operations?
+ public void postUpdate(Context ctx) throws Exception {
+ }
+ }
+
+ public static class Op extends RepoOnlyOp {
+ /**
* Override this method to modify a change.
*
* @param ctx context
@@ -295,15 +313,6 @@
public boolean updateChange(ChangeContext ctx) throws Exception {
return false;
}
-
- /**
- * Override this method to perform operations after the update.
- *
- * @param ctx context
- */
- // TODO(dborowitz): Support async operations?
- public void postUpdate(Context ctx) throws Exception {
- }
}
public abstract static class InsertChangeOp extends Op {
@@ -317,7 +326,7 @@
* methods are called after that phase has been completed for <em>all</em> updates.
*/
public static class Listener {
- private static final Listener NONE = new Listener();
+ public static final Listener NONE = new Listener();
/**
* Called after updating all repositories and flushing objects but before
@@ -463,6 +472,7 @@
private final ReviewDb db;
private final SchemaFactory<ReviewDb> schemaFactory;
+ private final long logThresholdNanos;
private final Project.NameKey project;
private final CurrentUser user;
private final Timestamp when;
@@ -473,6 +483,7 @@
private final Map<Change.Id, Change> newChanges = new HashMap<>();
private final List<CheckedFuture<?, IOException>> indexFutures =
new ArrayList<>();
+ private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
private Repository repo;
private ObjectInserter inserter;
@@ -485,6 +496,7 @@
@AssistedInject
BatchUpdate(
+ @GerritServerConfig Config cfg,
AllUsersName allUsers,
ChangeControl.GenericFactory changeControlFactory,
ChangeIndexer indexer,
@@ -513,6 +525,10 @@
this.schemaFactory = schemaFactory;
this.updateManagerFactory = updateManagerFactory;
+ this.logThresholdNanos = MILLISECONDS.toNanos(
+ ConfigUtil.getTimeUnit(
+ cfg, "change", null, "updateDebugLogThreshold",
+ SECONDS.toMillis(2), MILLISECONDS));
this.db = db;
this.project = project;
this.user = user;
@@ -585,10 +601,17 @@
public BatchUpdate addOp(Change.Id id, Op op) {
checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+ checkNotNull(op);
ops.put(id, op);
return this;
}
+ public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
+ checkArgument(!(op instanceof Op), "use addOp()");
+ repoOnlyOps.add(op);
+ return this;
+ }
+
public BatchUpdate insertChange(InsertChangeOp op) {
Context ctx = new Context();
Change c = op.createChange(ctx);
@@ -614,6 +637,11 @@
for (Op op : ops.values()) {
op.updateRepo(ctx);
}
+
+ for (RepoOnlyOp op : repoOnlyOps) {
+ op.updateRepo(ctx);
+ }
+
if (inserter != null) {
inserter.flush();
}
@@ -669,10 +697,14 @@
tasks.add(task);
futures.add(executor.submit(task));
}
+ long startNanos = System.nanoTime();
Futures.allAsList(futures).get();
+ maybeLogSlowUpdate(startNanos, "change");
if (notesMigration.commitChangeWrites()) {
+ startNanos = System.nanoTime();
executeNoteDbUpdates(tasks);
+ maybeLogSlowUpdate(startNanos, "NoteDb");
}
} catch (ExecutionException | InterruptedException e) {
Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
@@ -692,6 +724,25 @@
}
}
+ private static class SlowUpdateException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private SlowUpdateException(String fmt, Object... args) {
+ super(String.format(fmt, args));
+ }
+ }
+
+ private void maybeLogSlowUpdate(long startNanos, String desc) {
+ long elapsedNanos = System.nanoTime() - startNanos;
+ if (!log.isDebugEnabled() || elapsedNanos <= logThresholdNanos) {
+ return;
+ }
+ log.debug("Slow " + desc + " update",
+ new SlowUpdateException(
+ "Slow %s update (%d ms) to %s for %s",
+ desc, NANOSECONDS.toMillis(elapsedNanos), project, ops.keySet()));
+ }
+
private void executeNoteDbUpdates(List<ChangeTask> tasks) {
// Aggregate together all NoteDb ref updates from the ops we executed,
// possibly in parallel. Each task had its own NoteDbUpdateManager instance
@@ -928,5 +979,9 @@
for (Op op : ops.values()) {
op.postUpdate(ctx);
}
+
+ for (RepoOnlyOp op : repoOnlyOps) {
+ op.postUpdate(ctx);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index da9cf1d..093d036b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -403,7 +403,8 @@
private boolean isRepo(Path p) {
String name = p.getFileName().toString();
return !name.equals(Constants.DOT_GIT)
- && name.endsWith(Constants.DOT_GIT_EXT);
+ && (name.endsWith(Constants.DOT_GIT_EXT)
+ || FileKey.isGitRepository(p.toFile(), FS.DETECTED));
}
private void addProject(Path p) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 0974aed..c69abf5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -731,9 +731,9 @@
private void updateSuperProjects(Collection<Branch.NameKey> branches) {
logDebug("Updating superprojects");
- SubmoduleOp subOp = subOpFactory.create(orm);
+ SubmoduleOp subOp = subOpFactory.create(branches, orm);
try {
- subOp.updateSuperProjects(branches);
+ subOp.updateSuperProjects();
logDebug("Updating superprojects done");
} catch (SubmoduleException e) {
logError("The gitlinks were not updated according to the "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index 06ef492..2bfe5b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -38,7 +38,10 @@
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -99,6 +102,14 @@
return update;
}
+ /**
+ * Make sure the update has already executed before reset it.
+ * TODO:czhen Have a flag in BatchUpdate to mark if it has been executed
+ */
+ void resetUpdate() {
+ update = null;
+ }
+
void close() {
if (update != null) {
update.close();
@@ -197,6 +208,14 @@
}
}
+ public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
+ List<BatchUpdate> updates = new ArrayList<>(projects.size());
+ for (Project.NameKey project : projects) {
+ updates.add(getRepo(project).getUpdate());
+ }
+ return updates;
+ }
+
@Override
public void close() {
for (OpenRepo repo : openRepos.values()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index fa9fe2d..ff8a12a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -646,8 +646,8 @@
// Update superproject gitlinks if required.
try (MergeOpRepoManager orm = ormProvider.get()) {
orm.setContext(db, TimeUtil.nowTs(), user, "receiveID");
- SubmoduleOp op = subOpFactory.create(orm);
- op.updateSuperProjects(branches);
+ SubmoduleOp op = subOpFactory.create(branches, orm);
+ op.updateSuperProjects();
} catch (SubmoduleException e) {
log.error("Can't update the superprojects", e);
}
@@ -1799,6 +1799,7 @@
ApprovalsUtil.renderMessageWithApprovals(
psId.get(), approvals,
Collections.<String, PatchSetApproval> emptyMap()));
+ msg.append('.');
if (!Strings.isNullOrEmpty(magicBranch.message)) {
msg.append("\n").append(magicBranch.message);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index b012d4d..2e162b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -263,10 +263,12 @@
String approvalMessage = ApprovalsUtil.renderMessageWithApprovals(
patchSetId.get(), approvals, scanLabels(ctx, approvals));
- StringBuilder message = new StringBuilder(approvalMessage);
String kindMessage = changeKindMessage(changeKind);
+ StringBuilder message = new StringBuilder(approvalMessage);
if (!Strings.isNullOrEmpty(kindMessage)) {
message.append(kindMessage);
+ } else {
+ message.append('.');
}
if (!Strings.isNullOrEmpty(reviewMessage)) {
message.append("\n").append(reviewMessage);
@@ -293,9 +295,9 @@
case MERGE_FIRST_PARENT_UPDATE:
case TRIVIAL_REBASE:
case NO_CHANGE:
- return ": Patch Set " + priorPatchSetId.get() + " was rebased";
+ return ": Patch Set " + priorPatchSetId.get() + " was rebased.";
case NO_CODE_CHANGE:
- return ": Commit message was updated";
+ return ": Commit message was updated.";
case REWORK:
default:
return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index edaed13..1a8c4a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -16,16 +16,18 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
-import com.google.gerrit.common.Nullable;
+import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.data.SubscribeSection;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate.Listener;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
@@ -39,20 +41,17 @@
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.RefSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,50 +59,119 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
public class SubmoduleOp {
+
+ /**
+ * Only used for branches without code review changes
+ */
+ public class GitlinkOp extends BatchUpdate.RepoOnlyOp {
+ private final Branch.NameKey branch;
+
+ GitlinkOp(Branch.NameKey branch) {
+ this.branch = branch;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) throws Exception {
+ CodeReviewCommit c = composeGitlinksCommit(branch, null);
+ ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get()));
+ addBranchTip(branch, c);
+ }
+ }
+
public interface Factory {
- SubmoduleOp create(MergeOpRepoManager orm);
+ SubmoduleOp create(
+ Collection<Branch.NameKey> updatedBranches, MergeOpRepoManager orm);
}
private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
private final GitModules.Factory gitmodulesFactory;
private final PersonIdent myIdent;
- private final GitReferenceUpdated gitRefUpdated;
private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
- private final Account account;
private final boolean verboseSuperProject;
private final boolean enableSuperProjectSubscriptions;
+ private final Multimap<Branch.NameKey, SubmoduleSubscription> targets;
+ private final Collection<Branch.NameKey> updatedBranches;
private final MergeOpRepoManager orm;
+ private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
+
@AssistedInject
public SubmoduleOp(
GitModules.Factory gitmodulesFactory,
@GerritPersonIdent PersonIdent myIdent,
@GerritServerConfig Config cfg,
- GitReferenceUpdated gitRefUpdated,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
- @Nullable Account account,
- @Assisted MergeOpRepoManager orm) {
+ @Assisted Collection<Branch.NameKey> updatedBranches,
+ @Assisted MergeOpRepoManager orm) throws SubmoduleException {
this.gitmodulesFactory = gitmodulesFactory;
this.myIdent = myIdent;
- this.gitRefUpdated = gitRefUpdated;
this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
- this.account = account;
this.verboseSuperProject = cfg.getBoolean("submodule",
"verboseSuperprojectUpdate", true);
this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
"enableSuperProjectSubscriptions", true);
this.orm = orm;
+ this.updatedBranches = updatedBranches;
+ this.targets = HashMultimap.create();
+ this.branchTips = new HashMap<>();
+ calculateSubscriptionMap();
}
- public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
+ private void calculateSubscriptionMap() throws SubmoduleException {
+ if (!enableSuperProjectSubscriptions) {
+ logDebug("Updating superprojects disabled");
+ return;
+ }
+
+ logDebug("Calculating superprojects - submodules map");
+ for (Branch.NameKey updatedBranch : updatedBranches) {
+ logDebug("Now processing " + updatedBranch);
+ Set<Branch.NameKey> checkedTargets = new HashSet<>();
+ Set<Branch.NameKey> targetsToProcess = new HashSet<>();
+ targetsToProcess.add(updatedBranch);
+
+ while (!targetsToProcess.isEmpty()) {
+ Set<Branch.NameKey> newTargets = new HashSet<>();
+ for (Branch.NameKey b : targetsToProcess) {
+ try {
+ Collection<SubmoduleSubscription> subs =
+ superProjectSubscriptionsForSubmoduleBranch(b);
+ for (SubmoduleSubscription sub : subs) {
+ Branch.NameKey dst = sub.getSuperProject();
+ targets.put(dst, sub);
+ newTargets.add(dst);
+ }
+ } catch (IOException e) {
+ throw new SubmoduleException("Cannot find superprojects for " + b, e);
+ }
+ }
+ logDebug("adding to done " + targetsToProcess);
+ checkedTargets.addAll(targetsToProcess);
+ logDebug("completely done with " + checkedTargets);
+
+ Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
+ intersection.retainAll(newTargets);
+ if (!intersection.isEmpty()) {
+ throw new SubmoduleException(
+ "Possible circular subscription involving " + updatedBranch);
+ }
+
+ targetsToProcess = newTargets;
+ }
+ }
+ }
+
+ private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
SubscribeSection s) throws IOException {
Collection<Branch.NameKey> ret = new ArrayList<>();
logDebug("Inspecting SubscribeSection " + s);
@@ -179,72 +247,34 @@
return ret;
}
- protected void updateSuperProjects(Collection<Branch.NameKey> updatedBranches)
- throws SubmoduleException {
- if (!enableSuperProjectSubscriptions) {
- logDebug("Updating superprojects disabled");
- return;
- }
- logDebug("Updating superprojects");
-
- Multimap<Branch.NameKey, SubmoduleSubscription> targets =
- HashMultimap.create();
-
- for (Branch.NameKey updatedBranch : updatedBranches) {
- logDebug("Now processing " + updatedBranch);
- Set<Branch.NameKey> checkedTargets = new HashSet<>();
- Set<Branch.NameKey> targetsToProcess = new HashSet<>();
- targetsToProcess.add(updatedBranch);
-
- while (!targetsToProcess.isEmpty()) {
- Set<Branch.NameKey> newTargets = new HashSet<>();
- for (Branch.NameKey b : targetsToProcess) {
- try {
- Collection<SubmoduleSubscription> subs =
- superProjectSubscriptionsForSubmoduleBranch(b);
- for (SubmoduleSubscription sub : subs) {
- Branch.NameKey dst = sub.getSuperProject();
- targets.put(dst, sub);
- newTargets.add(dst);
- }
- } catch (IOException e) {
- throw new SubmoduleException("Cannot find superprojects for " + b, e);
- }
+ public void updateSuperProjects() throws SubmoduleException {
+ SetMultimap<Project.NameKey, Branch.NameKey> dst = branchesByProject();
+ Set<Project.NameKey> projects = dst.keySet();
+ try {
+ for (Project.NameKey project : projects) {
+ // get a new BatchUpdate for the project
+ orm.openRepo(project, false);
+ //TODO:czhen remove this when MergeOp combine this into BatchUpdate
+ orm.getRepo(project).resetUpdate();
+ for (Branch.NameKey branch : dst.get(project)) {
+ SubmoduleOp.GitlinkOp op = new SubmoduleOp.GitlinkOp(branch);
+ orm.getRepo(project).getUpdate().addRepoOnlyOp(op);
}
- logDebug("adding to done " + targetsToProcess);
- checkedTargets.addAll(targetsToProcess);
- logDebug("completely done with " + checkedTargets);
-
- Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
- intersection.retainAll(newTargets);
- if (!intersection.isEmpty()) {
- throw new SubmoduleException(
- "Possible circular subscription involving " + updatedBranch);
- }
-
- targetsToProcess = newTargets;
}
- }
-
- for (Branch.NameKey dst : targets.keySet()) {
- try {
- updateGitlinks(dst, targets.get(dst));
- } catch (SubmoduleException e) {
- throw new SubmoduleException("Cannot update gitlinks for " + dst, e);
- }
+ BatchUpdate.execute(orm.batchUpdates(projects), Listener.NONE);
+ } catch (RestApiException | UpdateException | IOException |
+ NoSuchProjectException e) {
+ throw new SubmoduleException("Cannot update gitlinks", e);
}
}
/**
- * Update the submodules in one branch of one repository.
- *
- * @param subscriber the branch of the repository which should be changed.
- * @param updates submodule updates which should be updated to.
- * @throws SubmoduleException
+ * Create a gitlink update commit on the tip of subscriber or modify the
+ * baseCommit with gitlink update patch
*/
- private void updateGitlinks(Branch.NameKey subscriber,
- Collection<SubmoduleSubscription> updates)
- throws SubmoduleException {
+ public CodeReviewCommit composeGitlinksCommit(
+ final Branch.NameKey subscriber, RevCommit baseCommit)
+ throws IOException, SubmoduleException {
PersonIdent author = null;
StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
boolean sameAuthorForAll = true;
@@ -254,150 +284,147 @@
} catch (NoSuchProjectException | IOException e) {
throw new SubmoduleException("Cannot access superproject", e);
}
+
OpenRepo or = orm.getRepo(subscriber.getParentKey());
- try {
- Ref r = or.repo.exactRef(subscriber.get());
- if (r == null) {
- throw new SubmoduleException(
- "The branch was probably deleted from the subscriber repository");
- }
-
- DirCache dc = readTree(r, or.rw);
- DirCacheEditor ed = dc.editor();
-
- for (SubmoduleSubscription s : updates) {
- try {
- orm.openRepo(s.getSubmodule().getParentKey(), false);
- } catch (NoSuchProjectException | IOException e) {
- throw new SubmoduleException("Cannot access submodule", e);
- }
- OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
- Repository subrepo = subOr.repo;
-
- Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
- if (ref == null) {
- ed.add(new DeletePath(s.getPath()));
- continue;
- }
-
- final ObjectId updateTo = ref.getObjectId();
- RevCommit newCommit = subOr.rw.parseCommit(updateTo);
-
- subOr.rw.parseBody(newCommit);
- if (author == null) {
- author = newCommit.getAuthorIdent();
- } else if (!author.equals(newCommit.getAuthorIdent())) {
- sameAuthorForAll = false;
- }
-
- DirCacheEntry dce = dc.getEntry(s.getPath());
- ObjectId oldId;
- if (dce != null) {
- if (!dce.getFileMode().equals(FileMode.GITLINK)) {
- log.error("Requested to update gitlink " + s.getPath() + " in "
- + s.getSubmodule().getParentKey().get() + " but entry "
- + "doesn't have gitlink file mode.");
- continue;
- }
- oldId = dce.getObjectId();
- } else {
- // This submodule did not exist before. We do not want to add
- // the full submodule history to the commit message, so omit it.
- oldId = updateTo;
- }
-
- ed.add(new PathEdit(s.getPath()) {
- @Override
- public void apply(DirCacheEntry ent) {
- ent.setFileMode(FileMode.GITLINK);
- ent.setObjectId(updateTo);
- }
- });
- if (verboseSuperProject) {
- msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
- msgbuf.append(" " + s.getSubmodule().getShortName());
- msgbuf.append(" " + updateTo.getName());
- msgbuf.append("\n\n");
-
- try {
- subOr.rw.resetRetain(subOr.canMergeFlag);
- subOr.rw.markStart(newCommit);
- subOr.rw.markUninteresting(subOr.rw.parseCommit(oldId));
- for (RevCommit c : subOr.rw) {
- subOr.rw.parseBody(c);
- msgbuf.append(c.getFullMessage() + "\n\n");
- }
- } catch (IOException e) {
- throw new SubmoduleException("Could not perform a revwalk to "
- + "create superproject commit message", e);
- }
- }
- }
- ed.finish();
-
- if (!sameAuthorForAll || author == null) {
- author = myIdent;
- }
-
- ObjectInserter oi = or.repo.newObjectInserter();
- ObjectId tree = dc.writeTree(oi);
-
- ObjectId currentCommitId =
- or.repo.exactRef(subscriber.get()).getObjectId();
-
- CommitBuilder commit = new CommitBuilder();
- commit.setTreeId(tree);
- commit.setParentIds(new ObjectId[] {currentCommitId});
- commit.setAuthor(author);
- commit.setCommitter(myIdent);
- commit.setMessage(msgbuf.toString());
- oi.insert(commit);
- oi.flush();
-
- ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
-
- final RefUpdate rfu = or.repo.updateRef(subscriber.get());
- rfu.setForceUpdate(false);
- rfu.setNewObjectId(commitId);
- rfu.setExpectedOldObjectId(currentCommitId);
- rfu.setRefLogMessage("Submit to " + subscriber.getParentKey().get(), true);
-
- switch (rfu.update()) {
- case NEW:
- case FAST_FORWARD:
- gitRefUpdated.fire(subscriber.getParentKey(), rfu, account);
- // TODO since this is performed "in the background" no mail will be
- // sent to inform users about the updated branch
- break;
- case FORCED:
- case IO_FAILURE:
- case LOCK_FAILURE:
- case NOT_ATTEMPTED:
- case NO_CHANGE:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- default:
- throw new IOException(rfu.getResult().name());
- }
- } catch (IOException e) {
- throw new SubmoduleException("Cannot update gitlinks for "
- + subscriber.get(), e);
+ Ref r = or.repo.exactRef(subscriber.get());
+ if (r == null) {
+ throw new SubmoduleException(
+ "The branch was probably deleted from the subscriber repository");
}
+
+ RevCommit currentCommit = (baseCommit != null) ? baseCommit :
+ or.rw.parseCommit(or.repo.exactRef(subscriber.get()).getObjectId());
+ or.rw.parseBody(currentCommit);
+
+ DirCache dc = readTree(or.rw, currentCommit);
+ DirCacheEditor ed = dc.editor();
+
+ for (SubmoduleSubscription s : targets.get(subscriber)) {
+ try {
+ orm.openRepo(s.getSubmodule().getParentKey(), false);
+ } catch (NoSuchProjectException | IOException e) {
+ throw new SubmoduleException("Cannot access submodule", e);
+ }
+ OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
+ Repository subRepo = subOr.repo;
+
+ Ref ref = subRepo.getRefDatabase().exactRef(s.getSubmodule().get());
+ if (ref == null) {
+ ed.add(new DeletePath(s.getPath()));
+ continue;
+ }
+
+ ObjectId updateTo = ref.getObjectId();
+ if (branchTips.containsKey(s.getSubmodule())) {
+ updateTo = branchTips.get(s.getSubmodule());
+ }
+ RevWalk subOrRw = subOr.rw;
+ final RevCommit newCommit = subOrRw.parseCommit(updateTo);
+
+ subOrRw.parseBody(newCommit);
+ if (author == null) {
+ author = newCommit.getAuthorIdent();
+ } else if (!author.equals(newCommit.getAuthorIdent())) {
+ sameAuthorForAll = false;
+ }
+
+ DirCacheEntry dce = dc.getEntry(s.getPath());
+ ObjectId oldId;
+ if (dce != null) {
+ if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+ String errMsg = "Requested to update gitlink " + s.getPath() + " in "
+ + s.getSubmodule().getParentKey().get() + " but entry "
+ + "doesn't have gitlink file mode.";
+ throw new SubmoduleException(errMsg);
+ }
+ oldId = dce.getObjectId();
+ } else {
+ // This submodule did not exist before. We do not want to add
+ // the full submodule history to the commit message, so omit it.
+ oldId = updateTo;
+ }
+
+ ed.add(new PathEdit(s.getPath()) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.GITLINK);
+ ent.setObjectId(newCommit.getId());
+ }
+ });
+ if (verboseSuperProject) {
+ msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
+ msgbuf.append(" " + s.getSubmodule().getShortName());
+ msgbuf.append(" " + newCommit.getName());
+ msgbuf.append("\n\n");
+
+ try {
+ subOrRw.resetRetain(subOr.canMergeFlag);
+ subOrRw.markStart(newCommit);
+ subOrRw.markUninteresting(subOrRw.parseCommit(oldId));
+ for (RevCommit c : subOrRw) {
+ subOrRw.parseBody(c);
+ msgbuf.append(c.getFullMessage() + "\n\n");
+ }
+ } catch (IOException e) {
+ throw new SubmoduleException("Could not perform a revwalk to "
+ + "create superproject commit message", e);
+ }
+ }
+ }
+ ed.finish();
+
+
+ ObjectInserter oi = or.ins;
+ CodeReviewRevWalk rw = or.rw;
+ ObjectId tree = dc.writeTree(oi);
+
+ if (!sameAuthorForAll || author == null) {
+ author = myIdent;
+ }
+
+ CommitBuilder commit = new CommitBuilder();
+ commit.setTreeId(tree);
+ if (baseCommit != null) {
+ // modify the baseCommit
+ commit.setParentIds(baseCommit.getParents());
+ commit.setMessage(baseCommit.getFullMessage() + "\n\n" + msgbuf.toString());
+ commit.setAuthor(baseCommit.getAuthorIdent());
+ } else {
+ // create a new commit
+ commit.setParentId(currentCommit);
+ commit.setMessage(msgbuf.toString());
+ commit.setAuthor(author);
+ }
+ commit.setCommitter(myIdent);
+
+ ObjectId id = oi.insert(commit);
+ return rw.parseCommit(id);
}
- private static DirCache readTree(final Ref branch, RevWalk rw)
- throws MissingObjectException, IncorrectObjectTypeException,
- IOException {
+ private static DirCache readTree(RevWalk rw, ObjectId base)
+ throws IOException {
final DirCache dc = DirCache.newInCore();
final DirCacheBuilder b = dc.builder();
b.addTree(new byte[0], // no prefix path
DirCacheEntry.STAGE_0, // standard stage
- rw.getObjectReader(), rw.parseTree(branch.getObjectId()));
+ rw.getObjectReader(), rw.parseTree(base));
b.finish();
return dc;
}
+ public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject() {
+ SetMultimap<Project.NameKey, Branch.NameKey> ret = HashMultimap.create();
+ for (Branch.NameKey branch : targets.keySet()) {
+ ret.put(branch.getParentKey(), branch);
+ }
+
+ return ret;
+ }
+
+ public void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
+ branchTips.put(branch, tip);
+ }
+
private void logDebug(String msg, Object... args) {
if (log.isDebugEnabled()) {
log.debug("[" + orm.getSubmissionId() + "]" + msg, args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index eb4d400..6334cd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -78,7 +78,7 @@
}
}
- private RevCommit revision;
+ protected RevCommit revision;
protected ObjectReader reader;
protected ObjectInserter inserter;
protected DirCache newTree;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index f9bad6f..4c1a734 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -82,6 +82,7 @@
private static final String FILE = "File";
private static final String LENGTH = "Bytes";
private static final String PARENT = "Parent";
+ private static final String PARENT_NUMBER = "Parent-number";
private static final String PATCH_SET = "Patch-set";
private static final String REVISION = "Revision";
private static final String UUID = "UUID";
@@ -151,11 +152,13 @@
int sizeOfNote = note.length;
byte[] psb = PATCH_SET.getBytes(UTF_8);
byte[] bpsb = BASE_PATCH_SET.getBytes(UTF_8);
+ byte[] bpn = PARENT_NUMBER.getBytes(UTF_8);
RevId revId = new RevId(parseStringField(note, p, changeId, REVISION));
String fileName = null;
PatchSet.Id psId = null;
boolean isForBase = false;
+ Integer parentNumber = null;
while (p.value < sizeOfNote) {
boolean matchPs = match(note, p, psb);
@@ -168,13 +171,16 @@
fileName = null;
psId = parsePsId(note, p, changeId, BASE_PATCH_SET);
isForBase = true;
+ if (match(note, p, bpn)) {
+ parentNumber = parseParentNumber(note, p, changeId);
+ }
} else if (psId == null) {
throw parseException(changeId, "missing %s or %s header",
PATCH_SET, BASE_PATCH_SET);
}
- PatchLineComment c =
- parseComment(note, p, fileName, psId, revId, isForBase, status);
+ PatchLineComment c = parseComment(
+ note, p, fileName, psId, revId, isForBase, parentNumber, status);
fileName = c.getKey().getParentKey().getFileName();
if (!seen.add(c.getKey())) {
throw parseException(
@@ -187,7 +193,7 @@
private PatchLineComment parseComment(byte[] note, MutableInteger curr,
String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
- Status status) throws ConfigInvalidException {
+ Integer parentNumber, Status status) throws ConfigInvalidException {
Change.Id changeId = psId.getParentKey();
// Check if there is a new file.
@@ -235,7 +241,13 @@
range.getEndLine(), aId, parentUUID, commentTime);
plc.setMessage(message);
plc.setTag(tag);
- plc.setSide((short) (isForBase ? 0 : 1));
+
+ if (isForBase) {
+ plc.setSide((short) (parentNumber == null ? 0 : -parentNumber));
+ } else {
+ plc.setSide((short) 1);
+ }
+
if (range.getStartCharacter() != -1) {
plc.setRange(range);
}
@@ -333,6 +345,23 @@
return new PatchSet.Id(changeId, patchSetId);
}
+ private static Integer parseParentNumber(byte[] note, MutableInteger curr,
+ Change.Id changeId) throws ConfigInvalidException {
+ checkHeaderLineFormat(note, curr, PARENT_NUMBER, changeId);
+
+ int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+ MutableInteger i = new MutableInteger();
+ int parentNumber = RawParseUtils.parseBase10(note, start, i);
+ int endOfLine = RawParseUtils.nextLF(note, curr.value);
+ if (i.value != endOfLine - 1) {
+ throw parseException(changeId, "could not parse %s", PARENT_NUMBER);
+ }
+ checkResult(parentNumber, "parent number", changeId);
+ curr.value = endOfLine;
+ return Integer.valueOf(parentNumber);
+
+ }
+
private static String parseFilename(byte[] note, MutableInteger curr,
Change.Id changeId) throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, FILE, changeId);
@@ -461,10 +490,13 @@
PatchLineComment first = psComments.get(0);
short side = first.getSide();
- appendHeaderField(writer, side == 0
+ appendHeaderField(writer, side <= 0
? BASE_PATCH_SET
: PATCH_SET,
Integer.toString(psId.get()));
+ if (side < 0) {
+ appendHeaderField(writer, PARENT_NUMBER, Integer.toString(-side));
+ }
String currentFilename = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 4737ba3..cdaf08c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -75,6 +75,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
/** View of a single {@link Change} based on the log of its notes branch. */
public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
@@ -559,8 +560,8 @@
private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId)
throws IOException {
- try (Timer1.Context timer =
- args.metrics.autoRebuildLatency.start(CHANGES)) {
+ Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
+ try {
Change.Id cid = getChangeId();
ReviewDb db = args.db.get();
ChangeRebuilder rebuilder = args.rebuilder.get();
@@ -583,6 +584,7 @@
//
// Parse notes from the staged result so we can return something useful
// to the caller instead of throwing.
+ log.debug("Rebuilding change {} failed", getChangeId());
args.metrics.autoRebuildFailureCount.increment(CHANGES);
rebuildResult = checkNotNull(r);
checkNotNull(r.newState());
@@ -599,6 +601,10 @@
return super.openHandle(repo, oldId);
} catch (OrmException e) {
throw new IOException(e);
+ } finally {
+ log.debug("Rebuilt change {} in project {} in {} ms",
+ getChangeId(), getProjectName(),
+ TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 4605887..684e4f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -43,14 +43,20 @@
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.concurrent.TimeUnit;
/**
* View of the draft comments for a single {@link Change} based on the log of
* its drafts branch.
*/
public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
+ private static final Logger log =
+ LoggerFactory.getLogger(DraftCommentNotes.class);
+
public interface Factory {
DraftCommentNotes create(Change change, Account.Id accountId);
DraftCommentNotes createWithAutoRebuildingDisabled(
@@ -184,8 +190,8 @@
}
private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
- try (Timer1.Context timer =
- args.metrics.autoRebuildLatency.start(CHANGES)) {
+ Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
+ try {
Change.Id cid = getChangeId();
ReviewDb db = args.db.get();
ChangeRebuilder rebuilder = args.rebuilder.get();
@@ -200,6 +206,7 @@
repo.scanForRepoChanges();
} catch (OrmException | IOException e) {
// See ChangeNotes#rebuildAndOpen.
+ log.debug("Rebuilding change {} via drafts failed", getChangeId());
args.metrics.autoRebuildFailureCount.increment(CHANGES);
checkNotNull(r.staged());
return LoadHandle.create(
@@ -213,6 +220,13 @@
return super.openHandle(repo);
} catch (OrmException e) {
throw new IOException(e);
+ } finally {
+ log.debug("Rebuilt change {} in {} in {} ms via drafts",
+ getChangeId(),
+ change != null
+ ? "project " + change.getProject()
+ : "unknown project",
+ TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index 038ad51..f22d8a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -25,7 +25,7 @@
import java.io.Serializable;
public class IntraLineDiffKey implements Serializable {
- static final long serialVersionUID = 4L;
+ public static final long serialVersionUID = 4L;
private transient boolean ignoreWhitespace;
private transient ObjectId aId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 2099376..8a2403f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -28,7 +28,7 @@
PatchList get(Change change, PatchSet patchSet)
throws PatchListNotAvailableException;
- ObjectId getOldId(Change change, PatchSet patchSet)
+ ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
throws PatchListNotAvailableException;
IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 7c8f19f..abafad7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -103,6 +103,17 @@
@Override
public PatchList get(Change change, PatchSet patchSet)
throws PatchListNotAvailableException {
+ return get(change, patchSet, null);
+ }
+
+ @Override
+ public ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
+ throws PatchListNotAvailableException {
+ return get(change, patchSet, parentNum).getOldId();
+ }
+
+ private PatchList get(Change change, PatchSet patchSet, Integer parentNum)
+ throws PatchListNotAvailableException {
Project.NameKey project = change.getProject();
if (patchSet.getRevision() == null) {
throw new PatchListNotAvailableException(
@@ -110,13 +121,10 @@
}
ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
Whitespace ws = Whitespace.IGNORE_NONE;
- return get(new PatchListKey(null, b, ws), project);
- }
-
- @Override
- public ObjectId getOldId(Change change, PatchSet patchSet)
- throws PatchListNotAvailableException {
- return get(change, patchSet).getOldId();
+ if (parentNum != null) {
+ return get(PatchListKey.againstParentNum(parentNum, b, ws), project);
+ }
+ return get(PatchListKey.againstDefaultBase(b, ws), project);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index b04558d..961ed5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -32,9 +32,10 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.util.Objects;
public class PatchListKey implements Serializable {
- static final long serialVersionUID = 20L;
+ public static final long serialVersionUID = 21L;
public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
Whitespace.IGNORE_NONE, 'N',
@@ -46,7 +47,36 @@
checkState(WHITESPACE_TYPES.size() == Whitespace.values().length);
}
+ public static PatchListKey againstDefaultBase(AnyObjectId newId,
+ Whitespace ws) {
+ return new PatchListKey(null, newId, ws);
+ }
+
+ public static PatchListKey againstParentNum(int parentNum, AnyObjectId newId,
+ Whitespace ws) {
+ return new PatchListKey(parentNum, newId, ws);
+ }
+
+ /**
+ * Old patch-set ID
+ * <p>
+ * When null, it represents the Base of the newId for a non-merge commit.
+ * <p>
+ * When newId is a merge commit, null value of the oldId represents either
+ * the auto-merge commit of the newId or a parent commit of the newId.
+ * These two cases are distinguished by the parentNum.
+ */
private transient ObjectId oldId;
+
+ /**
+ * 1-based parent number when newId is a merge commit
+ * <p>
+ * For the auto-merge case this field is null.
+ * <p>
+ * Used only when oldId is null and newId is a merge commit
+ */
+ private transient Integer parentNum;
+
private transient ObjectId newId;
private transient Whitespace whitespace;
@@ -56,12 +86,24 @@
whitespace = ws;
}
+ private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws) {
+ this.parentNum = Integer.valueOf(parentNum);
+ newId = b.copy();
+ whitespace = ws;
+ }
+
/** Old side commit, or null to assume ancestor or combined merge. */
@Nullable
public ObjectId getOldId() {
return oldId;
}
+ /** Parent number (old side) of the new side (merge) commit */
+ @Nullable
+ public Integer getParentNum() {
+ return parentNum;
+ }
+
/** New side commit name. */
public ObjectId getNewId() {
return newId;
@@ -73,24 +115,16 @@
@Override
public int hashCode() {
- int h = 0;
-
- if (oldId != null) {
- h = h * 31 + oldId.hashCode();
- }
-
- h = h * 31 + newId.hashCode();
- h = h * 31 + whitespace.name().hashCode();
-
- return h;
+ return Objects.hash(oldId, parentNum, newId, whitespace);
}
@Override
public boolean equals(final Object o) {
if (o instanceof PatchListKey) {
- final PatchListKey k = (PatchListKey) o;
- return eq(oldId, k.oldId) //
- && eq(newId, k.newId) //
+ PatchListKey k = (PatchListKey) o;
+ return Objects.equals(oldId, k.oldId)
+ && Objects.equals(parentNum, k.parentNum)
+ && Objects.equals(newId, k.newId)
&& whitespace == k.whitespace;
}
return false;
@@ -109,15 +143,9 @@
return n.toString();
}
- private static boolean eq(final ObjectId a, final ObjectId b) {
- if (a == null && b == null) {
- return true;
- }
- return a != null && b != null && AnyObjectId.equals(a, b);
- }
-
private void writeObject(final ObjectOutputStream out) throws IOException {
writeCanBeNull(out, oldId);
+ out.writeInt(parentNum == null ? 0 : parentNum);
writeNotNull(out, newId);
Character c = WHITESPACE_TYPES.get(whitespace);
if (c == null) {
@@ -128,6 +156,8 @@
private void readObject(final ObjectInputStream in) throws IOException {
oldId = readCanBeNull(in);
+ int n = in.readInt();
+ parentNum = n == 0 ? null : Integer.valueOf(n);
newId = readNotNull(in);
char t = in.readChar();
whitespace = WHITESPACE_TYPES.inverse().get(t);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 90bebfc..0b2a7ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -151,7 +151,7 @@
}
final boolean againstParent =
- b.getParentCount() > 0 && b.getParent(0) == a;
+ b.getParentCount() > 0 && b.getParent(0).equals(a);
RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
RevTree aTree = rw.parseTree(a);
@@ -163,11 +163,11 @@
List<DiffEntry> diffEntries = df.scan(aTree, bTree);
Set<String> paths = null;
- if (key.getOldId() != null) {
- PatchListKey newKey =
- new PatchListKey(null, key.getNewId(), key.getWhitespace());
- PatchListKey oldKey =
- new PatchListKey(null, key.getOldId(), key.getWhitespace());
+ if (key.getOldId() != null && b.getParentCount() == 1) {
+ PatchListKey newKey = PatchListKey.againstDefaultBase(
+ key.getNewId(), key.getWhitespace());
+ PatchListKey oldKey = PatchListKey.againstDefaultBase(
+ key.getOldId(), key.getWhitespace());
paths = FluentIterable
.from(patchListCache.get(newKey, project).getPatches())
.append(patchListCache.get(oldKey, project).getPatches())
@@ -331,6 +331,11 @@
return r;
}
case 2:
+ if (key.getParentNum() != null) {
+ RevCommit r = b.getParent(key.getParentNum() - 1);
+ rw.parseBody(r);
+ return r;
+ }
return autoMerger.merge(repo, rw, ins, b, mergeStrategy);
default:
// TODO(sop) handle an octopus merge.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index b7ca69d4..a7d2523 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.patch;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.util.GitUtil.getParent;
import com.google.common.base.Optional;
import com.google.gerrit.common.Nullable;
@@ -44,9 +45,9 @@
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
@@ -70,6 +71,13 @@
@Assisted("patchSetA") PatchSet.Id patchSetA,
@Assisted("patchSetB") PatchSet.Id patchSetB,
DiffPreferencesInfo diffPrefs);
+
+ PatchScriptFactory create(
+ ChangeControl control,
+ String fileName,
+ int parentNum,
+ PatchSet.Id patchSetB,
+ DiffPreferencesInfo diffPrefs);
}
private static final Logger log =
@@ -86,6 +94,7 @@
private final String fileName;
@Nullable
private final PatchSet.Id psa;
+ private final int parentNum;
private final PatchSet.Id psb;
private final DiffPreferencesInfo diffPrefs;
private final ChangeEditUtil editReader;
@@ -103,7 +112,7 @@
private List<Patch> history;
private CommentDetail comments;
- @Inject
+ @AssistedInject
PatchScriptFactory(GitRepositoryManager grm,
PatchSetUtil psUtil,
Provider<PatchScriptBuilder> builderFactory,
@@ -129,14 +138,45 @@
this.fileName = fileName;
this.psa = patchSetA;
+ this.parentNum = -1;
this.psb = patchSetB;
this.diffPrefs = diffPrefs;
changeId = patchSetB.getParentKey();
- checkArgument(
- patchSetA == null || patchSetA.getParentKey().equals(changeId),
- "cannot compare PatchSets from different changes: %s and %s",
- patchSetA, patchSetB);
+ }
+
+ @AssistedInject
+ PatchScriptFactory(GitRepositoryManager grm,
+ PatchSetUtil psUtil,
+ Provider<PatchScriptBuilder> builderFactory,
+ PatchListCache patchListCache,
+ ReviewDb db,
+ AccountInfoCacheFactory.Factory aicFactory,
+ PatchLineCommentsUtil plcUtil,
+ ChangeEditUtil editReader,
+ @Assisted ChangeControl control,
+ @Assisted String fileName,
+ @Assisted int parentNum,
+ @Assisted PatchSet.Id patchSetB,
+ @Assisted DiffPreferencesInfo diffPrefs) {
+ this.repoManager = grm;
+ this.psUtil = psUtil;
+ this.builderFactory = builderFactory;
+ this.patchListCache = patchListCache;
+ this.db = db;
+ this.control = control;
+ this.aicFactory = aicFactory;
+ this.plcUtil = plcUtil;
+ this.editReader = editReader;
+
+ this.fileName = fileName;
+ this.psa = null;
+ this.parentNum = parentNum;
+ this.psb = patchSetB;
+ this.diffPrefs = diffPrefs;
+
+ changeId = patchSetB.getParentKey();
+ checkArgument(parentNum >= 0, "parentNum must be >= 0");
}
public void setLoadHistory(boolean load) {
@@ -151,7 +191,9 @@
public PatchScript call() throws OrmException, NoSuchChangeException,
LargeObjectException, AuthException,
InvalidChangeOperationException, IOException {
- validatePatchSetId(psa);
+ if (parentNum < 0) {
+ validatePatchSetId(psa);
+ }
validatePatchSetId(psb);
change = control.getChange();
@@ -163,15 +205,19 @@
? new PatchSet(psb)
: psUtil.get(db, control.getNotes(), psb);
- aId = psEntityA != null ? toObjectId(psEntityA) : null;
- bId = toObjectId(psEntityB);
-
if ((psEntityA != null && !control.isPatchVisible(psEntityA, db)) ||
(psEntityB != null && !control.isPatchVisible(psEntityB, db))) {
throw new NoSuchChangeException(changeId);
}
try (Repository git = repoManager.openRepository(project)) {
+ bId = toObjectId(psEntityB);
+ if (parentNum < 0) {
+ aId = psEntityA != null ? toObjectId(psEntityA) : null;
+ } else {
+ aId = getParent(git, bId, parentNum);
+ }
+
try {
final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace));
final PatchScriptBuilder b = newBuilder(list, git);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 4e8e98e..fa385f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -227,7 +227,7 @@
return Response.created(json.format(p));
}
- public Project createProject(CreateProjectArgs args)
+ private Project createProject(CreateProjectArgs args)
throws BadRequestException, ResourceConflictException, IOException,
ConfigInvalidException {
final Project.NameKey nameKey = args.getProject();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 6a21e89..b311b8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -610,19 +610,6 @@
rules = relevant.getPermission(permissionName);
- if (rules.isEmpty()) {
- effective.put(permissionName, rules);
- return rules;
- }
-
- if (rules.size() == 1) {
- if (!projectControl.match(rules.get(0), isChangeOwner)) {
- rules = Collections.emptyList();
- }
- effective.put(permissionName, rules);
- return rules;
- }
-
List<PermissionRule> mine = new ArrayList<>(rules.size());
for (PermissionRule rule : rules) {
if (projectControl.match(rule, isChangeOwner)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
new file mode 100644
index 0000000..2d1e1fa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class GitUtil {
+
+ /**
+ * @param git
+ * @param commitId
+ * @param parentNum
+ * @return the {@code paretNo} parent of given commit or {@code null}
+ * when {@code parentNo} exceed number of {@code commitId} parents.
+ * @throws IncorrectObjectTypeException
+ * the supplied id is not a commit or an annotated tag.
+ * @throws IOException
+ * a pack file or loose object could not be read.
+ */
+ public static RevCommit getParent(Repository git,
+ ObjectId commitId, int parentNum) throws IOException {
+ try (RevWalk walk = new RevWalk(git)) {
+ RevCommit commit = walk.parseCommit(commitId);
+ if (commit.getParentCount() > parentNum) {
+ return commit.getParent(parentNum);
+ }
+ }
+ return null;
+ }
+
+ private GitUtil() {
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java
new file mode 100644
index 0000000..87bfa00
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/BatchUpdateTest.java
@@ -0,0 +1,134 @@
+package com.google.gerrit.server.git;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoOnlyOp;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BatchUpdateTest {
+ @Inject
+ private AccountManager accountManager;
+
+ @Inject
+ private IdentifiedUser.GenericFactory userFactory;
+
+ @Inject
+ private InMemoryDatabase schemaFactory;
+
+ @Inject
+ private InMemoryRepositoryManager repoManager;
+
+ @Inject
+ private SchemaCreator schemaCreator;
+
+ @Inject
+ private ThreadLocalRequestContext requestContext;
+
+ @Inject
+ private BatchUpdate.Factory batchUpdateFactory;
+
+ private LifecycleManager lifecycle;
+ private ReviewDb db;
+ private TestRepository<InMemoryRepository> repo;
+ private Project.NameKey project;
+ private IdentifiedUser user;
+
+ @Before
+ public void setUp() throws Exception {
+ Injector injector = Guice.createInjector(new InMemoryModule());
+ injector.injectMembers(this);
+ lifecycle = new LifecycleManager();
+ lifecycle.add(injector);
+ lifecycle.start();
+
+ db = schemaFactory.open();
+ schemaCreator.create(db);
+ Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
+ .getAccountId();
+ user = userFactory.create(userId);
+
+ project = new Project.NameKey("test");
+
+ InMemoryRepository inMemoryRepo = repoManager.createRepository(project);
+ repo = new TestRepository<>(inMemoryRepo);
+
+ requestContext.setContext(new RequestContext() {
+ @Override
+ public CurrentUser getUser() {
+ return user;
+ }
+
+ @Override
+ public Provider<ReviewDb> getReviewDbProvider() {
+ return Providers.of(db);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() {
+ if (repo != null) {
+ repo.getRepository().close();
+ }
+ if (lifecycle != null) {
+ lifecycle.stop();
+ }
+ requestContext.setContext(null);
+ if (db != null) {
+ db.close();
+ }
+ InMemoryDatabase.drop(schemaFactory);
+ }
+
+ @Test
+ public void addRefUpdateFromFastForwardCommit() throws Exception {
+ final RevCommit masterCommit = repo.branch("master").commit().create();
+ final RevCommit branchCommit =
+ repo.branch("branch").commit().parent(masterCommit).create();
+
+ try (BatchUpdate bu = batchUpdateFactory
+ .create(db, project, user, TimeUtil.nowTs())) {
+ bu.addRepoOnlyOp(new RepoOnlyOp() {
+ @Override
+ public void updateRepo(RepoContext ctx) throws Exception {
+ ctx.addRefUpdate(
+ new ReceiveCommand(masterCommit.getId(), branchCommit.getId(),
+ "refs/heads/master"));
+ }
+ });
+ bu.execute();
+ }
+
+ assertEquals(
+ repo.getRepository().exactRef("refs/heads/master").getObjectId(),
+ branchCommit.getId());
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
new file mode 100644
index 0000000..9327514
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
@@ -0,0 +1,41 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-account-entry">
+ <template>
+ <style>
+ gr-autocomplete {
+ display: inline-block;
+ overflow: hidden;
+ }
+ </style>
+ <gr-autocomplete
+ id="input"
+ borderless="[[borderless]]"
+ placeholder="[[placeholder]]"
+ threshold="[[suggestFrom]]"
+ query="[[query]]"
+ on-commit="_handleInputCommit"
+ clear-on-commit>
+ </gr-autocomplete>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-account-entry.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
new file mode 100644
index 0000000..23f0b13
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -0,0 +1,126 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-account-entry',
+
+ /**
+ * Fired when an account is entered.
+ *
+ * @event add
+ */
+
+ properties: {
+ borderless: Boolean,
+ change: Object,
+ placeholder: String,
+
+ suggestFrom: {
+ type: Number,
+ value: 3,
+ },
+
+ filter: {
+ type: Function,
+ value: function() {
+ return this.notOwnerOrReviewer.bind(this);
+ },
+ },
+
+ query: {
+ type: Function,
+ value: function() {
+ return this._getReviewerSuggestions.bind(this);
+ },
+ },
+
+ _reviewers: {
+ type: Array,
+ value: function() { return []; },
+ },
+ },
+
+ observers: [
+ '_reviewersChanged(change.reviewers.*, change.owner)',
+ ],
+
+ get focusStart() {
+ return this.$.input.focusStart;
+ },
+
+ focus: function() {
+ this.$.input.focus();
+ },
+
+ clear: function() {
+ this.$.input.clear();
+ },
+
+ _handleInputCommit: function(e) {
+ this.fire('add', {value: e.detail.value});
+ },
+
+ _reviewersChanged: function(changeRecord, owner) {
+ var reviewerSet = {};
+ reviewerSet[owner._account_id] = true;
+ var addReviewers = function(reviewers) {
+ if (!reviewers) {
+ return;
+ }
+ reviewers.forEach(function(reviewer) {
+ reviewerSet[reviewer._account_id] = true;
+ });
+ };
+
+ var reviewers = changeRecord.base;
+ addReviewers(reviewers.CC);
+ addReviewers(reviewers.REVIEWER);
+ this._reviewers = reviewerSet;
+ },
+
+ notOwnerOrReviewer: function(reviewer) {
+ var account = reviewer.account;
+ if (!account) { return true; }
+ return !this._reviewers[reviewer.account._account_id];
+ },
+
+ _makeSuggestion: function(reviewer) {
+ if (reviewer.account) {
+ return {
+ name: reviewer.account.name + ' (' + reviewer.account.email + ')',
+ value: reviewer,
+ };
+ } else if (reviewer.group) {
+ return {
+ name: reviewer.group.name + ' (group)',
+ value: reviewer,
+ };
+ }
+ },
+
+ _getReviewerSuggestions: function(input) {
+ var xhr = this.$.restAPI.getChangeSuggestedReviewers(
+ this.change._number, input);
+
+ return xhr.then(function(reviewers) {
+ if (!reviewers) { return []; }
+ return reviewers
+ .filter(this.filter)
+ .map(this._makeSuggestion);
+ }.bind(this));
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
new file mode 100644
index 0000000..f9527ca
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-account-entry</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-entry.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-entry></gr-account-entry>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-entry tests', function() {
+ var _nextAccountId = 0;
+ var makeAccount = function() {
+ var accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
+ name: 'name ' + accountId,
+ email: 'email ' + accountId,
+ };
+ };
+
+ var owner;
+ var existingReviewer1;
+ var existingReviewer2;
+ var suggestion1;
+ var suggestion2;
+ var suggestion3;
+ var element;
+
+ setup(function() {
+ owner = makeAccount();
+ existingReviewer1 = makeAccount();
+ existingReviewer2 = makeAccount();
+ suggestion1 = {account: makeAccount()};
+ suggestion2 = {account: makeAccount()};
+ suggestion3 = {
+ group: {
+ id: 'suggested group id',
+ name: 'suggested group',
+ },
+ };
+
+ element = fixture('basic');
+ element.change = {
+ owner: owner,
+ reviewers: {
+ CC: [existingReviewer1],
+ REVIEWER: [existingReviewer2],
+ },
+ };
+
+ stub('gr-rest-api-interface', {
+ getChangeSuggestedReviewers: function() {
+ var redundantSuggestion1 = {account: existingReviewer1};
+ var redundantSuggestion2 = {account: existingReviewer2};
+ var redundantSuggestion3 = {account: owner};
+ return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+ },
+ });
+ });
+
+ test('notOwnerOrReviewer', function() {
+ var account = makeAccount();
+ assert.isTrue(element.notOwnerOrReviewer({}));
+ assert.isTrue(element.notOwnerOrReviewer({account: account}));
+ assert.isFalse(element.notOwnerOrReviewer({account: owner}));
+ assert.isFalse(element.notOwnerOrReviewer({account: existingReviewer1}));
+ assert.isFalse(element.notOwnerOrReviewer({account: existingReviewer2}));
+ });
+
+ test('_makeSuggestion formats account or group accordingly', function() {
+ var account = makeAccount();
+ var suggestion = element._makeSuggestion({account: account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' (' + account.email + ')',
+ value: {account: account},
+ });
+
+ var group = {name: 'test'};
+ suggestion = element._makeSuggestion({group: group});
+ assert.deepEqual(suggestion, {
+ name: group.name + ' (group)',
+ value: {group: group},
+ });
+ });
+
+ test('_getReviewerSuggestions excludes owner+reviewers', function(done) {
+ element._getReviewerSuggestions().then(function(reviewers) {
+ assert.deepEqual(reviewers, [
+ element._makeSuggestion(suggestion1),
+ element._makeSuggestion(suggestion2),
+ element._makeSuggestion(suggestion3),
+ ]);
+ done();
+ });
+ });
+
+ test('_updateReviewers', function() {
+ // delete existingReviewer1
+ element.splice('change.reviewers.CC', 0, 1);
+ var expected = {};
+ expected[owner._account_id] = true;
+ expected[existingReviewer2._account_id] = true;
+ assert.deepEqual(element._reviewers, expected);
+
+ // delete existingReviewer2
+ element.splice('change.reviewers.REVIEWER', 0, 1);
+ delete expected[existingReviewer2._account_id];
+ assert.deepEqual(element._reviewers, expected);
+
+ // add two new reviewers
+ var account1 = makeAccount();
+ var account2 = makeAccount();
+ element.push('change.reviewers.CC', account1);
+ element.push('change.reviewers.REVIEWER', account2);
+ expected[account1._account_id] = true;
+ expected[account2._account_id] = true;
+ assert.deepEqual(element._reviewers, expected);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
new file mode 100644
index 0000000..e074e15
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -0,0 +1,53 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="../gr-account-entry/gr-account-entry.html">
+
+<dom-module id="gr-account-list">
+ <template>
+ <style>
+ gr-account-chip {
+ display: inline-block;
+ }
+ .group {
+ --account-label-suffix: ' (group)';
+ }
+ .pending-add {
+ font-style: italic;
+ }
+ </style>
+ <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
+ <gr-account-chip
+ account="[[account]]"
+ class$="[[_computeChipClass(account)]]"
+ data-account-id$="[[account._account_id]]"
+ removable="[[_computeRemovable(account)]]">
+ </gr-account-chip>
+ </template>
+ <gr-account-entry
+ borderless
+ hidden$="[[readonly]]"
+ id="entry"
+ change="[[change]]"
+ filter="[[filter]]"
+ placeholder="[[placeholder]]"
+ on-add="_handleAdd">
+ </gr-account-entry>
+ </template>
+ <script src="gr-account-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
new file mode 100644
index 0000000..828169d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -0,0 +1,130 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-account-list',
+
+ properties: {
+ accounts: {
+ type: Array,
+ value: function() { return []; },
+ },
+ change: Object,
+ placeholder: String,
+ readonly: Boolean,
+
+ filter: {
+ type: Function,
+ value: function() {
+ return this._filterSuggestion.bind(this);
+ },
+ },
+ },
+
+ listeners: {
+ 'remove': '_handleRemove',
+ },
+
+ get focusStart() {
+ return this.$.entry.focusStart;
+ },
+
+ _handleAdd: function(e) {
+ var reviewer = e.detail.value;
+ // Append new account or group to the accounts property. We add our own
+ // internal properties to the account/group here, so we clone the object
+ // to avoid cluttering up the shared change object.
+ // TODO(logan): Polyfill for Object.assign in IE.
+ if (reviewer.account) {
+ var account = Object.assign({}, reviewer.account, {_pendingAdd: true});
+ this.push('accounts', account);
+ } else if (reviewer.group) {
+ var group = Object.assign({}, reviewer.group,
+ {_pendingAdd: true, _group: true});
+ this.push('accounts', group);
+ }
+ },
+
+ _computeChipClass: function(account) {
+ var classes = [];
+ if (account._group) {
+ classes.push('group');
+ }
+ if (account._pendingAdd) {
+ classes.push('pendingAdd');
+ }
+ return classes.join(' ');
+ },
+
+ _computeRemovable: function(account) {
+ return !this.readonly && !!account._pendingAdd;
+ },
+
+ _filterSuggestion: function(reviewer) {
+ if (!this.$.entry.notOwnerOrReviewer(reviewer)) {
+ return false;
+ }
+ for (var i = 0; i < this.accounts.length; i++) {
+ var account = this.accounts[i];
+ if (!account._pendingAdd) {
+ continue;
+ }
+ if (reviewer.group && account._group &&
+ reviewer.group.id === account.id) {
+ return false;
+ }
+ if (reviewer.account && !account._group &&
+ account._account_id === account._account_id) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ _handleRemove: function(e) {
+ var toRemove = e.detail.account;
+ for (var i = 0; i < this.accounts.length; i++) {
+ var matches;
+ var account = this.accounts[i];
+ if (toRemove._group) {
+ matches = toRemove.id === account.id;
+ } else {
+ matches = toRemove._account_id === account._account_id;
+ }
+ if (matches) {
+ this.splice('accounts', i, 1);
+ return;
+ }
+ }
+ console.warn('received remove event for missing account',
+ e.detail.account);
+ },
+
+ additions: function() {
+ var result = [];
+ return this.accounts.filter(function(account) {
+ return account._pendingAdd;
+ }).map(function(account) {
+ if (account._group) {
+ return {group: account};
+ } else {
+ return {account: account};
+ }
+ });
+ return result;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
new file mode 100644
index 0000000..79d5e59
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-account-list</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-list></gr-account-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-list tests', function() {
+ var _nextAccountId = 0;
+ var makeAccount = function() {
+ var accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
+ };
+ };
+ var makeGroup = function() {
+ var groupId = 'group' + (++_nextAccountId);
+ return {
+ id: groupId,
+ };
+ };
+
+ var existingReviewer1;
+ var existingReviewer2;
+ var element;
+
+ function getChips() {
+ return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
+ }
+
+ setup(function() {
+ existingReviewer1 = makeAccount();
+ existingReviewer2 = makeAccount();
+
+ element = fixture('basic');
+ element.accounts = [existingReviewer1, existingReviewer2];
+
+ stub('gr-rest-api-interface', {
+ getConfig: function() {
+ return Promise.resolve({});
+ },
+ });
+ });
+
+ test('account entry only appears when editable', function() {
+ element.readonly = false;
+ assert.isFalse(element.$.entry.hasAttribute('hidden'));
+ element.readonly = true;
+ assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ });
+
+ test('addition and removal of account/group chips', function() {
+ flushAsynchronousOperations();
+
+ // Existing accounts are listed.
+ var chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+
+ // New accounts are added to end with pendingAdd class.
+ var newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 3);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+ assert.isTrue(chips[2].classList.contains('pendingAdd'));
+
+ // Removed accounts are taken out of the list.
+ element.fire('remove', {account: existingReviewer1});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Invalid remove is ignored.
+ element.fire('remove', {account: existingReviewer1});
+ element.fire('remove', {account: newAccount});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+
+ // New groups are added to end with pendingAdd and group classes.
+ var newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isTrue(chips[1].classList.contains('group'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Removed groups are taken out of the list.
+ element.fire('remove', {account: newGroup});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ });
+
+ test('_computeChipClass', function() {
+ var account = makeAccount();
+ assert.equal(element._computeChipClass(account), '');
+ account._pendingAdd = true;
+ assert.equal(element._computeChipClass(account), 'pendingAdd');
+ account._group = true;
+ assert.equal(element._computeChipClass(account), 'group pendingAdd');
+ account._pendingAdd = false;
+ assert.equal(element._computeChipClass(account), 'group');
+ });
+
+ test('_computeRemovable', function() {
+ var newAccount = makeAccount();
+ newAccount._pendingAdd = true;
+ element.readonly = false;
+ assert.isFalse(element._computeRemovable(existingReviewer1));
+ assert.isTrue(element._computeRemovable(newAccount));
+
+ element.readonly = true;
+ assert.isFalse(element._computeRemovable(existingReviewer1));
+ assert.isFalse(element._computeRemovable(newAccount));
+ });
+
+ test('additions returns sanitized new accounts and groups', function() {
+ assert.equal(element.additions().length, 0);
+
+ var newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ var newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+
+ assert.deepEqual(element.additions(), [
+ {
+ account: {
+ _account_id: newAccount._account_id,
+ _pendingAdd: true,
+ },
+ },
+ {
+ group: {
+ id: newGroup.id,
+ _group: true,
+ _pendingAdd: true,
+ },
+ },
+ ]);
+ });
+ });
+</script>
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 a0a9305..9ddd66d9 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
@@ -93,7 +93,7 @@
<span class="title">Reviewers</span>
<span class="value">
<gr-reviewer-list
- change="[[change]]"
+ change="{{change}}"
mutable="[[mutable]]"
suggest-from="[[serverConfig.suggest.from]]"></gr-reviewer-list>
</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 79f20ab..b90bed8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -237,7 +237,7 @@
<option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
<span>[[patchNumber]]</span>
/
- <span>[[_computeLatestPatchNum(_change)]]</span>
+ <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
</option>
</template>
</select>
@@ -248,10 +248,12 @@
<section class="changeInfo">
<div class="changeInfo-column changeMetadata">
<gr-change-metadata
- change="[[_change]]"
+ change="{{_change}}"
commit-info="[[_commitInfo]]"
server-config="[[serverConfig]]"
- mutable="[[_loggedIn]]"></gr-change-metadata>
+ mutable="[[_loggedIn]]"
+ on-show-reply-dialog="_handleShowReplyDialog">
+ </gr-change-metadata>
<gr-change-actions id="actions"
actions="[[_change.actions]]"
change-num="[[_changeNum]]"
@@ -311,7 +313,7 @@
on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
- change-num="[[_changeNum]]"
+ change="[[_change]]"
patch-num="[[_patchRange.patchNum]]"
revisions="[[_change.revisions]]"
labels="[[_change.labels]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f19d528..ac1855b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -189,6 +189,7 @@
if (isNaN(patchNum)) { return true; }
var change = changeRecord.base;
+ if (!change.current_revision) { return true; }
if (change.revisions[change.current_revision]._number !== patchNum) {
return true;
}
@@ -266,8 +267,13 @@
_handlePatchChange: function(e) {
var patchNum = e.target.value;
- var currentPatchNum =
- this._change.revisions[this._change.current_revision]._number;
+ var currentPatchNum;
+ if (this._change.current_revision) {
+ currentPatchNum =
+ this._change.revisions[this._change.current_revision]._number;
+ } else {
+ currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
+ }
if (patchNum == currentPatchNum) {
page.show(this.changePath(this._changeNum));
return;
@@ -310,6 +316,10 @@
this.$.replyOverlay.close();
},
+ _handleShowReplyDialog: function(e) {
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.REVIEWERS);
+ },
+
_paramsChanged: function(value) {
if (value.view !== this.tagName.toLowerCase()) { return; }
@@ -378,7 +388,7 @@
this._patchRange.basePatchNum || 'PARENT');
this.set('_patchRange.patchNum',
this._patchRange.patchNum ||
- change.revisions[change.current_revision]._number);
+ this._computeLatestPatchNum(this._allPatchSets));
var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
this.fire('title-change', {title: title});
@@ -399,8 +409,8 @@
return '(' + status.toLowerCase() + ')';
},
- _computeLatestPatchNum: function(change) {
- return change.revisions[change.current_revision]._number;
+ _computeLatestPatchNum: function(allPatchSets) {
+ return allPatchSets[allPatchSets.length - 1];
},
_computeAllPatchSets: function(change) {
@@ -496,9 +506,10 @@
});
},
- _openReplyDialog: function() {
+ _openReplyDialog: function(opt_section) {
this.$.replyOverlay.open().then(function() {
this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ this.$.replyDialog.focusOn(opt_section);
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 49e0239..bba0a63 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -170,6 +170,58 @@
element.fire('change', {}, {node: selectEl});
});
+ test('patch num change with missing current_revision', function(done) {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ status: 'NEW',
+ labels: {},
+ };
+ flushAsynchronousOperations();
+ var selectEl = element.$$('.header select');
+ assert.ok(selectEl);
+ var optionEls =
+ Polymer.dom(element.root).querySelectorAll('.header option');
+ assert.equal(optionEls.length, 4);
+ assert.isFalse(
+ element.$$('.header option[value="1"]').hasAttribute('selected'));
+ assert.isTrue(
+ element.$$('.header option[value="2"]').hasAttribute('selected'));
+ assert.isFalse(
+ element.$$('.header option[value="3"]').hasAttribute('selected'));
+ assert.equal(optionEls[3].value, 13);
+
+ var showStub = sinon.stub(page, 'show');
+
+ var numEvents = 0;
+ selectEl.addEventListener('change', function(e) {
+ numEvents++;
+ if (numEvents == 1) {
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+ selectEl.value = '3';
+ element.fire('change', {}, {node: selectEl});
+ } else if (numEvents == 2) {
+ assert(showStub.lastCall.calledWithExactly('/c/42/3'),
+ 'Should navigate to /c/42/3');
+ showStub.restore();
+ done();
+ }
+ });
+ selectEl.value = '1';
+ element.fire('change', {}, {node: selectEl});
+ });
+
test('change status new', function() {
element._changeNum = '1';
element._patchRange = {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 556691a..89cb4a2 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -21,6 +21,7 @@
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-account-list/gr-account-list.html">
<dom-module id="gr-reply-dialog">
<template>
@@ -43,11 +44,26 @@
section {
border-top: 1px solid #ddd;
padding: .5em .75em;
+ width: 100%;
}
.labelsContainer,
.actionsContainer {
flex-shrink: 0;
}
+ .peopleContainer {
+ display: table;
+ }
+ .peopleList {
+ display: flex;
+ }
+ .peopleListLabel {
+ color: #666;
+ min-width: 7em;
+ padding-right: .5em;
+ }
+ gr-account-list {
+ flex: 1;
+ }
.textareaContainer {
position: relative;
display: flex;
@@ -105,6 +121,24 @@
}
</style>
<div class="container">
+ <section class="peopleContainer">
+ <div class="peopleList">
+ <div class="peopleListLabel">Owner</div>
+ <gr-account-list readonly accounts="[[_owners]]">
+ </gr-account-list>
+ </div>
+ </section>
+ <section class="peopleContainer">
+ <div class="peopleList">
+ <div class="peopleListLabel">Reviewers</div>
+ <gr-account-list
+ id="reviewers"
+ accounts="[[_reviewers]]"
+ change="[[change]]"
+ placeholder="Add reviewer...">
+ </gr-account-list>
+ </div>
+ </section>
<section class="textareaContainer">
<iron-autogrow-textarea
id="textarea"
@@ -136,7 +170,7 @@
<template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
<span class="labelsNotShown">
Labels are not shown because this is not the most recent patch set.
- <a href$="/c/[[changeNum]]">Go to the latest patch set.</a>
+ <a href$="/c/[[change._number]]">Go to the latest patch set.</a>
</span>
</template>
</section>
@@ -144,7 +178,7 @@
<h3>[[_computeDraftsTitle(diffDrafts)]]</h3>
<gr-comment-list
comments="[[diffDrafts]]"
- change-num="[[changeNum]]"
+ change-num="[[change._number]]"
patch-num="[[patchNum]]"></gr-comment-list>
</section>
<section class="actionsContainer">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 0ee779e..9c2d023 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -14,6 +14,11 @@
(function() {
'use strict';
+ var FocusTarget = {
+ BODY: 'body',
+ REVIEWERS: 'reviewers',
+ };
+
Polymer({
is: 'gr-reply-dialog',
@@ -30,7 +35,7 @@
*/
properties: {
- changeNum: String,
+ change: Object,
patchNum: String,
revisions: Object,
disabled: {
@@ -47,12 +52,20 @@
permittedLabels: Object,
_account: Object,
+ _owners: Array,
+ _reviewers: Array,
},
+ FocusTarget: FocusTarget,
+
behaviors: [
Gerrit.RESTClientBehavior,
],
+ observers: [
+ '_changeUpdated(change.*)',
+ ],
+
attached: function() {
this._getAccount().then(function(account) {
this._account = account;
@@ -64,14 +77,22 @@
},
focus: function() {
- this.async(function() {
- this.$.textarea.textarea.focus();
- }.bind(this));
+ this.focusOn(FocusTarget.BODY);
+ },
+
+ focusOn: function(section) {
+ if (section === FocusTarget.BODY) {
+ var textarea = this.$.textarea;
+ textarea.async(textarea.textarea.focus.bind(textarea.textarea));
+ } else if (section === FocusTarget.REVIEWERS) {
+ var reviewerEntry = this.$.reviewers.focusStart;
+ reviewerEntry.async(reviewerEntry.focus);
+ }
},
getFocusStops: function() {
return {
- start: this.$.textarea.$.textarea,
+ start: this.$.reviewers.focusStart,
end: this.$.cancelButton,
};
},
@@ -105,6 +126,21 @@
if (this.draft != null) {
obj.message = this.draft;
}
+
+ var newReviewers = this.$.reviewers.additions();
+ newReviewers.forEach(function(reviewer) {
+ var reviewerId;
+ if (reviewer.account) {
+ reviewerId = reviewer.account._account_id;
+ } else if (reviewer.group) {
+ reviewerId = reviewer.group.id;
+ }
+ if (!obj.reviewers) {
+ obj.reviewers = [];
+ }
+ obj.reviewers.push({reviewer: reviewerId});
+ });
+
this.disabled = true;
return this._saveReview(obj).then(function(response) {
this.disabled = false;
@@ -182,6 +218,32 @@
return permittedLabels[label];
},
+ _changeUpdated: function(changeRecord) {
+ if (!changeRecord.path || !changeRecord.base) {
+ return;
+ }
+
+ if (changeRecord.path !== 'change' &&
+ changeRecord.path !== 'change.reviewers.CC.splices' &&
+ changeRecord.path !== 'change.reviewers.REVIEWER.splices') {
+ return;
+ }
+
+ var owner = changeRecord.base.owner;
+ this._owners = [owner];
+
+ if (!changeRecord.base.reviewers) {
+ return;
+ }
+
+ var reviewers = changeRecord.base.reviewers.REVIEWER || [];
+ reviewers = reviewers.concat(changeRecord.base.reviewers.CC);
+ reviewers = reviewers.filter(function(account) {
+ return account && account._account_id !== owner._account_id;
+ }.bind(this));
+ this._reviewers = reviewers;
+ },
+
_getAccount: function() {
return this.$.restAPI.getAccount();
},
@@ -197,7 +259,7 @@
},
_saveReview: function(review) {
- return this.$.restAPI.saveChangeReview(this.changeNum, this.patchNum,
+ return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
review);
},
});
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 4ff9f04..99e3cd9 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -18,7 +18,6 @@
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-reviewer-list">
@@ -45,19 +44,12 @@
gr-account-chip {
margin-top: .3em;
}
- .remove,
- .cancel {
+ .remove {
color: #999;
}
.remove {
font-size: .9em;
}
- .cancel {
- font-size: 2em;
- line-height: 1;
- padding: 0 .15em;
- text-decoration: none;
- }
@media screen and (max-width: 50em), screen and (min-width: 75em) {
gr-account-chip:first-of-type {
margin-top: 0;
@@ -72,28 +64,11 @@
</gr-account-chip>
</template>
<div class="controlsContainer" hidden$="[[!mutable]]">
- <div class="autocompleteContainer" hidden$="[[!_showInput]]">
- <div class="inputContainer">
- <gr-autocomplete
- id="input"
- threshold="[[suggestFrom]]"
- clear-on-commit
- query="[[_query]]"
- disabled="[[disabled]]"
- on-commit="_sendAddRequest"
- on-cancel="_handleCancelTap"></gr-autocomplete>
- <gr-button
- link
- class="cancel"
- on-tap="_handleCancelTap">×</gr-button>
- </div>
- </div>
<gr-button
link
id="addReviewer"
class="addReviewer"
- on-tap="_handleAddTap"
- hidden$="[[_showInput]]">Add reviewer</gr-button>
+ on-tap="_handleAddTap">Add reviewer</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 1410821..7037fc2 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -17,17 +17,23 @@
Polymer({
is: 'gr-reviewer-list',
+ /**
+ * Fired when the "Add reviewer..." button is tapped.
+ *
+ * @event show-reply-dialog
+ */
+
properties: {
change: Object,
- mutable: {
- type: Boolean,
- value: false,
- },
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
+ mutable: {
+ type: Boolean,
+ value: false,
+ },
suggestFrom: {
type: Number,
value: 3,
@@ -42,13 +48,6 @@
value: false,
},
- _query: {
- type: Function,
- value: function() {
- return this._getReviewerSuggestions.bind(this);
- },
- },
-
// Used for testing.
_lastAutocompleteRequest: Object,
_xhrPromise: Object,
@@ -111,98 +110,11 @@
_handleAddTap: function(e) {
e.preventDefault();
- this._showInput = true;
- this.$.input.focus();
- },
-
- _handleCancelTap: function(e) {
- e.preventDefault();
- this.$.input.clear();
- this._cancel();
- },
-
- _cancel: function() {
- this._showInput = false;
- this.$.input.clear();
- this.$.addReviewer.focus();
- },
-
- _sendAddRequest: function(e, detail) {
- var reviewer = detail.value;
- var reviewerID;
- if (reviewer.account) {
- reviewerID = reviewer.account._account_id;
- } else if (reviewer.group) {
- reviewerID = reviewer.group.id;
- }
-
- this.disabled = true;
- this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
- this.change.reviewers.CC = this.change.reviewers.CC || [];
- this.disabled = false;
- if (!response.ok) { return response; }
-
- return this.$.restAPI.getResponseObject(response).then(function(obj) {
- obj.reviewers.forEach(function(r) {
- this.push('change.removable_reviewers', r);
- this.push('change.reviewers.CC', r);
- }, this);
- this.$.input.focus();
- }.bind(this));
- }.bind(this)).catch(function(err) {
- this.disabled = false;
- throw err;
- }.bind(this));
- },
-
- _addReviewer: function(id) {
- return this.$.restAPI.addChangeReviewer(this.change._number, id);
+ this.fire('show-reply-dialog');
},
_removeReviewer: function(id) {
return this.$.restAPI.removeChangeReviewer(this.change._number, id);
},
-
- _notInList: function(reviewer) {
- var account = reviewer.account;
- if (!account) { return true; }
- if (account._account_id === this.change.owner._account_id) {
- return false;
- }
- for (var i = 0; i < this._reviewers.length; i++) {
- if (account._account_id === this._reviewers[i]._account_id) {
- return false;
- }
- }
- return true;
- },
-
- _makeSuggestion: function(reviewer) {
- if (reviewer.account) {
- return {
- name: reviewer.account.name + ' (' + reviewer.account.email + ')',
- value: reviewer,
- };
- } else if (reviewer.group) {
- return {
- name: reviewer.group.name + ' (group)',
- value: reviewer,
- };
- }
- },
-
- _getReviewerSuggestions: function(input) {
- var xhr = this.$.restAPI.getChangeSuggestedReviewers(
- this.change._number, input);
-
- this._lastAutocompleteRequest = xhr;
-
- return xhr.then(function(reviewers) {
- if (!reviewers) { return []; }
- return reviewers
- .filter(this._notInList.bind(this))
- .map(this._makeSuggestion);
- }.bind(this));
- },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 9be76d9..e6f7a20 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,56 +34,10 @@
<script>
suite('gr-reviewer-list tests', function() {
var element;
- var autocompleteInput;
setup(function() {
element = fixture('basic');
- autocompleteInput = element.$.input.$.input;
stub('gr-rest-api-interface', {
- getChangeSuggestedReviewers: function() {
- return Promise.resolve([
- {
- account: {
- _account_id: 1021482,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- }
- },
- {
- account: {
- _account_id: 1021863,
- name: 'Andrew Bonventre',
- email: 'andybons@google.com',
- }
- },
- {
- group: {
- id: 'c7af6dd375c092ff3b23c0937aa910693dc0c41b',
- name: 'andy',
- }
- }
- ]);
- },
- addChangeReviewer: function() {
- return Promise.resolve({
- ok: true,
- text: function() {
- return Promise.resolve(
- ')]}\'\n' +
- JSON.stringify({
- reviewers: [{
- _account_id: 1021482,
- approvals: {
- 'Code-Review': ' 0'
- },
- email: 'andybons@chromium.org',
- name: 'Andrew Bonventre',
- }]
- })
- );
- },
- });
- },
removeChangeReviewer: function() {
return Promise.resolve({ok: true});
},
@@ -97,30 +51,11 @@
assert.isFalse(element.$$('.controlsContainer').hasAttribute('hidden'));
});
- function getActiveElement() {
- return document.activeElement.shadowRoot ?
- document.activeElement.shadowRoot.activeElement :
- document.activeElement;
- }
-
- test('show/hide input', function() {
- element.mutable = true;
- assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
- assert.isTrue(
- element.$$('.autocompleteContainer').hasAttribute('hidden'));
- assert.notEqual(getActiveElement().id, 'input');
+ test('add reviewer button opens reply dialog', function(done) {
+ element.addEventListener('show-reply-dialog', function() {
+ done();
+ });
MockInteractions.tap(element.$$('.addReviewer'));
- assert.isTrue(element.$$('.addReviewer').hasAttribute('hidden'));
- assert.isFalse(
- element.$$('.autocompleteContainer').hasAttribute('hidden'));
- assert.equal(getActiveElement().id, 'input');
-
- MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
-
- assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
- assert.isTrue(
- element.$$('.autocompleteContainer').hasAttribute('hidden'));
- assert.equal(getActiveElement().id, 'addReviewer');
});
test('only show remove for removable reviewers', function() {
@@ -178,127 +113,5 @@
}
});
});
-
- test('autocomplete starts at >= 3 chars', function() {
- element._inputRequestTimeout = 0;
- element._mutable = true;
- element.change = {_number: 123};
-
- element.$.input.text = 'fo';
-
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.restAPI.getChangeSuggestedReviewers.called);
- });
-
- test('add/remove reviewer flow', function(done) {
- element.change = {
- _number: 42,
- reviewers: {},
- removable_reviewers: [],
- owner: {_account_id: 0},
- };
- element._inputRequestTimeout = 0;
- element._mutable = true;
- MockInteractions.tap(element.$$('.addReviewer'));
- flushAsynchronousOperations();
- element.$.input.text = 'andy';
-
- element._lastAutocompleteRequest.then(function() {
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(autocompleteInput, 27); // 'esc'
- assert.isTrue(element.$$('.autocompleteContainer')
- .hasAttribute('hidden'));
-
- MockInteractions.tap(element.$$('.addReviewer'));
-
- element.$.input.text = 'andyb';
- element._lastAutocompleteRequest.then(function() {
-
- MockInteractions.pressAndReleaseKeyOn(
- autocompleteInput, 13); // 'enter'
- assert.isTrue(element.disabled);
-
- element._xhrPromise.then(function() {
- assert.isFalse(element.disabled);
- flushAsynchronousOperations();
- var reviewerEls =
- Polymer.dom(element.root).querySelectorAll('.reviewer');
- assert.equal(reviewerEls.length, 1);
- MockInteractions.tap(element.$$('.reviewer').$$('gr-button'));
- flushAsynchronousOperations();
- assert.isTrue(element.disabled);
-
- element._xhrPromise.then(function() {
- flushAsynchronousOperations();
- assert.isFalse(element.disabled);
- var reviewerEls =
- Polymer.dom(element.root).querySelectorAll('.reviewer');
- assert.equal(reviewerEls.length, 0);
- done();
- });
- });
- });
- });
- });
-
- test('_makeSuggestion', function() {
- var account = {
- _account_id: 123456,
- name: 'name',
- email: 'email'
- };
- var group = {
- id: '123456',
- name: 'name',
- };
-
- var suggestion = element._makeSuggestion({account: account});
-
- assert.deepEqual(suggestion, {
- name: 'name (email)',
- value: {account: account},
- });
-
- suggestion = element._makeSuggestion({group: group});
-
- assert.deepEqual(suggestion, {
- name: 'name (group)',
- value: {group: group},
- });
- });
-
- test('_notInList', function() {
- var group = {
- id: '123456',
- name: 'name',
- };
- var account = {
- _account_id: 123456,
- name: 'name',
- email: 'email',
- };
-
- element.change = {owner: {_account_id: 123456}};
-
- // Is true when passing a group.
- assert.isTrue(element._notInList({group: group}));
-
- // Is false when passing the change owner.
- assert.isFalse(element._notInList({account: account}));
-
- element.change.owner._account_id = 789;
-
- // Is true when passing a different user than the change owner, and is not
- // in the reviewer list.
- assert.isTrue(element._notInList({account: account}));
-
- element._reviewers = [{_account_id: 123456}];
-
- // Is false when passing a different user than the change owner, but *is*
- // the reviewer list.
- assert.isFalse(element._notInList({account: account}));
- });
});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 90e1ef8..71774e3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -50,7 +50,7 @@
test('show auth error', function(done) {
var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
element.fire('server-error', {response: {status: 403}});
- flush(function() {
+ element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() {
assert.isTrue(showAuthErrorStub.calledOnce);
done();
});
@@ -90,7 +90,7 @@
var toastSpy = sandbox.spy(element, '_createToastAlert');
var windowOpen = sandbox.stub(window, 'open');
element.fire('server-error', {response: {status: 403}});
- flush(function() {
+ element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() {
assert.isTrue(toastSpy.called);
var toast = toastSpy.lastCall.returnValue;
assert.isOk(toast);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index feb21e2..6b1e59e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -19,7 +19,7 @@
function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
revisionImage) {
- GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+ GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl, []);
this._baseImage = baseImage;
this._revisionImage = revisionImage;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 1044b77..0d9ecbc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -17,8 +17,8 @@
// Prevent redefinition.
if (window.GrDiffBuilderSideBySide) { return; }
- function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl) {
- GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+ function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl, layers) {
+ GrDiffBuilder.call(this, diff, comments, prefs, outputEl, layers);
}
GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
@@ -57,7 +57,7 @@
if (action) {
row.appendChild(action);
} else {
- var textEl = this._createTextEl(line);
+ var textEl = this._createTextEl(line, side);
var threadEl = this._commentThreadForLine(line, side);
if (threadEl) {
textEl.appendChild(threadEl);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index e69f369..d9c9b1b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -17,8 +17,8 @@
// Prevent redefinition.
if (window.GrDiffBuilderUnified) { return; }
- function GrDiffBuilderUnified(diff, comments, prefs, outputEl) {
- GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+ function GrDiffBuilderUnified(diff, comments, prefs, outputEl, layers) {
+ GrDiffBuilder.call(this, diff, comments, prefs, outputEl, layers);
}
GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index bf78708..0aa5c94 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -15,12 +15,16 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
+<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
<dom-module id="gr-diff-builder">
<template>
<div class="contentWrapper">
<content></content>
</div>
+ <gr-ranged-comment-layer
+ id="rangeLayer"
+ comments="[[comments]]"></gr-ranged-comment-layer>
<gr-diff-processor
id="processor"
groups="{{_groups}}"></gr-diff-processor>
@@ -52,11 +56,13 @@
properties: {
viewMode: String,
+ comments: Object,
isImageDiff: Boolean,
baseImage: Object,
revisionImage: Object,
_builder: Object,
_groups: Array,
+ _layers: Array,
},
get diffElement() {
@@ -67,6 +73,14 @@
'_groupsChanged(_groups.splices)',
],
+ attached: function() {
+ // Setup annotation layers.
+ this._layers = [
+ this._createIntralineLayer(),
+ this.$.rangeLayer,
+ ];
+ },
+
render: function(diff, comments, prefs) {
// Stop the processor (if it's running).
this.$.processor.cancel();
@@ -216,10 +230,10 @@
this.diffElement, this.baseImage, this.revisionImage);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
return new GrDiffBuilderSideBySide(
- diff, comments, prefs, this.diffElement);
+ diff, comments, prefs, this.diffElement, this._layers);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
return new GrDiffBuilderUnified(
- diff, comments, prefs, this.diffElement);
+ diff, comments, prefs, this.diffElement, this._layers);
}
throw Error('Unsupported diff view mode: ' + this.viewMode);
},
@@ -256,6 +270,35 @@
}
}, this);
},
+
+ _createIntralineLayer: function() {
+ return {
+ addListener: function() {},
+
+ // Take a DIV.contentText element and a line object with intraline
+ // differences to highlight and apply them to the element as
+ // annotations.
+ annotate: function(el, line, GrAnnotation) {
+ var HL_CLASS = 'style-scope gr-diff';
+ line.highlights.forEach(function(highlight) {
+ // The start and end indices could be the same if a highlight is
+ // meant to start at the end of a line and continue onto the
+ // next one. Ignore it.
+ if (highlight.startIndex === highlight.endIndex) { return; }
+
+ // If endIndex isn't present, continue to the end of the line.
+ var endIndex = highlight.endIndex === undefined ?
+ line.text.length : highlight.endIndex;
+
+ GrAnnotation.annotateElement(
+ el,
+ highlight.startIndex,
+ endIndex - highlight.startIndex,
+ HL_CLASS);
+ });
+ },
+ };
+ },
});
})();
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index fb2d230..389d290 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -19,12 +19,18 @@
var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
- function GrDiffBuilder(diff, comments, prefs, outputEl) {
+ function GrDiffBuilder(diff, comments, prefs, outputEl, layers) {
this._diff = diff;
this._comments = comments;
this._prefs = prefs;
this._outputEl = outputEl;
this.groups = [];
+
+ this.layers = layers || [];
+
+ this.layers.forEach(function(layer) {
+ layer.addListener(this._handleLayerUpdate.bind(this));
+ }.bind(this));
}
GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
@@ -165,7 +171,8 @@
for (var i = 0; i < lines.length; i++) {
line = lines[i];
el = elements[i];
- el.parentElement.replaceChild(this._createTextEl(line).firstChild, el);
+ el.parentElement.replaceChild(this._createTextEl(line, side).firstChild,
+ el);
}
};
@@ -350,7 +357,7 @@
return td;
};
- GrDiffBuilder.prototype._createTextEl = function(line) {
+ GrDiffBuilder.prototype._createTextEl = function(line, opt_side) {
var td = this._createElement('td');
if (line.type !== GrDiffLine.Type.BLANK) {
td.classList.add('content');
@@ -366,6 +373,9 @@
}
var contentText = this._createElement('div', 'contentText');
+ if (opt_side) {
+ contentText.setAttribute('data-side', opt_side);
+ }
// If the html is equivalent to the text then it didn't get highlighted
// or escaped. Use textContent which is faster than innerHTML.
@@ -377,9 +387,10 @@
td.classList.add(line.highlights.length > 0 ?
'lightHighlight' : 'darkHighlight');
- if (line.highlights.length > 0) {
- this._addIntralineHighlights(contentText, line);
- }
+
+ this.layers.forEach(function(layer) {
+ layer.annotate(contentText, line, GrAnnotation);
+ });
td.appendChild(contentText);
@@ -488,33 +499,6 @@
return result;
};
- /**
- * Take a DIV.contentText element and a line object with intraline differences
- * to highlight and apply them to the element as annotations.
- * @param {HTMLDivElement} el
- * @param {[type]} line
- */
- GrDiffBuilder.prototype._addIntralineHighlights = function(el, line) {
- var HL_CLASS = 'style-scope gr-diff';
-
- line.highlights.forEach(function(highlight) {
- // The start and end indices could be the same if a highlight is meant
- // to start at the end of a line and continue onto the next one.
- // Ignore it.
- if (highlight.startIndex === highlight.endIndex) { return; }
-
- // If endIndex isn't present, continue to the end of the line.
- var endIndex = highlight.endIndex === undefined ?
- line.text.length : highlight.endIndex;
-
- GrAnnotation.annotateElement(
- el,
- highlight.startIndex,
- endIndex - highlight.startIndex,
- HL_CLASS);
- });
- };
-
GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
// Force this to be a number to prevent arbitrary injection.
tabSize = +tabSize;
@@ -549,5 +533,9 @@
return el;
};
+ GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) {
+ this._renderContentByRange(start, end, side);
+ };
+
window.GrDiffBuilder = GrDiffBuilder;
})(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 12c8f3c..f3d24f7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -20,12 +20,13 @@
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
+<script src="../gr-diff-highlight/gr-annotation.js"></script>
<script src="gr-diff-builder.js"></script>
-<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../gr-diff-cursor/mock-diff-response_test.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-diff-builder.html">
<test-fixture id="basic">
@@ -255,6 +256,7 @@
var el;
var str;
var annotateElementSpy;
+ var layer;
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
@@ -264,19 +266,21 @@
el = fixture('div-with-text');
str = el.textContent;
annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+ layer = document.createElement('gr-diff-builder')
+ ._createIntralineLayer();
});
teardown(function() {
annotateElementSpy.restore();
});
- test('_addIntralineHighlights no highlights', function() {
+ test('annotate no highlights', function() {
var line = {
text: str,
highlights: [],
};
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
// The content is unchanged.
assert.isFalse(annotateElementSpy.called);
@@ -285,7 +289,7 @@
assert.equal(str, el.childNodes[0].textContent);
});
- test('_addIntralineHighlights with highlights', function() {
+ test('annotate with highlights', function() {
var line = {
text: str,
highlights: [
@@ -299,7 +303,7 @@
var str3 = slice(str, 18, 22);
var str4 = slice(str, 22);
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 5);
@@ -320,7 +324,7 @@
assert.equal(el.childNodes[4].textContent, str4);
});
- test('_addIntralineHighlights without endIndex', function() {
+ test('annotate without endIndex', function() {
var line = {
text: str,
highlights: [
@@ -331,7 +335,7 @@
var str0 = slice(str, 0, 28);
var str1 = slice(str, 28);
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 2);
@@ -343,7 +347,7 @@
assert.equal(el.childNodes[1].textContent, str1);
});
- test('_addIntralineHighlights ignores empty highlights', function() {
+ test('annotate ignores empty highlights', function() {
var line = {
text: str,
highlights: [
@@ -351,17 +355,16 @@
],
};
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
assert.isFalse(annotateElementSpy.called);
assert.equal(el.childNodes.length, 1);
});
- test('_addIntralineHighlights handles unicode', function() {
+ test('annotate handles unicode', function() {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
-
var line = {
text: str,
highlights: [
@@ -373,7 +376,7 @@
var str1 = slice(str, 6, 12);
var str2 = slice(str, 12);
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 3);
@@ -388,7 +391,7 @@
assert.equal(el.childNodes[2].textContent, str2);
});
- test('_addIntralineHighlights handles unicode w/o endIndex', function() {
+ test('annotate handles unicode w/o endIndex', function() {
// Put some unicode into the string:
str = str.replace(/\s/g, '💢');
el.textContent = str;
@@ -403,7 +406,7 @@
var str0 = slice(str, 0, 6);
var str1 = slice(str, 6);
- GrDiffBuilder.prototype._addIntralineHighlights(el, line);
+ layer.annotate(el, line, GrAnnotation);
assert.isTrue(annotateElementSpy.called);
assert.equal(el.childNodes.length, 2);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 3baf200..a2d7fd0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -15,7 +15,6 @@
'use strict';
var STORAGE_DEBOUNCE_INTERVAL = 400;
- var UPDATE_DEBOUNCE_INTERVAL = 500;
Polymer({
is: 'gr-diff-comment',
@@ -160,7 +159,7 @@
_fireUpdate: function() {
this.debounce('fire-update', function() {
this.fire('comment-update', this._getEventPayload());
- }, UPDATE_DEBOUNCE_INTERVAL);
+ });
},
_draftChanged: function(draft) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index de5eb48..54dfad2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -236,21 +236,19 @@
element._xhrPromise.then(function(draft) {
assert(fireStub.calledWith('comment-save'),
'comment-save should be sent');
- assert.deepEqual(fireStub.lastCall.args, [
- 'comment-save', {
- comment: {
- __draft: true,
- __draftID: 'temp_draft_id',
- __editing: false,
- id: 'baf0414d_40572e03',
- line: 5,
- message: 'saved!',
- path: '/path/to/file',
- updated: '2015-12-08 21:52:36.177000000',
- },
- patchNum: 1,
+ assert.deepEqual(fireStub.lastCall.args[1], {
+ comment: {
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ __editing: false,
+ id: 'baf0414d_40572e03',
+ line: 5,
+ message: 'saved!',
+ path: '/path/to/file',
+ updated: '2015-12-08 21:52:36.177000000',
},
- ]);
+ patchNum: 1,
+ });
assert.isFalse(element.disabled,
'Element should be enabled when done creating draft.');
assert.equal(draft.message, 'saved!');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 4c5ee62..5bdd138 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -25,7 +25,7 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../gr-diff/gr-diff.html">
<link rel="import" href="./gr-diff-cursor.html">
-<link rel="import" href="./mock-diff-response_test.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<test-fixture id="basic">
<template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index e44086e..54294a1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -24,11 +24,11 @@
position: relative;
}
.contentWrapper ::content .range {
- background-color: #ffd500 !important;
+ background-color: rgba(255,213,0,0.5);
display: inline;
}
.contentWrapper ::content .rangeHighlight {
- background-color: #ff0 !important;
+ background-color: rgba(255,255,0,0.5);
display: inline;
}
</style>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index e84f6be..86f660b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,9 +14,6 @@
(function() {
'use strict';
- var RANGE_HIGHLIGHT = 'range';
- var HOVER_HIGHLIGHT = 'rangeHighlight';
-
Polymer({
is: 'gr-diff-highlight',
@@ -27,13 +24,9 @@
},
listeners: {
- 'comment-discard': '_handleCommentDiscard',
'comment-mouse-out': '_handleCommentMouseOut',
'comment-mouse-over': '_handleCommentMouseOver',
'create-comment': '_createComment',
- 'render': '_handleRender',
- 'show-context': '_handleShowContext',
- 'thread-discard': '_handleThreadDiscard',
},
get diffBuilder() {
@@ -56,21 +49,6 @@
return !!this.$$('gr-selection-action-box');
},
- _handleThreadDiscard: function(e) {
- var comment = e.detail.lastComment;
- // Comment Element was removed from DOM already.
- if (comment.range) {
- this._renderCommentRange(comment, e.target);
- }
- },
-
- _handleCommentDiscard: function(e) {
- var comment = e.detail.comment;
- if (comment.range) {
- this._renderCommentRange(comment, e.target);
- }
- },
-
_handleSelectionChange: function() {
// Can't use up or down events to handle selection started and/or ended in
// in comment threads or outside of diff.
@@ -79,45 +57,36 @@
this.debounce('selectionChange', this._handleSelection, 200);
},
- _handleRender: function() {
- this._applyAllHighlights();
- },
-
- _handleShowContext: function() {
- // TODO (viktard): Re-render expanded sections only.
- this._applyAllHighlights();
- },
-
_handleCommentMouseOver: function(e) {
var comment = e.detail.comment;
- var range = comment.range;
- if (!range) {
- return;
- }
+ if (!comment.range) { return; }
var lineEl = this.diffBuilder.getLineElByChild(e.target);
var side = this.diffBuilder.getSideByLineEl(lineEl);
- this._applyRangedHighlight(
- HOVER_HIGHLIGHT, range.start_line, range.start_character,
- range.end_line, range.end_character, side);
+ var index = this._indexOfComment(side, comment);
+ if (index !== undefined) {
+ this.set(['comments', side, index, '__hovering'], true);
+ }
},
_handleCommentMouseOut: function(e) {
var comment = e.detail.comment;
- var range = comment.range;
- if (!range) {
- return;
- }
+ if (!comment.range) { return; }
var lineEl = this.diffBuilder.getLineElByChild(e.target);
var side = this.diffBuilder.getSideByLineEl(lineEl);
- var contentEls = this.diffBuilder.getContentsByLineRange(
- range.start_line, range.end_line, side);
- contentEls.forEach(function(content) {
- Polymer.dom(content).querySelectorAll('.' + HOVER_HIGHLIGHT).forEach(
- function(el) {
- el.classList.remove(HOVER_HIGHLIGHT);
- el.classList.add(RANGE_HIGHLIGHT);
- });
- }, this);
+ var index = this._indexOfComment(side, comment);
+ if (index !== undefined) {
+ this.set(['comments', side, index, '__hovering'], false);
+ }
+ },
+
+ _indexOfComment: function(side, comment) {
+ var idProp = comment.id ? 'id' : '__draftID';
+ for (var i = 0; i < this.comments[side].length; i++) {
+ if (comment[idProp] &&
+ this.comments[side][i][idProp] === comment[idProp]) {
+ return i;
+ }
+ }
},
/**
@@ -226,26 +195,8 @@
}
},
- _renderCommentRange: function(comment, el) {
- var lineEl = this.diffBuilder.getLineElByChild(el);
- if (!lineEl) {
- return;
- }
- var side = this.diffBuilder.getSideByLineEl(lineEl);
- this._rerenderByLines(
- comment.range.start_line, comment.range.end_line, side);
- },
-
_createComment: function(e) {
this._removeActionBox();
- var side = e.detail.side;
- var range = e.detail.range;
- if (!range) {
- return;
- }
- this._applyRangedHighlight(
- RANGE_HIGHLIGHT, range.startLine, range.startChar,
- range.endLine, range.endChar, side);
},
_removeActionBoxDebounced: function() {
@@ -314,226 +265,5 @@
return GrAnnotation.getLength(node);
}
},
-
- /**
- * Creates hl tag with cssClass for starting side of range highlight.
- *
- * @param {!Element} startContent Range start diff content
- * aka div.contentText.
- * @param {!Element} endContent Range end diff content
- * aka div.contentText.
- * @param {number} startOffset Range start within start content.
- * @param {number} endOffset Range end within end content.
- * @param {string} cssClass
- * @return {!Element} Range start node.
- */
- _normalizeStart: function(
- startContent, endContent, startOffset, endOffset, cssClass) {
- var isOneLine = startContent === endContent;
- var startNode = startContent.firstChild;
- var length = endOffset - startOffset;
-
- if (!startNode) {
- return startNode;
- }
-
- // Skip nodes before startOffset.
- var nodeLength = this._getLength(startNode);
- while (startNode && (nodeLength <= startOffset || nodeLength === 0)) {
- startOffset -= nodeLength;
- startNode = startNode.nextSibling;
- nodeLength = startNode && this._getLength(startNode);
- }
- if (!startNode) { return null; }
-
- // Split Text node.
- if (startNode instanceof Text) {
- startNode = GrAnnotation.splitAndWrapInHighlight(
- startNode, startOffset, cssClass);
- // Edge case: single line, text node wraps the highlight.
- if (isOneLine && this._getLength(startNode) > length) {
- var extra = GrAnnotation.splitTextNode(startNode.firstChild, length);
- startContent.insertBefore(extra, startNode.nextSibling);
- startContent.normalize();
- }
- } else if (startNode.tagName == 'HL') {
- if (!startNode.classList.contains(cssClass)) {
- // Edge case: single line, <hl> wraps the highlight.
- // Should leave wrapping HL's content after the highlight.
- if (isOneLine && startOffset + length < this._getLength(startNode)) {
- GrAnnotation.splitNode(startNode, startOffset + length);
- }
- startNode = GrAnnotation.splitAndWrapInHighlight(
- startNode, startOffset, cssClass);
- }
- } else if (startNode.tagName == 'SPAN') {
- startNode = GrAnnotation.splitAndWrapInHighlight(
- startNode, startOffset, cssClass);
- } else {
- startNode = null;
- }
- return startNode;
- },
-
- /**
- * Creates hl tag with cssClass for ending side of range highlight.
- *
- * @param {!Element} startContent Range start diff content
- * aka div.contentText.
- * @param {!Element} endContent Range end diff content
- * aka div.contentText.
- * @param {number} startOffset Range start within start content.
- * @param {number} endOffset Range end within end content.
- * @param {string} cssClass
- * @return {!Element} Range start node.
- */
- _normalizeEnd: function(
- startContent, endContent, startOffset, endOffset, cssClass) {
- var endNode = endContent.firstChild;
-
- if (!endNode) {
- return endNode;
- }
-
- // Find the node where endOffset points at.
- var nodeLength = this._getLength(endNode);
- while (endNode && (nodeLength < endOffset || nodeLength === 0)) {
- endOffset -= nodeLength;
- endNode = endNode.nextSibling;
- nodeLength = endNode && this._getLength(endNode);
- }
- if (!endNode) { return null; }
-
- if (endNode instanceof Text) {
- endNode = GrAnnotation.splitAndWrapInHighlight(
- endNode, endOffset, cssClass, true);
- } else if (endNode.tagName == 'HL') {
- if (!endNode.classList.contains(cssClass)) {
- // Split text inside HL.
- var hl = endNode;
- endNode = GrAnnotation.splitAndWrapInHighlight(
- endNode, endOffset, cssClass, true);
- if (hl.textContent.length === 0) {
- hl.remove();
- }
- }
- } else {
- endNode = null;
- }
- return endNode;
- },
-
- /**
- * Applies highlight to first and last lines in range.
- *
- * @param {!Element} startContent Range start diff content
- * aka div.contentText.
- * @param {!Element} endContent Range end diff content
- * aka div.contentText.
- * @param {number} startOffset Range start within start content.
- * @param {number} endOffset Range end within end content.
- * @param {string} cssClass
- */
- _highlightSides: function(
- startContent, endContent, startOffset, endOffset, cssClass) {
- var isOneLine = startContent === endContent;
- var startNode = this._normalizeStart(
- startContent, endContent, startOffset, endOffset, cssClass);
- var endNode = this._normalizeEnd(
- startContent, endContent, startOffset, endOffset, cssClass);
-
- // Grow starting highlight until endNode or end of line.
- if (startNode && startNode != endNode) {
- var growStartHl = function(node) {
- if (node instanceof Text || node.tagName === 'SPAN') {
- startNode.appendChild(node);
- } else if (node.tagName === 'HL') {
- this._traverseContentSiblings(node.firstChild, growStartHl);
- node.remove();
- }
- return node == endNode;
- }.bind(this);
- this._traverseContentSiblings(startNode.nextSibling, growStartHl);
- startNode.normalize();
- }
-
- if (!isOneLine && endNode) {
- var growEndHl = function(node) {
- if (node instanceof Text || node.tagName === 'SPAN') {
- endNode.insertBefore(node, endNode.firstChild);
- } else if (node.tagName === 'HL') {
- this._traverseContentSiblings(node.firstChild, growEndHl);
- node.remove();
- }
- }.bind(this);
- // Prepend text up to line start to the ending highlight.
- this._traverseContentSiblings(
- endNode.previousSibling, growEndHl, {left: true});
- endNode.normalize();
- }
- },
-
- /**
- * @param {string} cssClass
- * @param {number} startLine Range start code line number.
- * @param {number} startCol Range start column number.
- * @param {number} endLine Range end line number.
- * @param {number} endCol Range end column number.
- * @param {string=} opt_side Side selector (right or left).
- */
- _applyRangedHighlight: function(
- cssClass, startLine, startCol, endLine, endCol, opt_side) {
- var startEl = this.diffBuilder.getContentByLine(startLine, opt_side);
- var endEl = this.diffBuilder.getContentByLine(endLine, opt_side);
- this._highlightSides(startEl, endEl, startCol, endCol, cssClass);
- if (endLine - startLine > 1) {
- // There is at least one line in between.
- var contents = this.diffBuilder.getContentsByLineRange(
- startLine + 1, endLine - 1, opt_side);
- contents.forEach(function(content) {
- if (!content.firstChild) {
- return;
- }
- // Wrap contents in highlight.
- var hl = GrAnnotation.wrapInHighlight(content.firstChild, cssClass);
- var wrapInHl = function(node) {
- if (node instanceof Text || node.tagName === 'SPAN') {
- hl.appendChild(node);
- } else if (node.tagName === 'HL') {
- this._traverseContentSiblings(node.firstChild, wrapInHl);
- node.remove();
- }
- return node === content.lastChild;
- }.bind(this);
- this._traverseContentSiblings(hl.nextSibling, wrapInHl);
- hl.normalize();
- }, this);
- }
- },
-
- _applyAllHighlights: function() {
- var rangedLeft =
- this.comments.left.filter(function(item) { return !!item.range; });
- var rangedRight =
- this.comments.right.filter(function(item) { return !!item.range; });
- rangedLeft.forEach(function(item) {
- var range = item.range;
- this._applyRangedHighlight(
- RANGE_HIGHLIGHT, range.start_line, range.start_character,
- range.end_line, range.end_character, 'left');
- }, this);
- rangedRight.forEach(function(item) {
- var range = item.range;
- this._applyRangedHighlight(
- RANGE_HIGHLIGHT, range.start_line, range.start_character,
- range.end_line, range.end_character, 'right');
- }, this);
- },
-
- _rerenderByLines: function(startLine, endLine, opt_side) {
- this.async(function() {
- this.diffBuilder.renderLineRange(startLine, endLine, opt_side);
- }, 1);
- },
});
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 04e47b1..45adb37 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -167,42 +167,17 @@
});
});
- test('renders lines in comment range on thread discard', function(done) {
- element.fire('thread-discard', {
- lastComment: {
- range: {
- start_line: 10,
- end_line: 24,
- },
- },
- });
- flush(function() {
- assert.isTrue(
- builder.renderLineRange.calledWithExactly(10, 24, 'other-side'));
- done();
- });
- });
-
- test('renders lines in comment range on comment discard', function(done) {
- element.fire('comment-discard', {
- comment: {
- range: {
- start_line: 10,
- end_line: 24,
- },
- },
- });
- flush(function() {
- assert.isTrue(
- builder.renderLineRange.calledWithExactly(10, 24, 'other-side'));
- done();
- });
- });
-
test('comment-mouse-over from line comments is ignored', function() {
- sandbox.stub(element, '_applyRangedHighlight');
+ sandbox.stub(element, 'set');
element.fire('comment-mouse-over', {comment: {}});
- assert.isFalse(element._applyRangedHighlight.called);
+ assert.isFalse(element.set.called);
+ });
+
+ test('comment-mouse-over from ranged comment causes set', function() {
+ sandbox.stub(element, 'set');
+ sandbox.stub(element, '_indexOfComment').returns(0);
+ element.fire('comment-mouse-over', {comment: {range:{}}});
+ assert.isTrue(element.set.called);
});
test('comment-mouse-out from line comments is ignored', function() {
@@ -210,56 +185,7 @@
assert.isFalse(builder.getContentsByLineRange.called);
});
- test('on comment-mouse-out highlight classes are removed', function() {
- var testEl = fixture('highlighted');
- builder.getContentsByLineRange.returns([testEl]);
- element.fire('comment-mouse-out', {
- comment: {
- range: {
- start_line: 3,
- start_character: 14,
- end_line: 10,
- end_character: 24,
- }
- }});
- assert.isTrue(builder.getContentsByLineRange.calledWithExactly(
- 3, 10, 'other-side'));
- assert.equal(0, testEl.querySelectorAll('.rangeHighlight').length);
- assert.equal(2, testEl.querySelectorAll('.range').length);
- });
-
- test('on comment-mouse-over range is highlighted', function() {
- sandbox.stub(element, '_applyRangedHighlight');
- element.fire('comment-mouse-over', {
- comment: {
- range: {
- start_line: 3,
- start_character: 14,
- end_line: 10,
- end_character: 24,
- },
- }});
- assert.isTrue(element._applyRangedHighlight.calledWithExactly(
- 'rangeHighlight', 3, 14, 10, 24, 'other-side'));
- });
-
- test('on create-comment range is highlighted', function() {
- sandbox.stub(element, '_applyRangedHighlight');
- element.fire('create-comment', {
- range: {
- startLine: 3,
- startChar: 14,
- endLine: 10,
- endChar: 24,
- },
- side: 'some-side',
- });
- assert.isTrue(element._applyRangedHighlight.calledWithExactly(
- 'range', 3, 14, 10, 24, 'some-side'));
- });
-
test('on create-comment action box is removed', function() {
- sandbox.stub(element, '_applyRangedHighlight');
sandbox.stub(element, '_removeActionBox');
element.fire('create-comment', {
comment: {
@@ -270,290 +196,6 @@
});
});
- test('apply multiline highlight', function() {
- var diff = element.querySelector('#diffTable');
- var startContent = diff.querySelector(
- '.left.lineNum[data-value="138"] ~ .content .contentText');
- var betweenContent = diff.querySelector(
- '.left.lineNum[data-value="140"] ~ .content .contentText');
- var endContent = diff.querySelector(
- '.left.lineNum[data-value="141"] ~ .content .contentText');
- var builder = {
- getContentByLine: sandbox.stub().returns({}),
- getContentsByLineRange: sandbox.stub().returns([betweenContent]),
- getLineElByChild: sandbox.stub().returns(
- {getAttribute: sandbox.stub()}),
- };
- element._cachedDiffBuilder = builder;
- builder.getContentByLine.withArgs(138, 'left').returns(
- startContent);
- builder.getContentByLine.withArgs(141, 'left').returns(
- endContent);
- element._applyRangedHighlight('some', 138, 4, 141, 28, 'left');
- assert.instanceOf(startContent.childNodes[0], Text);
- assert.equal(startContent.childNodes[0].textContent, '[14]');
- assert.instanceOf(startContent.childNodes[1], Element);
- assert.equal(startContent.childNodes[1].textContent,
- ' Nam cum ad me in Cumanum salutandi causa uterque venisset,');
- assert.equal(startContent.childNodes[1].tagName, 'HL');
- assert.equal(startContent.childNodes[1].className, 'some');
-
- assert.instanceOf(betweenContent.firstChild, Element);
- assert.equal(betweenContent.firstChild.tagName, 'HL');
- assert.equal(betweenContent.firstChild.className, 'some');
- assert.equal(betweenContent.childNodes.length, 1);
- assert.equal(betweenContent.firstChild.childNodes.length, 5);
- assert.equal(betweenContent.firstChild.textContent,
- 'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
- 'quid sit, quod Epicurum');
-
- assert.isNull(diff.querySelector('.right + .content .some'),
- 'Highlight should be applied only to the left side content.');
-
- assert.instanceOf(endContent.childNodes[0], Element);
- assert.equal(endContent.childNodes[0].textContent,
- 'nam et\tcomplectitur\tverbis, ');
- assert.equal(endContent.childNodes[0].tagName, 'HL');
- assert.equal(endContent.childNodes[0].className, 'some');
- assert.instanceOf(endContent.childNodes[1], Text);
- assert.equal(endContent.childNodes[1].textContent,
- 'quod vult, et dicit plane, quod intellegam;');
- var endHl = endContent.querySelector('hl.some');
- assert.equal(endHl.childNodes.length, 5);
- var tabs = endHl.querySelectorAll('span.tab');
- assert.equal(tabs.length, 2);
- assert.equal(tabs[0].previousSibling.textContent, 'nam et');
- assert.equal(tabs[1].previousSibling.textContent, 'complectitur');
- assert.equal(tabs[1].nextSibling.textContent, 'verbis, ');
- });
-
- test('multiline highlight w/ start at end of 1st line', function() {
- var diff = element.querySelector('#diffTable');
- var startContent =
- diff.querySelector('.left.lineNum[data-value="138"] ~ .content');
- var betweenContent =
- diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
- var endContent =
- diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
- var commentThread =
- diff.querySelector('gr-diff-comment-thread');
- var builder = {
- getCommentThreadByContentEl: sandbox.stub().returns(commentThread),
- getContentByLine: sandbox.stub().returns({}),
- getContentsByLineRange: sandbox.stub().returns([betweenContent]),
- getLineElByChild: sandbox.stub().returns(
- {getAttribute: sandbox.stub()}),
- };
- element._cachedDiffBuilder = builder;
- builder.getContentByLine.withArgs(138, 'left').returns(
- startContent);
- builder.getContentByLine.withArgs(141, 'left').returns(
- endContent);
-
- var expectedStartContentNodes = startContent.childNodes.length;
-
- // The following should not cause an error.
- element._applyRangedHighlight(
- 'some', 138, startContent.textContent.length, 141, 28, 'left');
-
- assert.equal(startContent.childNodes.length, expectedStartContentNodes,
- 'Should not add a highlight to the start content');
- });
-
- suite('single line ranges', function() {
- var diff;
- var contentText;
- var commentThread;
- var builder;
-
- setup(function() {
- diff = element.querySelector('#diffTable');
- var contentTd = diff.querySelector(
- '.left.lineNum[data-value="140"] ~ .content');
- contentText = contentTd.querySelector('.contentText');
- commentThread = diff.querySelector('gr-diff-comment-thread');
- builder = {
- getCommentThreadByContentEl: sandbox.stub().returns(commentThread),
- getContentByLine: sandbox.stub().returns(contentText),
- getContentsByLineRange: sandbox.stub().returns([]),
- getLineElByChild: sandbox.stub().returns(
- {getAttribute: sandbox.stub()}),
- };
- element._cachedDiffBuilder = builder;
- });
-
- test('whole line range', function() {
- element._applyRangedHighlight('some', 140, 0, 140, 81, 'left');
- assert.equal(contentText.childNodes.length, 1);
- assert.instanceOf(contentText.firstChild, Element);
- assert.equal(contentText.firstChild.tagName, 'HL');
- assert.equal(contentText.firstChild.className, 'some');
- assert.equal(contentText.firstChild.childNodes.length, 5);
- assert.equal(contentText.firstChild.textContent,
- 'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
- 'quid sit, quod Epicurum');
- var tabs = contentText.querySelectorAll('span.tab');
- assert.equal(tabs.length, 2);
- assert.strictEqual(tabs[1].previousSibling, tabs[0].nextSibling);
- assert.equal(tabs[0].previousSibling.textContent,
- 'na💢ti te, inquit, sumus aliquando otiosum, certe a ');
- assert.equal(tabs[1].previousSibling.textContent,
- 'udiam, quid sit, ');
- assert.equal(tabs[1].nextSibling.textContent, 'quod Epicurum');
- });
-
- test('merging multiple other hls', function() {
- element._applyRangedHighlight('some', 140, 1, 140, 80, 'left');
- assert.instanceOf(contentText.firstChild, Text);
- assert.equal(contentText.childNodes.length, 3);
- var hl = contentText.querySelector('hl.some');
- assert.strictEqual(contentText.firstChild, hl.previousSibling);
- assert.equal(hl.childNodes.length, 5);
- assert.equal(contentText.querySelectorAll('span.tab').length, 2);
- assert.equal(hl.textContent,
- 'a💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
- 'quid sit, quod Epicuru');
- });
-
- test('hl inside Text node', function() {
- // Before: na💢ti
- // After: n<hl class="some">a💢t</hl>i
- element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
- });
-
- test('hl ending over different hl', function() {
- // Before: na💢ti <hl>te, inquit</hl>,
- // After: na💢<hl class="some">ti te</hl><hl class="foo">, inquit</hl>,
- element._applyRangedHighlight('some', 140, 3, 140, 8, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">ti te</hl>');
- assert.equal(hl.nextSibling.outerHTML,
- '<hl class="foo">, inquit</hl>');
- });
-
- test('hl starting inside different hl', function() {
- // Before: na💢ti <hl>te, inquit</hl>, sumus
- // After: na💢ti <hl class="foo">te, in</hl><hl class="some">quit, ...
- element._applyRangedHighlight('some', 140, 12, 140, 21, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.textContent, 'quit, sum');
- assert.equal(
- hl.previousSibling.outerHTML, '<hl class="foo">te, in</hl>');
- });
-
- test('hl inside different hl', function() {
- // Before: na💢ti <hl class="foo">te, inquit</hl>, sumus
- // After: <hl class="foo">t</hl><hl="some">e, i</hl><hl class="foo">n..
- element._applyRangedHighlight('some', 140, 7, 140, 12, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.textContent, 'e, in');
- assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">t</hl>');
- assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">quit</hl>');
- });
-
- test('hl starts and ends in different hls', function() {
- element._applyRangedHighlight('some', 140, 8, 140, 27, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.textContent, ', inquit, sumus ali');
- assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">te</hl>');
- assert.equal(hl.nextSibling.outerHTML, '<hl class="bar">quando</hl>');
- });
-
- test('hl over different hl', function() {
- element._applyRangedHighlight('some', 140, 2, 140, 21, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">💢ti te, inquit, sum</hl>');
- assert.notOk(contentText.querySelector('.foo'));
- });
-
- test('hl starting and ending in boundaries', function() {
- element._applyRangedHighlight('some', 140, 6, 140, 33, 'left');
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.textContent, 'te, inquit, sumus aliquando');
- assert.notOk(contentText.querySelector('.bar'));
- });
-
- test('overlapping hls', function() {
- element._applyRangedHighlight('some', 140, 1, 140, 3, 'left');
- element._applyRangedHighlight('some', 140, 2, 140, 4, 'left');
- assert.equal(contentText.querySelectorAll('hl.some').length, 1);
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
- });
-
- test('growing hl right including another hl', function() {
- element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
- element._applyRangedHighlight('some', 140, 3, 140, 10, 'left');
- assert.equal(contentText.querySelectorAll('hl.some').length, 1);
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">a💢ti te, </hl>');
- assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">inquit</hl>');
- });
-
- test('growing hl left to start of line', function() {
- element._applyRangedHighlight('some', 140, 2, 140, 5, 'left');
- element._applyRangedHighlight('some', 140, 0, 140, 3, 'left');
- assert.equal(contentText.querySelectorAll('hl.some').length, 1);
- var hl = contentText.querySelector('hl.some');
- assert.equal(hl.outerHTML, '<hl class="some">na💢ti</hl>');
- assert.strictEqual(contentText.firstChild, hl);
- });
-
- test('splitting hl containing a tab', function() {
- element._applyRangedHighlight('some', 140, 63, 140, 72, 'left');
- assert.equal(contentText.querySelector('hl.some').textContent,
- 'sit, quod');
- element._applyRangedHighlight('another', 140, 66, 140, 81, 'left');
- assert.equal(contentText.querySelector('hl.another').textContent,
- ', quod Epicurum');
- });
- });
-
- test('_applyAllHighlights', function() {
- element.comments = {
- left: [
- {
- range: {
- start_line: 3,
- start_character: 14,
- end_line: 10,
- end_character: 24,
- },
- },
- ],
- right: [
- {
- range: {
- start_line: 320,
- start_character: 200,
- end_line: 1024,
- end_character: 768,
- },
- },
- ],
- };
- sandbox.stub(element, '_applyRangedHighlight');
- element._applyAllHighlights();
- sinon.assert.calledWith(element._applyRangedHighlight,
- 'range', 3, 14, 10, 24, 'left');
- sinon.assert.calledWith(element._applyRangedHighlight,
- 'range', 320, 200, 1024, 768, 'right');
- });
-
- test('apply comment ranges on render', function() {
- sandbox.stub(element, '_applyAllHighlights');
- element.fire('render');
- assert.isTrue(element._applyAllHighlights.called);
- });
-
- test('apply comment ranges on context expand', function() {
- sandbox.stub(element, '_applyAllHighlights');
- element.fire('show-context');
- assert.isTrue(element._applyAllHighlights.called);
- });
-
suite('selection', function() {
var diff;
var builder;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 3055b511..2e7fd56 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -147,9 +147,10 @@
<gr-diff-highlight
id="highlights"
logged-in="[[_loggedIn]]"
- comments="[[_comments]]">
+ comments="{{_comments}}">
<gr-diff-builder
id="diffBuilder"
+ comments="[[_comments]]"
view-mode="[[viewMode]]"
is-image-diff="[[isImageDiff]]"
base-image="[[_baseImage]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
new file mode 100644
index 0000000..53ce0e9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
@@ -0,0 +1,20 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<dom-module id="gr-ranged-comment-layer">
+ <script src="gr-ranged-comment-layer.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
new file mode 100644
index 0000000..b1c6bf7
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -0,0 +1,186 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ var HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
+ var SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;
+
+ var RANGE_HIGHLIGHT = 'range';
+ var HOVER_HIGHLIGHT = 'rangeHighlight';
+
+ Polymer({
+ is: 'gr-ranged-comment-layer',
+
+ properties: {
+ comments: Object,
+ _listeners: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _commentMap: {
+ type: Object,
+ value: function() { return {left: [], right: []}; },
+ }
+ },
+
+ observers: [
+ '_handleCommentChange(comments.*)',
+ ],
+
+ /**
+ * Layer method to add annotations to a line.
+ * @param {HTMLElement} el The DIV.contentText element to apply the
+ * annotation to.
+ * @param {GrDiffLine} line The line object.
+ * @param {Object} GrAnnotation The annotation library.
+ */
+ annotate: function(el, line, GrAnnotation) {
+ var ranges = [];
+ if (line.type === GrDiffLine.Type.REMOVE || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'right')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'left'));
+ }
+ if (line.type === GrDiffLine.Type.ADD || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'left')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'right'));
+ }
+
+ ranges.forEach(function(range) {
+ GrAnnotation.annotateElement(el, range.start,
+ range.end - range.start,
+ range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
+ });
+ },
+
+ /**
+ * Register a listener for layer updates.
+ * @param {Function<Number, Number, String>} fn The update handler function.
+ * Should accept as arguments the line numbers for the start and end of
+ * the update and the side as a string.
+ */
+ addListener: function(fn) {
+ this._listeners.push(fn);
+ },
+
+ /**
+ * Notify Layer listeners of changes to annotations.
+ * @param {Number} start The line where the update starts.
+ * @param {Number} end The line where the update ends.
+ * @param {String} side The side of the update. ('left' or 'right')
+ */
+ _notifyUpdateRange: function(start, end, side) {
+ this._listeners.forEach(function(listener) {
+ listener(start, end, side);
+ });
+ },
+
+ /**
+ * Handle change in the comments by updating the comment maps and by
+ * emitting appropriate update notifications.
+ * @param {Object} record The change record.
+ */
+ _handleCommentChange: function(record) {
+ if (!record.path) { return; }
+
+ // If the entire set of comments was changed.
+ if (record.path === 'comments') {
+ this._commentMap.left = this._computeCommentMap(this.comments.left);
+ this._commentMap.right = this._computeCommentMap(this.comments.right);
+ return;
+ }
+
+ // If the change only changed the `hovering` property of a comment.
+ var match = record.path.match(HOVER_PATH_PATTERN);
+ if (match) {
+ var side = match[1];
+ var index = match[2];
+ var comment = this.comments[side][index];
+ if (comment && comment.range) {
+ this._commentMap[side] = this._computeCommentMap(this.comments[side]);
+ this._notifyUpdateRange(
+ comment.range.start_line, comment.range.end_line, side);
+ }
+ return;
+ }
+
+ // If comments were spliced in or out.
+ match = record.path.match(SPLICE_PATH_PATTERN);
+ if (match) {
+ var side = match[1];
+ this._commentMap[side] = this._computeCommentMap(this.comments[side]);
+ this._handleCommentSplice(record.value, side);
+ }
+ },
+
+ /**
+ * Take a list of comments and return a sparse list mapping line numbers to
+ * partial ranges. Uses an end-character-index of -1 to indicate the end of
+ * the line.
+ * @param {Array<Object>} commentList The list of comments.
+ * @return {Object} The sparse list.
+ */
+ _computeCommentMap: function(commentList) {
+ var result = {};
+ commentList.forEach(function(comment) {
+ if (!comment.range) { return; }
+ var range = comment.range;
+ for (var line = range.start_line; line <= range.end_line; line++) {
+ if (!result[line]) { result[line] = []; }
+ result[line].push({
+ comment: comment,
+ start: line === range.start_line ? range.start_character : 0,
+ end: line === range.end_line ? range.end_character : -1,
+ });
+ }
+ });
+ return result;
+ },
+
+ /**
+ * Translate a splice record into range update notifications.
+ */
+ _handleCommentSplice: function(record, side) {
+ if (!record || !record.indexSplices) { return; }
+ record.indexSplices.forEach(function(splice) {
+ var ranges = splice.removed.length ?
+ splice.removed.map(function(c) { return c.range; }) :
+ [splice.object[splice.index].range];
+ ranges.forEach(function(range) {
+ if (!range) { return; }
+ this._notifyUpdateRange(range.start_line, range.end_line, side);
+ }.bind(this));
+ }.bind(this));
+ },
+
+ _getRangesForLine: function(line, side) {
+ var lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
+ var ranges = this.get(['_commentMap', side, lineNum]) || [];
+ return ranges
+ .map(function(range) {
+ return {
+ start: range.start,
+ end: range.end === -1 ? line.text.length : range.end,
+ hovering: !!range.comment.__hovering,
+ };
+ })
+ .sort(function(a, b) {
+ // Sort the ranges so that hovering highlights are on top.
+ return a.hovering && !b.hovering ? 1 : 0;
+ });
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
new file mode 100644
index 0000000..22ad439
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -0,0 +1,322 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-ranged-comment-layer</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../gr-diff/gr-diff-line.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-ranged-comment-layer.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-ranged-comment-layer></gr-ranged-comment-layer>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-ranged-comment-layer', function() {
+ var element;
+ var sandbox;
+
+ setup(function() {
+ var initialComments = {
+ left: [
+ {
+ id: '12345',
+ line: 39,
+ message: 'range comment',
+ range: {
+ end_character: 9,
+ end_line: 39,
+ start_character: 6,
+ start_line: 36,
+ },
+ }, {
+ id: '23456',
+ line: 100,
+ message: 'non range comment',
+ },
+ ],
+ right: [
+ {
+ id: '34567',
+ line: 10,
+ message: 'range comment',
+ range: {
+ end_character: 22,
+ end_line: 12,
+ start_character: 10,
+ start_line: 10,
+ },
+ }, {
+ id: '45678',
+ line: 100,
+ message: 'single line range comment',
+ range: {
+ end_character: 15,
+ end_line: 100,
+ start_character: 5,
+ start_line: 100,
+ },
+ },
+ ],
+ };
+
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.comments = initialComments;
+ });
+
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ suite('annotate', function() {
+ var GrAnnotation;
+ var el;
+ var line;
+
+ setup(function() {
+ GrAnnotation = {annotateElement: sinon.stub()};
+ el = document.createElement('div');
+ el.setAttribute('data-side', 'left');
+ line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
+ });
+
+ test('type=Remove no-comment', function() {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 40;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isFalse(GrAnnotation.annotateElement.called);
+ });
+
+ test('type=Remove has-comment', function() {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ var expectedStart = 6;
+ var expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isTrue(GrAnnotation.annotateElement.called);
+ var lastCall = GrAnnotation.annotateElement.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'range');
+ });
+
+ test('type=Remove has-comment hovering', function() {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ element.set(['comments', 'left', 0, '__hovering'], true);
+
+ var expectedStart = 6;
+ var expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isTrue(GrAnnotation.annotateElement.called);
+ var lastCall = GrAnnotation.annotateElement.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'rangeHighlight');
+ });
+
+ test('type=Both has-comment', function() {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
+
+ var expectedStart = 6;
+ var expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isTrue(GrAnnotation.annotateElement.called);
+ var lastCall = GrAnnotation.annotateElement.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'range');
+ });
+
+ test('type=Both has-comment off side', function() {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
+ el.setAttribute('data-side', 'right');
+
+ var expectedStart = 6;
+ var expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isFalse(GrAnnotation.annotateElement.called);
+ });
+
+ test('type=Add has-comment', function() {
+ line.type = GrDiffLine.Type.ADD;
+ line.afterNumber = 12;
+ el.setAttribute('data-side', 'right');
+
+ var expectedStart = 0;
+ var expectedLength = 22;
+
+ element.annotate(el, line, GrAnnotation);
+
+ assert.isTrue(GrAnnotation.annotateElement.called);
+ var lastCall = GrAnnotation.annotateElement.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'range');
+ });
+ });
+
+ test('_handleCommentChange overwrite', function() {
+ var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+ var mapSpy = sandbox.spy(element, '_computeCommentMap');
+
+ element.set('comments', {left: [], right: []});
+
+ assert.isTrue(handlerSpy.called);
+ assert.equal(mapSpy.callCount, 2);
+
+ assert.equal(Object.keys(element._commentMap.left).length, 0);
+ assert.equal(Object.keys(element._commentMap.right).length, 0);
+ });
+
+ test('_handleCommentChange hovering', function() {
+ var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+ var mapSpy = sandbox.spy(element, '_computeCommentMap');
+ var notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+
+ element.set(['comments', 'right', 0, '__hovering'], true);
+
+ assert.isTrue(handlerSpy.called);
+ assert.isTrue(mapSpy.called);
+
+ assert.isTrue(notifyStub.called);
+ var lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 10);
+ assert.equal(lastCall.args[1], 12);
+ assert.equal(lastCall.args[2], 'right');
+ });
+
+ test('_handleCommentChange splice out', function() {
+ var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+ var mapSpy = sandbox.spy(element, '_computeCommentMap');
+ var notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+
+ element.splice('comments.right', 0, 1);
+
+ assert.isTrue(handlerSpy.called);
+ assert.isTrue(mapSpy.called);
+
+ assert.isTrue(notifyStub.called);
+ var lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 10);
+ assert.equal(lastCall.args[1], 12);
+ assert.equal(lastCall.args[2], 'right');
+ });
+
+ test('_handleCommentChange splice in', function() {
+ var handlerSpy = sandbox.spy(element, '_handleCommentChange');
+ var mapSpy = sandbox.spy(element, '_computeCommentMap');
+ var notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+
+ element.splice('comments.left', element.comments.left.length, 0, {
+ id: '56123',
+ line: 250,
+ message: 'new range comment',
+ range: {
+ end_character: 15,
+ end_line: 275,
+ start_character: 5,
+ start_line: 250,
+ },
+ });
+
+ assert.isTrue(handlerSpy.called);
+ assert.isTrue(mapSpy.called);
+
+ assert.isTrue(notifyStub.called);
+ var lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 250);
+ assert.equal(lastCall.args[1], 275);
+ assert.equal(lastCall.args[2], 'left');
+ });
+
+ test('_computeCommentMap creates maps correctly', function() {
+ // There is only one ranged comment on the left, but it spans ll.36-39.
+ var leftKeys = [];
+ for (var i = 36; i <= 39; i++) { leftKeys.push('' + i); }
+ assert.deepEqual(Object.keys(element._commentMap.left).sort(),
+ leftKeys.sort());
+
+ assert.equal(element._commentMap.left[36].length, 1);
+ assert.equal(element._commentMap.left[36][0].start, 6);
+ assert.equal(element._commentMap.left[36][0].end, -1);
+
+ assert.equal(element._commentMap.left[37].length, 1);
+ assert.equal(element._commentMap.left[37][0].start, 0);
+ assert.equal(element._commentMap.left[37][0].end, -1);
+
+ assert.equal(element._commentMap.left[38].length, 1);
+ assert.equal(element._commentMap.left[38][0].start, 0);
+ assert.equal(element._commentMap.left[38][0].end, -1);
+
+ assert.equal(element._commentMap.left[39].length, 1);
+ assert.equal(element._commentMap.left[39][0].start, 0);
+ assert.equal(element._commentMap.left[39][0].end, 9);
+
+ // The right has two ranged comments, one spanning ll.10-12 and the other
+ // on line 100.
+ var rightKeys = [];
+ for (i = 10; i <= 12; i++) { rightKeys.push('' + i); }
+ rightKeys.push('100');
+ assert.deepEqual(Object.keys(element._commentMap.right).sort(),
+ rightKeys.sort());
+
+ assert.equal(element._commentMap.right[10].length, 1);
+ assert.equal(element._commentMap.right[10][0].start, 10);
+ assert.equal(element._commentMap.right[10][0].end, -1);
+
+ assert.equal(element._commentMap.right[11].length, 1);
+ assert.equal(element._commentMap.right[11][0].start, 0);
+ assert.equal(element._commentMap.right[11][0].end, -1);
+
+ assert.equal(element._commentMap.right[12].length, 1);
+ assert.equal(element._commentMap.right[12][0].start, 0);
+ assert.equal(element._commentMap.right[12][0].end, 22);
+
+ assert.equal(element._commentMap.right[100].length, 1);
+ assert.equal(element._commentMap.right[100][0].start, 5);
+ assert.equal(element._commentMap.right[100][0].end, 15);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index f5d7ee7..45bf8fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -38,7 +38,7 @@
_handleRemoveTap: function(e) {
e.preventDefault();
- this.fire('remove', {account: this.account}, {bubbles: false});
+ this.fire('remove', {account: this.account});
},
_getHasAvatars: function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index b7f9715..f136907 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -23,6 +23,9 @@
:host {
display: inline;
}
+ :host::after {
+ content: var(--account-label-suffix);
+ }
gr-avatar {
height: 1.3em;
width: 1.3em;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index c7438df..6fd57b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -23,6 +23,11 @@
input {
font-size: 1em;
}
+ input.borderless,
+ input.borderless:focus {
+ border: none;
+ outline: none;
+ }
#suggestions {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
@@ -42,6 +47,7 @@
</style>
<input
id="input"
+ class$="[[_computeClass(borderless)]]"
is="iron-input"
disabled$="[[disabled]]"
bind-value="{{text}}"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 6ab5fa2..23fa800 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -59,6 +59,7 @@
value: 1,
},
+ borderless: Boolean,
disabled: Boolean,
text: {
@@ -96,6 +97,10 @@
this.unlisten(document.body, 'click', '_handleBodyClick');
},
+ get focusStart() {
+ return this.$.input;
+ },
+
focus: function() {
this.$.input.focus();
},
@@ -136,6 +141,10 @@
return !suggestions.length;
},
+ _computeClass: function(borderless) {
+ return borderless ? 'borderless' : '';
+ },
+
_getSuggestionElems: function() {
Polymer.dom.flush();
return this.$.suggestions.querySelectorAll('li');
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 755dc93..c044e4f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -208,5 +208,11 @@
assert.isTrue(queryStub.called);
});
+
+ test('_computeClass respects border property', function() {
+ assert.equal(element._computeClass(), '');
+ assert.equal(element._computeClass(false), '');
+ assert.equal(element._computeClass(true), 'borderless');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-diff-cursor/mock-diff-response_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index feb4f49..a7337f4 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -27,6 +27,8 @@
[
'change-list/gr-change-list-item/gr-change-list-item_test.html',
'change-list/gr-change-list/gr-change-list_test.html',
+ 'change/gr-account-entry/gr-account-entry_test.html',
+ 'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html',
'change/gr-change-metadata/gr-change-metadata_test.html',
'change/gr-change-view/gr-change-view_test.html',
@@ -56,6 +58,7 @@
'diff/gr-diff/gr-diff-group_test.html',
'diff/gr-diff/gr-diff_test.html',
'diff/gr-patch-range-select/gr-patch-range-select_test.html',
+ 'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
'settings/gr-account-info/gr-account-info_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',