Merge "Check for overlapping pattern matches"
diff --git a/BUILD b/BUILD
index a6d5067..c914fb7 100644
--- a/BUILD
+++ b/BUILD
@@ -1,3 +1,4 @@
+package(default_visibility = ['//visibility:public'])
load('//tools/bzl:pkg_war.bzl', 'pkg_war')
genrule(
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 462c226..564d298 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3419,6 +3419,67 @@
+
Default is 1.
+[[receiveemail]]
+=== Section receiveemail
+
+[[receiveemail.protocol]]receiveemail.protocol::
++
+Specifies the protocol used for receiving emails. Valid options are
+'POP3', 'IMAP' and 'NONE'. Note that Gerrit will automatically switch between
+POP3 and POP3s as well as IMAP and IMAPS depending on the specified
+link:#receiveemail.encryption[encryption].
++
+Defaults to 'NONE' which means that receiving emails is disabled.
+
+[[receiveemail.host]]receiveemail.host::
++
+The hostname of the mailserver. Example: 'imap.gmail.com'.
++
+Defaults to an empty string which means that receiving emails is disabled.
+
+[[receiveemail.port]]receiveemail.port::
++
+The port the email server exposes for receving emails.
++
+Defaults to the industry standard for a given protocol and encryption:
+POP3: 110; POP3S: 995; IMAP: 143; IMAPS: 995.
+
+[[receiveemail.username]]receiveemail.username::
++
+Username used for authenticating with the email server.
++
+Defaults to an empty string.
+
+[[receiveemail.password]]receiveemail.password::
++
+Password used for authenticating with the email server.
++
+Defaults to an empty string.
+
+[[receiveemail.encryption]]receiveemail.encryption::
++
+Encryption standard used for transport layer security between Gerrit and the
+email server. Possible values include 'NONE', 'SSL' and 'TLS'.
++
+Defaults to 'NONE'.
+
+[[receiveemail.fetchInterval]]receiveemail.fetchInterval::
++
+Time between two consecutive fetches from the email server. Communication with
+the email server is not kept alive. Examples: 60s, 10m, 1h.
++
+Defaults to 60 seconds.
+
+[[receiveemail.enableImapIdle]]receiveemail.enableImapIdle::
++
+If the IMAP protocol is used for retrieving emails, IMAPv4 IDLE can be used to
+keep the connection with the email server alive and receive a push when a new
+email is delivered to the inbox. In this case, Gerrit will process the email
+immediately and will not have a fetch delay.
+
++
+Defaults to false.
+
[[sendemail]]
=== Section sendemail
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 97124cb..77370df 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -2,7 +2,6 @@
Bazel build is experimental. Major missing parts:
-* PolyGerrit
* License tracking
* Version stamping
* Custom plugins
diff --git a/WORKSPACE b/WORKSPACE
index a3db958..cd19c74 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -156,7 +156,7 @@
)
maven_jar(
- name = 'ewah',
+ name = 'javaewah',
artifact = 'com.googlecode.javaewah:JavaEWAH:0.7.9',
sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a',
)
@@ -381,6 +381,36 @@
sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54',
)
+GREENMAIL_VERS = '1.5.2'
+
+maven_jar(
+ name = 'greenmail',
+ artifact = 'com.icegreen:greenmail:' + GREENMAIL_VERS,
+ sha1 = '6b4862a09f8642da58c109117b24ccc19a4a6d39',
+)
+
+MAIL_VERS = '1.5.6'
+
+maven_jar(
+ name = 'mail',
+ artifact = 'com.sun.mail:javax.mail:' + MAIL_VERS,
+ sha1 = 'ab5daef2f881c42c8e280cbe918ec4d7fdfd7efe',
+)
+
+MIME4J_VERS = '0.8.0'
+
+maven_jar(
+ name = 'mime4j_core',
+ artifact = 'org.apache.james:apache-mime4j-core:' + MIME4J_VERS,
+ sha1 = 'd54f45fca44a2f210569656b4ca3574b42911c95',
+)
+
+maven_jar(
+ name = 'mime4j_dom',
+ artifact = 'org.apache.james:apache-mime4j-dom:' + MIME4J_VERS,
+ sha1 = '6720c93d14225c3e12c4a69768a0370c80e376a3',
+)
+
OW2_VERS = '5.1'
maven_jar(
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index 89f8ea3..dca71e6 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -56,10 +56,12 @@
'//lib:truth',
],
provided_deps = PROVIDED + [
+ '//lib/greenmail:greenmail',
'//lib:gwtorm',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
+ '//lib/mail:mail',
],
visibility = ['PUBLIC'],
)
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index ec79be8..d01534a 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -49,10 +49,12 @@
'//lib:truth',
],
deps = PROVIDED + [ # We want these deps to be exported_deps
+ '//lib/greenmail:greenmail',
'//lib:gwtorm',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
+ '//lib/mail:mail',
],
visibility = ['//visibility:public'],
)
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index d5d0b0d..3f7e04f 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -29,11 +29,13 @@
'//lib/bouncycastle:bcpg',
'//lib/bouncycastle:bcprov',
+ '//lib/greenmail:greenmail',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
'//lib/log:api',
'//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/mail:mail',
'//lib/mina:sshd',
],
visibility = [
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 479e34b..de907ca 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -28,6 +28,7 @@
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.toList;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
@@ -54,6 +55,7 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -1002,6 +1004,65 @@
}
@Test
+ public void implicitlyCcOnNonVotingReview() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(new ReviewInput());
+
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ // If we're not reading from NoteDb, then the CCed user will be returned
+ // in the REVIEWER state.
+ ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER;
+ assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId)
+ .collect(toList())).containsExactly(user.id.get());
+ }
+
+ @Test
+ public void implicitlyAddReviewerOnVotingReview() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.recommend().message("LGTM"));
+
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId)
+ .collect(toList())).containsExactly(user.id.get());
+
+ // Further test: remove the vote, then comment again. The user should be
+ // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb.
+ setApiUser(admin);
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.getId().toString())
+ .remove();
+ c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(c.reviewers.values()).isEmpty();
+
+ setApiUser(user);
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(new ReviewInput().message("hi"));
+ c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER;
+ assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId)
+ .collect(toList())).containsExactly(user.id.get());
+ }
+
+ @Test
public void addReviewerToClosedChange() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
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 1ef6337..c9cb06f 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
@@ -277,12 +277,19 @@
assertThat(result.labels).isNull();
assertThat(result.reviewers).isNull();
- // Verify user is not added as reviewer.
+ // Verify user is added to CC list.
ChangeInfo c = gApi.changes()
.id(r.getChangeId())
.get();
- assertReviewers(c, REVIEWER);
- assertReviewers(c, CC);
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+ } else {
+ // If we aren't reading from NoteDb, the user will appear as a
+ // reviewer.
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ }
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK
new file mode 100644
index 0000000..5c3cd07
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ group = 'server_mail',
+ srcs = glob(['*IT.java']),
+ labels = ['server'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
new file mode 100644
index 0000000..77e6a0f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
@@ -0,0 +1,11 @@
+load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "server_mail",
+ labels = ["server"],
+ deps = [
+ "//lib/greenmail",
+ "//lib/mail",
+ ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java
new file mode 100644
index 0000000..66a7f15
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java
@@ -0,0 +1,104 @@
+// 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.server.mail;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.server.mail.receive.MailReceiver;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.inject.Inject;
+
+import com.icegreen.greenmail.junit.GreenMailRule;
+import com.icegreen.greenmail.user.GreenMailUser;
+import com.icegreen.greenmail.util.GreenMail;
+import com.icegreen.greenmail.util.GreenMailUtil;
+import com.icegreen.greenmail.util.ServerSetupTest;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.mail.internet.MimeMessage;
+
+@NoHttpd
+@RunWith(ConfigSuite.class)
+public class MailIT extends AbstractDaemonTest {
+ private final static String RECEIVEEMAIL = "receiveemail";
+ private final static String HOST = "localhost";
+ private final static String USERNAME = "user@domain.com";
+ private final static String PASSWORD = "password";
+
+ @Inject
+ private MailReceiver mailReceiver;
+
+ @Inject
+ private GreenMail greenMail;
+
+ @Rule
+ public final GreenMailRule mockPop3Server = new GreenMailRule(
+ ServerSetupTest.SMTP_POP3_IMAP);
+
+ @ConfigSuite.Default
+ public static Config pop3Config() {
+ Config cfg = new Config();
+ cfg.setString(RECEIVEEMAIL, null, "host", HOST);
+ cfg.setString(RECEIVEEMAIL, null, "port", "3110");
+ cfg.setString(RECEIVEEMAIL, null, "username", USERNAME);
+ cfg.setString(RECEIVEEMAIL, null, "password", PASSWORD);
+ cfg.setString(RECEIVEEMAIL, null, "protocol", "POP3");
+ cfg.setString(RECEIVEEMAIL, null, "fetchInterval", "99");
+ return cfg;
+ }
+
+ @ConfigSuite.Config
+ public static Config imapConfig() {
+ Config cfg = new Config();
+ cfg.setString(RECEIVEEMAIL, null, "host", HOST);
+ cfg.setString(RECEIVEEMAIL, null, "port", "3143");
+ cfg.setString(RECEIVEEMAIL, null, "username", USERNAME);
+ cfg.setString(RECEIVEEMAIL, null, "password", PASSWORD);
+ cfg.setString(RECEIVEEMAIL, null, "protocol", "IMAP");
+ cfg.setString(RECEIVEEMAIL, null, "fetchInterval", "99");
+ return cfg;
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ GreenMailUser user = mockPop3Server.setUser(USERNAME, USERNAME, PASSWORD);
+ user.deliver(createSimpleMessage());
+ assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1);
+ // Let Gerrit handle emails
+ mailReceiver.handleEmails();
+ // Check that the message is still present
+ assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1);
+ // Mark the message for deletion
+ mailReceiver.requestDeletion(
+ mockPop3Server.getReceivedMessages()[0].getMessageID());
+ // Let Gerrit handle emails
+ mailReceiver.handleEmails();
+ // Check that the message was deleted
+ assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(0);
+ }
+
+ private MimeMessage createSimpleMessage() {
+ return GreenMailUtil
+ .createTextEmail(USERNAME, "from@localhost.com", "subject",
+ "body",
+ greenMail.getImap().getServerSetup());
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
new file mode 100644
index 0000000..ba04366
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -0,0 +1,193 @@
+// 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.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.testutil.NoteDbMode;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class NoteDbPrimaryIT extends AbstractDaemonTest {
+ @Inject
+ private AllUsersName allUsers;
+
+ @Before
+ public void setUp() throws Exception {
+ assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.READ_WRITE);
+ db = ReviewDbUtil.unwrapDb(db);
+ }
+
+ @Test
+ public void updateChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ setNoteDbPrimary(id);
+
+ gApi.changes().id(id.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(id.get()).current().submit();
+
+ ChangeInfo info = gApi.changes().id(id.get()).get();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ ApprovalInfo approval =
+ Iterables.getOnlyElement(info.labels.get("Code-Review").all);
+ assertThat(approval._accountId).isEqualTo(admin.id.get());
+ assertThat(approval.value).isEqualTo(2);
+ assertThat(info.messages).hasSize(3);
+ assertThat(Iterables.getLast(info.messages).message)
+ .isEqualTo("Change has been successfully merged by " + admin.fullName);
+
+ ChangeNotes notes = notesFactory.create(db, project, id);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED);
+ assertThat(notes.getChange().getNoteDbState())
+ .isEqualTo(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+
+ // Writes weren't reflected in ReviewDb.
+ assertThat(db.changes().get(id).getStatus()).isEqualTo(Change.Status.NEW);
+ assertThat(db.patchSetApprovals().byChange(id)).isEmpty();
+ assertThat(db.changeMessages().byChange(id)).hasSize(1);
+ }
+
+ @Test
+ public void deleteDraftComment() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ setNoteDbPrimary(id);
+
+ DraftInput din = new DraftInput();
+ din.path = PushOneCommit.FILE_NAME;
+ din.line = 1;
+ din.message = "A comment";
+ gApi.changes().id(id.get()).current().createDraft(din);
+
+ CommentInfo di = Iterables.getOnlyElement(
+ gApi.changes().id(id.get()).current().drafts()
+ .get(PushOneCommit.FILE_NAME));
+ assertThat(di.message).isEqualTo(din.message);
+
+ assertThat(
+ db.patchComments().draftByChangeFileAuthor(id, din.path, admin.id))
+ .isEmpty();
+
+ gApi.changes().id(id.get()).current().draft(di.id).delete();
+ assertThat(gApi.changes().id(id.get()).current().drafts()).isEmpty();
+ }
+
+ @Test
+ public void deleteVote() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ setNoteDbPrimary(id);
+
+ gApi.changes().id(id.get()).current().review(ReviewInput.approve());
+ List<ApprovalInfo> approvals =
+ gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
+ assertThat(approvals).hasSize(1);
+ assertThat(approvals.get(0).value).isEqualTo(2);
+
+ gApi.changes().id(id.get()).reviewer(admin.id.toString())
+ .deleteVote("Code-Review");
+
+ approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
+ assertThat(approvals).hasSize(1);
+ assertThat(approvals.get(0).value).isEqualTo(0);
+ }
+
+ @Test
+ public void deleteVoteViaReview() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ setNoteDbPrimary(id);
+
+ gApi.changes().id(id.get()).current().review(ReviewInput.approve());
+ List<ApprovalInfo> approvals =
+ gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
+ assertThat(approvals).hasSize(1);
+ assertThat(approvals.get(0).value).isEqualTo(2);
+
+ gApi.changes().id(id.get()).current().review(ReviewInput.noScore());
+
+ approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all;
+ assertThat(approvals).hasSize(1);
+ assertThat(approvals.get(0).value).isEqualTo(0);
+ }
+
+ @Test
+ public void deleteReviewer() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ setNoteDbPrimary(id);
+
+ gApi.changes().id(id.get()).addReviewer(user.id.toString());
+ assertThat(getReviewers(id)).containsExactly(user.id);
+ gApi.changes().id(id.get()).reviewer(user.id.toString()).remove();
+ assertThat(getReviewers(id)).isEmpty();
+ }
+
+ private void setNoteDbPrimary(Change.Id id) throws Exception {
+ Change c = db.changes().get(id);
+ assertThat(c).named("change " + id).isNotNull();
+ NoteDbChangeState state = NoteDbChangeState.parse(c);
+ assertThat(state.getPrimaryStorage())
+ .named("storage of " + id)
+ .isEqualTo(REVIEW_DB);
+
+ try (Repository changeRepo = repoManager.openRepository(c.getProject());
+ Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ assertThat(
+ state.isUpToDate(
+ new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo)))
+ .named("change " + id + " up to date")
+ .isTrue();
+ }
+
+ c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+ db.changes().update(Collections.singleton(c));
+ }
+
+ private List<Account.Id> getReviewers(Change.Id id) throws Exception {
+ return gApi.changes().id(id.get()).get()
+ .reviewers.values().stream()
+ .flatMap(Collection::stream)
+ .map(a -> new Account.Id(a._accountId))
+ .collect(toList());
+ }
+}
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
index 160234f..cd1e7ec 100644
--- a/gerrit-acceptance-tests/tests.bzl
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -7,16 +7,12 @@
def acceptance_tests(
group,
- srcs,
- flaky = 0,
deps = [],
labels = [],
vm_args = ['-Xmx256m'],
**kwargs):
junit_tests(
name = group,
- srcs = srcs,
- flaky = flaky,
deps = deps + BOUNCYCASTLE + [
'//gerrit-acceptance-tests:lib',
],
@@ -24,6 +20,7 @@
'acceptance',
'slow',
],
+ size = "medium",
jvm_flags = vm_args,
**kwargs
)
diff --git a/gerrit-common/BUILD b/gerrit-common/BUILD
index 9add6e7..72bd71b 100644
--- a/gerrit-common/BUILD
+++ b/gerrit-common/BUILD
@@ -68,11 +68,13 @@
],
)
-junit_tests(
- name = 'auto_value_tests',
- srcs = AUTO_VALUE_TEST_SRCS,
- deps = [
- '//lib:truth',
- '//lib/auto:auto-value',
- ],
-)
+# TODO(davido): Enable this test again when this bazel bug is fixed:
+# https://github.com/bazelbuild/bazel/issues/2044
+#junit_tests(
+# name = 'auto_value_tests',
+# srcs = AUTO_VALUE_TEST_SRCS,
+# deps = [
+# '//lib:truth',
+# '//lib/auto:auto-value',
+# ],
+#)
diff --git a/gerrit-elasticsearch/BUILD b/gerrit-elasticsearch/BUILD
index bb1f0e2..b8cfaa1 100644
--- a/gerrit-elasticsearch/BUILD
+++ b/gerrit-elasticsearch/BUILD
@@ -35,6 +35,7 @@
name = 'elasticsearch_tests',
tags = ['elastic', 'flaky'],
srcs = glob(['src/test/java/**/*.java']),
+ size = "medium",
deps = [
':elasticsearch',
'//gerrit-extension-api:api',
diff --git a/gerrit-gpg/BUILD b/gerrit-gpg/BUILD
index 79f50b1..8cab45c 100644
--- a/gerrit-gpg/BUILD
+++ b/gerrit-gpg/BUILD
@@ -35,6 +35,7 @@
'//lib/bouncycastle:bcprov-without-neverlink',
],
visibility = ['//visibility:public'],
+ testonly = 1,
)
junit_tests(
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 7d62dd7..a0ab714 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -68,6 +68,7 @@
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.server.mail.send.SmtpEmailSender;
import com.google.gerrit.server.mime.MimeUtil2Module;
import com.google.gerrit.server.patch.DiffExecutorModule;
@@ -362,6 +363,7 @@
modules.add(new SearchingChangeCacheImpl.Module(slave));
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultCacheFactory.Module());
+ modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
if (emailModule != null) {
modules.add(emailModule);
} else {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
index afac097..b140ab1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -21,7 +21,7 @@
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.Section;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.mail.send.SmtpEmailSender.Encryption;
+import com.google.gerrit.server.mail.Encryption;
import com.google.inject.Inject;
import com.google.inject.Singleton;
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index a50df82..1e71009 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -70,6 +70,8 @@
'//lib/lucene:lucene-analyzers-common',
'//lib/lucene:lucene-core-and-backward-codecs',
'//lib/lucene:lucene-queryparser',
+ '//lib/mime4j:core',
+ '//lib/mime4j:dom',
'//lib/ow2:ow2-asm',
'//lib/ow2:ow2-asm-tree',
'//lib/ow2:ow2-asm-util',
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index 3874fc9..d410865 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -72,6 +72,8 @@
'//lib/lucene:lucene-analyzers-common',
'//lib/lucene:lucene-core-and-backward-codecs',
'//lib/lucene:lucene-queryparser',
+ '//lib/mime4j:core',
+ '//lib/mime4j:dom',
'//lib/ow2:ow2-asm',
'//lib/ow2:ow2-asm-tree',
'//lib/ow2:ow2-asm-util',
@@ -127,6 +129,7 @@
'//lib/powermock:powermock-module-junit4-common',
],
visibility = ['//visibility:public'],
+ testonly = 1,
)
PROLOG_TEST_CASE = [
@@ -151,6 +154,7 @@
'//lib/guice:guice',
'//lib/prolog:runtime',
],
+ testonly = 1,
)
junit_tests(
@@ -181,11 +185,13 @@
'//lib/antlr:java_runtime',
],
visibility = ['//visibility:public'],
+ testonly = 1,
)
junit_tests(
name = 'query_tests',
srcs = QUERY_TESTS,
+ size = "medium",
deps = TESTUTIL_DEPS + [
':testutil',
'//gerrit-antlr:query_exception',
@@ -204,6 +210,7 @@
exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
),
resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']),
+ size = "medium",
deps = TESTUTIL_DEPS + [
':testutil',
'//gerrit-antlr:query_exception',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
index d8ff413..d2c9a52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
@@ -26,7 +26,7 @@
* Typical usage in a try-with-resources block:
*
* <pre>
- * try (Timer.Context ctx = timer.start()) {
+ * try (Timer0.Context ctx = timer.start()) {
* }
* </pre>
*/
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index d1e0ae5..e65879b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -16,6 +16,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ComparisonChain;
@@ -42,6 +43,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -357,8 +359,11 @@
for (Comment c : comments) {
update.deleteComment(c);
}
- db.patchComments().delete(toPatchLineComments(update.getId(),
- PatchLineComment.Status.DRAFT, comments));
+ if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to delete non-existent entities.
+ db.patchComments().delete(toPatchLineComments(update.getId(),
+ PatchLineComment.Status.DRAFT, comments));
+ }
}
public void deleteAllDraftsFromAllUsers(Change.Id changeId)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
index 0dcf3bf..3ca11d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import static com.google.gerrit.server.notedb.PatchSetState.DRAFT;
import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED;
@@ -27,6 +28,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.notedb.PatchSetState;
import com.google.gwtorm.server.OrmException;
@@ -126,7 +128,10 @@
checkArgument(ps.isDraft(),
"cannot delete non-draft patch set %s", ps.getId());
update.setPatchSetState(PatchSetState.DELETED);
- db.patchSets().delete(Collections.singleton(ps));
+ if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to delete non-existent entities.
+ db.patchSets().delete(Collections.singleton(ps));
+ }
}
private void ensurePatchSetMatches(PatchSet.Id psId, ChangeUpdate update) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 1781f1a..a09537a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -14,16 +14,21 @@
package com.google.gerrit.server;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.Project;
@@ -49,6 +54,7 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
@@ -56,9 +62,46 @@
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
public class ReviewersUtil {
+ @Singleton
+ private static class Metrics {
+ final Timer0 queryAccountsLatency;
+ final Timer0 recommendAccountsLatency;
+ final Timer0 loadAccountsLatency;
+ final Timer0 queryGroupsLatency;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ queryAccountsLatency = metricMaker.newTimer(
+ "reviewer_suggestion/query_accounts",
+ new Description(
+ "Latency for querying accounts for reviewer suggestion")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ recommendAccountsLatency = metricMaker.newTimer(
+ "reviewer_suggestion/recommend_accounts",
+ new Description(
+ "Latency for recommending accounts for reviewer suggestion")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ loadAccountsLatency = metricMaker.newTimer(
+ "reviewer_suggestion/load_accounts",
+ new Description(
+ "Latency for loading accounts for reviewer suggestion")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ queryGroupsLatency = metricMaker.newTimer(
+ "reviewer_suggestion/query_groups",
+ new Description(
+ "Latency for querying groups for reviewer suggestion")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ }
+ }
+
private static final String MAX_SUFFIX = "\u9fa5";
// Generate a candidate list at 3x the size of what the user wants to see to
// give the ranking algorithm a good set of candidates it can work with
@@ -75,6 +118,7 @@
private final Provider<CurrentUser> currentUser;
private final Provider<ReviewDb> dbProvider;
private final ReviewerRecommender reviewerRecommender;
+ private final Metrics metrics;
@Inject
ReviewersUtil(AccountCache accountCache,
@@ -87,7 +131,8 @@
GroupMembers.Factory groupMembersFactory,
Provider<CurrentUser> currentUser,
Provider<ReviewDb> dbProvider,
- ReviewerRecommender reviewerRecommender) {
+ ReviewerRecommender reviewerRecommender,
+ Metrics metrics) {
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
this.accountCache = accountCache;
@@ -101,6 +146,7 @@
this.groupBackend = groupBackend;
this.groupMembersFactory = groupMembersFactory;
this.reviewerRecommender = reviewerRecommender;
+ this.metrics = metrics;
}
public interface VisibilityControl {
@@ -123,58 +169,34 @@
candidateList = suggestAccounts(suggestReviewers, visibilityControl);
}
- List<Account.Id> sortedRecommendations = reviewerRecommender
- .suggestReviewers(changeNotes, suggestReviewers, projectControl,
- candidateList);
-
- // Populate AccountInfo
- List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
- for (Account.Id id : sortedRecommendations) {
- AccountInfo account = accountLoader.get(id);
- if (account != null) {
- SuggestedReviewerInfo info = new SuggestedReviewerInfo();
- info.account = account;
- info.count = 1;
- reviewer.add(info);
- }
- }
- accountLoader.fill();
+ List<Account.Id> sortedRecommendations = recommendAccounts(changeNotes,
+ suggestReviewers, projectControl, candidateList);
+ List<SuggestedReviewerInfo> suggestedReviewer =
+ loadAccounts(sortedRecommendations);
if (!excludeGroups && !Strings.isNullOrEmpty(query)) {
- for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
- GroupAsReviewer result = suggestGroupAsReviewer(
- suggestReviewers, projectControl.getProject(), g, visibilityControl);
- if (result.allowed || result.allowedWithConfirmation) {
- GroupBaseInfo info = new GroupBaseInfo();
- info.id = Url.encode(g.getUUID().get());
- info.name = g.getName();
- SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
- suggestedReviewerInfo.group = info;
- suggestedReviewerInfo.count = result.size;
- if (result.allowedWithConfirmation) {
- suggestedReviewerInfo.confirm = true;
- }
- // Always add groups at the end as individual accounts are usually
- // more important
- reviewer.add(suggestedReviewerInfo);
- }
- }
+ // Add groups at the end as individual accounts are usually more
+ // important.
+ suggestedReviewer.addAll(suggestAccountGroups(
+ suggestReviewers, projectControl, visibilityControl));
}
- if (reviewer.size() <= limit) {
- return reviewer;
+ if (suggestedReviewer.size() <= limit) {
+ return suggestedReviewer;
}
- return reviewer.subList(0, limit);
+ return suggestedReviewer.subList(0, limit);
}
private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers,
VisibilityControl visibilityControl)
throws OrmException {
- AccountIndex searchIndex = accountIndexes.getSearchIndex();
- if (searchIndex != null) {
- return suggestAccountsFromIndex(suggestReviewers);
+ try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
+ AccountIndex searchIndex = accountIndexes.getSearchIndex();
+ if (searchIndex != null) {
+ return suggestAccountsFromIndex(suggestReviewers);
+ }
+ return suggestAccountsFromDb(suggestReviewers, visibilityControl);
}
- return suggestAccountsFromDb(suggestReviewers, visibilityControl);
}
private List<Account.Id> suggestAccountsFromIndex(
@@ -249,7 +271,60 @@
return false;
}
- private List<GroupReference> suggestAccountGroup(
+ private List<Account.Id> recommendAccounts(ChangeNotes changeNotes,
+ SuggestReviewers suggestReviewers, ProjectControl projectControl,
+ List<Account.Id> candidateList) throws OrmException {
+ try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
+ return reviewerRecommender.suggestReviewers(changeNotes, suggestReviewers,
+ projectControl, candidateList);
+ }
+ }
+
+ private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
+ throws OrmException {
+ try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
+ List<SuggestedReviewerInfo> reviewer = accountIds.stream()
+ .map(accountLoader::get)
+ .filter(Objects::nonNull)
+ .map(a -> {
+ SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+ info.account = a;
+ info.count = 1;
+ return info;
+ }).collect(toList());
+ accountLoader.fill();
+ return reviewer;
+ }
+ }
+
+ private List<SuggestedReviewerInfo> suggestAccountGroups(
+ SuggestReviewers suggestReviewers, ProjectControl projectControl,
+ VisibilityControl visibilityControl) throws OrmException, IOException {
+ try (Timer0.Context ctx = metrics.queryGroupsLatency.start()) {
+ List<SuggestedReviewerInfo> groups = new ArrayList<>();
+ for (GroupReference g : suggestAccountGroups(suggestReviewers,
+ projectControl)) {
+ GroupAsReviewer result = suggestGroupAsReviewer(suggestReviewers,
+ projectControl.getProject(), g, visibilityControl);
+ if (result.allowed || result.allowedWithConfirmation) {
+ GroupBaseInfo info = new GroupBaseInfo();
+ info.id = Url.encode(g.getUUID().get());
+ info.name = g.getName();
+ SuggestedReviewerInfo suggestedReviewerInfo =
+ new SuggestedReviewerInfo();
+ suggestedReviewerInfo.group = info;
+ suggestedReviewerInfo.count = result.size;
+ if (result.allowedWithConfirmation) {
+ suggestedReviewerInfo.confirm = true;
+ }
+ groups.add(suggestedReviewerInfo);
+ }
+ }
+ return groups;
+ }
+ }
+
+ private List<GroupReference> suggestAccountGroups(
SuggestReviewers suggestReviewers, ProjectControl ctl) {
return Lists.newArrayList(
Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
index d1f7cac..afec66a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -32,6 +33,7 @@
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.BatchUpdateReviewDb;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -153,6 +155,10 @@
private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id)
throws OrmException {
+ if (PrimaryStorage.of(ctx.getChange()) != REVIEW_DB) {
+ return;
+ }
+ // Avoid OrmConcurrencyException trying to delete non-existent entities.
// Only delete from ReviewDb here; deletion from NoteDb is handled in
// BatchUpdate.
ReviewDb db = unwrap(ctx.getDb());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index e473e39..79cc35b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -34,6 +36,7 @@
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -146,11 +149,14 @@
psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet);
accountPatchReviewStore.get().clearReviewed(psId);
- // Use the unwrap from DeleteChangeOp to handle BatchUpdateReviewDb.
- ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
- db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
- db.patchComments().delete(db.patchComments().byPatchSet(psId));
- db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
+ if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to delete non-existent entities.
+ // Use the unwrap from DeleteChangeOp to handle BatchUpdateReviewDb.
+ ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
+ db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
+ db.patchComments().delete(db.patchComments().byPatchSet(psId));
+ db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
+ }
}
private void deleteOrUpdateDraftChange(ChangeContext ctx)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 2882a29..14d9ae3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.TimeUtil;
@@ -46,6 +48,7 @@
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -179,8 +182,10 @@
} else {
msg.append(".");
}
-
- ctx.getDb().patchSetApprovals().delete(del);
+ if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to update non-existent entities.
+ ctx.getDb().patchSetApprovals().delete(del);
+ }
ChangeUpdate update = ctx.getUpdate(currPs.getId());
update.removeReviewer(reviewerId);
@@ -208,8 +213,10 @@
Account.Id accountId) throws OrmException {
Change.Id changeId = ctx.getNotes().getChangeId();
Iterable<PatchSetApproval> approvals;
+ PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
- if (migration.readChanges()) {
+ if (migration.readChanges()
+ && r == PrimaryStorage.REVIEW_DB) {
// Because NoteDb and ReviewDb have different semantics for zero-value
// approvals, we must fall back to ReviewDb as the source of truth here.
ReviewDb db = ctx.getDb();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index aab40e0..e1d1caa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.LabelTypes;
@@ -44,6 +45,7 @@
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.mail.send.DeleteVoteSender;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.util.LabelVote;
import com.google.gwtorm.server.OrmException;
@@ -166,8 +168,11 @@
}
ctx.getUpdate(psId).removeApprovalFor(accountId, label);
- ctx.getDb().patchSetApprovals().upsert(
- Collections.singleton(deletedApproval(ctx)));
+ if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to update non-existent entities.
+ ctx.getDb().patchSetApprovals().upsert(
+ Collections.singleton(deletedApproval(ctx)));
+ }
StringBuilder msg = new StringBuilder();
msg.append("Removed ");
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 c930c82..2decd12 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.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
@@ -47,8 +48,10 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -83,6 +86,7 @@
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
@@ -227,11 +231,48 @@
try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
revision.getChange().getProject(), revision.getUser(), ts)) {
+ Account.Id id = bu.getUser().getAccountId();
+ boolean ccOrReviewer = input.labels != null;
+
+ if (!ccOrReviewer) {
+ // Check if user was already CCed or reviewing prior to this review.
+ ReviewerSet currentReviewers = approvalsUtil.getReviewers(
+ db.get(), revision.getChangeResource().getNotes());
+ ccOrReviewer = currentReviewers.all().contains(id);
+ }
+
// Apply reviewer changes first. Revision emails should be sent to the
- // updated set of reviewers.
+ // updated set of reviewers. Also keep track of whether the user added
+ // themselves as a reviewer or to the CC list.
for (PostReviewers.Addition reviewerResult : reviewerResults) {
bu.addOp(revision.getChange().getId(), reviewerResult.op);
+ if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
+ for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
+ if (id.equals(reviewerInfo._accountId)) {
+ ccOrReviewer = true;
+ break;
+ }
+ }
+ }
+ if (!ccOrReviewer && reviewerResult.result.ccs != null) {
+ for (AccountInfo accountInfo : reviewerResult.result.ccs) {
+ if (id.equals(accountInfo._accountId)) {
+ ccOrReviewer = true;
+ break;
+ }
+ }
+ }
}
+
+ if (!ccOrReviewer) {
+ // User posting this review isn't currently in the reviewer or CC list,
+ // isn't being explicitly added, and isn't voting on any label.
+ // Automatically CC them on this change so they receive replies.
+ PostReviewers.Addition selfAddition =
+ postReviewers.ccCurrentUser(bu.getUser(), revision);
+ bu.addOp(revision.getChange().getId(), selfAddition.op);
+ }
+
bu.addOp(
revision.getChange().getId(),
new Op(revision.getPatchSet().getId(), input, reviewerResults));
@@ -833,8 +874,11 @@
}
forceCallerAsReviewer(ctx, current, ups, del);
- ctx.getDb().patchSetApprovals().delete(del);
- ctx.getDb().patchSetApprovals().upsert(ups);
+ if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) {
+ // Avoid OrmConcurrencyException trying to delete non-existent entities.
+ ctx.getDb().patchSetApprovals().delete(del);
+ ctx.getDb().patchSetApprovals().upsert(ups);
+ }
return !del.isEmpty() || !ups.isEmpty();
}
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 420c05c..f0af5da 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
@@ -39,6 +39,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
@@ -177,6 +178,13 @@
input.state(), input.notify);
}
+ Addition ccCurrentUser(CurrentUser user, RevisionResource revision) {
+ return new Addition(
+ user.getUserName(), revision.getChangeResource(),
+ ImmutableMap.of(user.getAccountId(), revision.getControl()),
+ CC, NotifyHandling.NONE);
+ }
+
private Addition putAccount(String reviewer, ReviewerResource rsrc,
ReviewerState state, NotifyHandling notify)
throws UnprocessableEntityException {
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 dc229b8..3953f27 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
@@ -53,7 +53,9 @@
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.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -61,7 +63,6 @@
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.util.RequestId;
-import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -937,10 +938,17 @@
@SuppressWarnings("resource") // Not always opened.
NoteDbUpdateManager updateManager = null;
try {
- ChangeContext ctx;
+ PrimaryStorage storage;
db.changes().beginTransaction(id);
try {
- ctx = newChangeContext(db, repo, rw, id);
+ ChangeContext ctx = newChangeContext(db, repo, rw, id);
+ storage = PrimaryStorage.of(ctx.getChange());
+ if (storage == PrimaryStorage.NOTE_DB
+ && !notesMigration.readChanges()) {
+ throw new OrmException(
+ "must have NoteDb enabled to update change " + id);
+ }
+
// Call updateChange on each op.
logDebug("Calling updateChange on {} ops", changeOps.size());
for (Op op : changeOps) {
@@ -960,34 +968,45 @@
updateManager = stageNoteDbUpdate(ctx, deleted);
}
- // Bump lastUpdatedOn or rowVersion and commit.
- Iterable<Change> cs = changesToUpdate(ctx);
- if (newChanges.containsKey(id)) {
- // Insert rather than upsert in case of a race on change IDs.
- logDebug("Inserting change");
- db.changes().insert(cs);
- } else if (deleted) {
- logDebug("Deleting change");
- db.changes().delete(cs);
+ if (storage == PrimaryStorage.REVIEW_DB) {
+ // If primary storage of this change is in ReviewDb, bump
+ // lastUpdatedOn or rowVersion and commit. Otherwise, don't waste
+ // time updating ReviewDb at all.
+ Iterable<Change> cs = changesToUpdate(ctx);
+ if (isNewChange(id)) {
+ // Insert rather than upsert in case of a race on change IDs.
+ logDebug("Inserting change");
+ db.changes().insert(cs);
+ } else if (deleted) {
+ logDebug("Deleting change");
+ db.changes().delete(cs);
+ } else {
+ logDebug("Updating change");
+ db.changes().update(cs);
+ }
+ if (!dryrun) {
+ db.commit();
+ }
} else {
- logDebug("Updating change");
- db.changes().update(cs);
- }
- if (!dryrun) {
- db.commit();
+ logDebug(
+ "Skipping ReviewDb write since primary storage is {}", storage);
}
} finally {
db.rollback();
}
- if (notesMigration.commitChangeWrites()) {
+ // Do not execute the NoteDbUpdateManager, as we don't want too much
+ // contention on the underlying repo, and we would rather use a single
+ // ObjectInserter/BatchRefUpdate later.
+ //
+ // TODO(dborowitz): May or may not be worth trying to batch together
+ // flushed inserters as well.
+ if (storage == PrimaryStorage.NOTE_DB) {
+ // Should have failed above if NoteDb is disabled.
+ checkState(notesMigration.commitChangeWrites());
+ noteDbResult = updateManager.stage().get(id);
+ } else if (notesMigration.commitChangeWrites()) {
try {
- // Do not execute the NoteDbUpdateManager, as we don't want too much
- // contention on the underlying repo, and we would rather use a
- // single ObjectInserter/BatchRefUpdate later.
- //
- // TODO(dborowitz): May or may not be worth trying to batch
- // together flushed inserters as well.
noteDbResult = updateManager.stage().get(id);
} catch (IOException ex) {
// Ignore all errors trying to update NoteDb at this point. We've
@@ -1033,20 +1052,29 @@
for (ChangeUpdate u : ctx.updates.values()) {
updateManager.add(u);
}
+
+ Change c = ctx.getChange();
if (deleted) {
- updateManager.deleteChange(ctx.getChange().getId());
+ updateManager.deleteChange(c.getId());
}
try {
- updateManager.stageAndApplyDelta(ctx.getChange());
- } catch (OrmConcurrencyException ex) {
- // Refused to apply update because NoteDb was out of sync. Go ahead with
- // this ReviewDb update; it's still out of sync, but this is no worse
- // than before, and it will eventually get rebuilt.
- logDebug("Ignoring OrmConcurrencyException while staging");
+ updateManager.stageAndApplyDelta(c);
+ } catch (MismatchedStateException ex) {
+ // Refused to apply update because NoteDb was out of sync, which can
+ // only happen if ReviewDb is the primary storage for this change.
+ //
+ // Go ahead with this ReviewDb update; it's still out of sync, but this
+ // is no worse than before, and it will eventually get rebuilt.
+ logDebug("Ignoring MismatchedStateException while staging");
}
+
return updateManager;
}
+ private boolean isNewChange(Change.Id id) {
+ return newChanges.containsKey(id);
+ }
+
private void logDebug(String msg, Throwable t) {
if (log.isDebugEnabled()) {
BatchUpdate.this.logDebug("[" + taskId + "]" + msg, t);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index d4655f5..2088261 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -24,7 +24,16 @@
if (0 <= lt && lt < gt && lt + 1 < at && at + 1 < gt) {
final String email = in.substring(lt + 1, gt).trim();
final String name = in.substring(0, lt).trim();
- return new Address(name.length() > 0 ? name : null, email);
+ int nameStart = 0;
+ int nameEnd = name.length();
+ if (name.startsWith("\"")) {
+ nameStart++;
+ }
+ if (name.endsWith("\"")) {
+ nameEnd--;
+ }
+ return new Address(name.length() > 0 ?
+ name.substring(nameStart, nameEnd): null, email);
}
if (lt < 0 && gt < 0 && 0 < at && at < in.length() - 1) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
index 2f2fd8c5..b719193 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -15,21 +15,47 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.receive.Protocol;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
+import java.util.concurrent.TimeUnit;
+
@Singleton
public class EmailSettings {
+ private static final String SEND_EMAL = "sendemail";
+ private static final String RECEIVE_EMAL = "receiveemail";
+ // Send
public final boolean html;
public final boolean includeDiff;
public final int maximumDiffSize;
+ // Receive
+ public final Protocol protocol;
+ public final String host;
+ public final int port;
+ public final String username;
+ public final String password;
+ public final Encryption encryption;
+ public final long fetchInterval; // in milliseconds
@Inject
EmailSettings(@GerritServerConfig Config cfg) {
- html = cfg.getBoolean("sendemail", "html", true);
- includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
- maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
+ // Send
+ html = cfg.getBoolean(SEND_EMAL, "html", true);
+ includeDiff = cfg.getBoolean(SEND_EMAL, "includeDiff", false);
+ maximumDiffSize = cfg.getInt(SEND_EMAL, "maximumDiffSize", 256 << 10);
+ // Receive
+ protocol = cfg.getEnum(RECEIVE_EMAL, null, "protocol", Protocol.NONE);
+ host = cfg.getString(RECEIVE_EMAL, null, "host");
+ port = cfg.getInt(RECEIVE_EMAL, "port", 0);
+ username = cfg.getString(RECEIVE_EMAL, null, "username");
+ password = cfg.getString(RECEIVE_EMAL, null, "password");
+ encryption =
+ cfg.getEnum(RECEIVE_EMAL, null, "encryption", Encryption.NONE);
+ fetchInterval = cfg.getTimeUnit(RECEIVE_EMAL, null, "fetchInterval",
+ TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS),
+ TimeUnit.MILLISECONDS);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java
new file mode 100644
index 0000000..a557532
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java
@@ -0,0 +1,19 @@
+// 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.mail;
+
+public enum Encryption {
+ NONE, SSL, TLS
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
new file mode 100644
index 0000000..32a26b1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
@@ -0,0 +1,143 @@
+// 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.mail.receive;
+
+import com.google.gerrit.server.mail.EmailSettings;
+import com.google.gerrit.server.mail.Encryption;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.commons.net.imap.IMAPClient;
+import org.apache.commons.net.imap.IMAPSClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class ImapMailReceiver extends MailReceiver {
+ private static final Logger log =
+ LoggerFactory.getLogger(ImapMailReceiver.class);
+ private static final String inboxFolder = "INBOX";
+
+ @Inject
+ public ImapMailReceiver(EmailSettings mailSettings) {
+ super(mailSettings);
+ }
+
+ /**
+ * handleEmails will open a connection to the mail server, remove emails
+ * where deletion is pending, read new email and close the connection.
+ */
+ @Override
+ public synchronized void handleEmails() {
+ IMAPClient imap;
+ if (mailSettings.encryption != Encryption.NONE) {
+ imap = new IMAPSClient(mailSettings.encryption.name(), false);
+ } else {
+ imap = new IMAPClient();
+ }
+ if (mailSettings.port > 0) {
+ imap.setDefaultPort(mailSettings.port);
+ }
+ // Set a 30s timeout for each operation
+ imap.setDefaultTimeout(30 * 1000);
+ try {
+ imap.connect(mailSettings.host);
+ try {
+ if (!imap.login(mailSettings.username, mailSettings.password)) {
+ log.error("Could not login to IMAP server");
+ return;
+ }
+ try {
+ if (!imap.select(inboxFolder)){
+ log.error("Could not select IMAP folder " + inboxFolder);
+ return;
+ }
+ // Fetch just the internal dates first to know how many messages we
+ // should fetch.
+ if (!imap.fetch("1:*", "(INTERNALDATE)")) {
+ log.error("IMAP fetch failed. Will retry in next fetch cycle.");
+ return;
+ }
+ // Format of reply is one line per email and one line to indicate
+ // that the fetch was successful.
+ // Example:
+ // * 1 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)")
+ // * 2 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)")
+ // AAAC OK FETCH completed.
+ int numMessages = imap.getReplyStrings().length - 1;
+ log.info("Fetched " + numMessages + " messages via IMAP");
+ if (numMessages == 0) {
+ return;
+ }
+ // Fetch the full version of all emails
+ List<MailMessage> mailMessages = new ArrayList<>(numMessages);
+ for (int i = 1; i <= numMessages; i++) {
+ if (imap.fetch(i + ":" + i, "(BODY.PEEK[])")) {
+ // Obtain full reply
+ String[] rawMessage = imap.getReplyStrings();
+ if (rawMessage.length < 2) {
+ continue;
+ }
+ // First and last line are IMAP status codes. We have already
+ // checked, that the fetch returned true (OK), so we safely ignore
+ // those two lines.
+ StringBuilder b = new StringBuilder(2 * (rawMessage.length - 2));
+ for(int j = 1; j < rawMessage.length - 1; j++) {
+ if (j > 1) {
+ b.append("\n");
+ }
+ b.append(rawMessage[j]);
+ }
+ try {
+ MailMessage mailMessage = RawMailParser.parse(b.toString());
+ if (pendingDeletion.contains(mailMessage.id())) {
+ // Mark message as deleted
+ if (imap.store(i + ":" + i, "+FLAGS", "(\\Deleted)")) {
+ pendingDeletion.remove(mailMessage.id());
+ } else {
+ log.error("Could not mark mail message as deleted: " +
+ mailMessage.id());
+ }
+ } else {
+ mailMessages.add(mailMessage);
+ }
+ } catch (MailParsingException e) {
+ log.error("Exception while parsing email after IMAP fetch", e);
+ }
+ } else {
+ log.error("IMAP fetch failed. Will retry in next fetch cycle.");
+ }
+ }
+ // Permanently delete emails marked for deletion
+ if (!imap.expunge()) {
+ log.error("Could not expunge IMAP emails");
+ }
+ // TODO(hiesel) Call email handling logic with mailMessages
+ } finally {
+ imap.logout();
+ }
+ } finally {
+ imap.disconnect();
+ }
+ } catch (IOException e) {
+ log.error("Error while talking to IMAP server", e);
+ return;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
new file mode 100644
index 0000000..966f3e6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
@@ -0,0 +1,89 @@
+// 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.mail.receive;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.mail.Address;
+
+import org.joda.time.DateTime;
+
+/**
+ * MailMessage is a simplified representation of an RFC 2045-2047 mime email
+ * message used for representing received emails inside Gerrit. It is populated
+ * by the MailParser after MailReceiver has received a message. Transformations
+ * done by the parser include stitching mime parts together, transforming all
+ * content to UTF-16 and removing attachments.
+ *
+ * A valid MailMessage contains at least the following fields: id, from, to,
+ * subject and dateReceived.
+ */
+@AutoValue
+public abstract class MailMessage {
+ // Unique Identifier
+ public abstract String id();
+ // Envelop Information
+ public abstract Address from();
+ public abstract ImmutableList<Address> to();
+ @Nullable
+ public abstract ImmutableList<Address> cc();
+ // Metadata
+ public abstract DateTime dateReceived();
+ public abstract ImmutableList<String> additionalHeaders();
+ // Content
+ public abstract String subject();
+ @Nullable
+ public abstract String textContent();
+ @Nullable
+ public abstract String htmlContent();
+
+ public static Builder builder() {
+ return new AutoValue_MailMessage.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder id(String val);
+ public abstract Builder from(Address val);
+ public abstract ImmutableList.Builder<Address> toBuilder();
+
+ public Builder addTo(Address val) {
+ toBuilder().add(val);
+ return this;
+ }
+
+ public abstract ImmutableList.Builder<Address> ccBuilder();
+
+ public Builder addCc(Address val) {
+ ccBuilder().add(val);
+ return this;
+ }
+
+ public abstract Builder dateReceived(DateTime val);
+ public abstract ImmutableList.Builder<String> additionalHeadersBuilder();
+
+ public Builder addAdditionalHeader(String val) {
+ additionalHeadersBuilder().add(val);
+ return this;
+ }
+
+ public abstract Builder subject(String val);
+ public abstract Builder textContent(String val);
+ public abstract Builder htmlContent(String val);
+
+ public abstract MailMessage build();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.java
new file mode 100644
index 0000000..edadef8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.java
@@ -0,0 +1,28 @@
+// 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.mail.receive;
+
+/** MailParsingException indicates that an email could not be parsed. */
+public class MailParsingException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public MailParsingException(String msg) {
+ super(msg);
+ }
+
+ public MailParsingException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java
new file mode 100644
index 0000000..4a8f7fb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -0,0 +1,106 @@
+// 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.mail.receive;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.mail.EmailSettings;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/** MailReceiver implements base functionality for receiving emails. */
+public abstract class MailReceiver implements LifecycleListener {
+ protected EmailSettings mailSettings;
+ protected Set<String> pendingDeletion;
+ private Timer timer;
+
+ public static class Module extends LifecycleModule {
+ private final EmailSettings mailSettings;
+
+ @Inject
+ Module(EmailSettings mailSettings) {
+ this.mailSettings = mailSettings;
+ }
+
+ @Override
+ protected void configure() {
+ if (mailSettings.protocol == Protocol.NONE) {
+ return;
+ }
+ listener().to(MailReceiver.class);
+ switch (mailSettings.protocol) {
+ case IMAP:
+ bind(MailReceiver.class).to(ImapMailReceiver.class);
+ break;
+ case POP3:
+ bind(MailReceiver.class).to(Pop3MailReceiver.class);
+ break;
+ case NONE:
+ default:
+ }
+ }
+ }
+
+ @Inject
+ public MailReceiver(EmailSettings mailSettings) {
+ this.mailSettings = mailSettings;
+ pendingDeletion = Collections.synchronizedSet(new HashSet<>());
+ }
+
+ @Override
+ public void start() {
+ if (timer == null) {
+ timer = new Timer();
+ } else {
+ timer.cancel();
+ }
+ timer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ MailReceiver.this.handleEmails();
+ }
+ }, 0l, mailSettings.fetchInterval);
+ }
+
+ @Override
+ public void stop() {
+ if (timer != null) {
+ timer.cancel();
+ }
+ }
+
+ /**
+ * requestDeletion will enqueue an email for deletion and delete it the
+ * next time we connect to the email server. This does not guarantee deletion
+ * as the Gerrit instance might fail before we connect to the email server.
+ * @param messageId
+ */
+ public void requestDeletion(String messageId) {
+ pendingDeletion.add(messageId);
+ }
+
+ /**
+ * handleEmails will open a connection to the mail server, remove emails
+ * where deletion is pending, read new email and close the connection.
+ */
+ @VisibleForTesting
+ public abstract void handleEmails();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
new file mode 100644
index 0000000..6c81011
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -0,0 +1,134 @@
+// 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.mail.receive;
+
+import com.google.common.primitives.Ints;
+import com.google.gerrit.server.mail.EmailSettings;
+import com.google.gerrit.server.mail.Encryption;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.commons.net.pop3.POP3Client;
+import org.apache.commons.net.pop3.POP3MessageInfo;
+import org.apache.commons.net.pop3.POP3SClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class Pop3MailReceiver extends MailReceiver {
+ private static final Logger log =
+ LoggerFactory.getLogger(Pop3MailReceiver.class);
+
+ @Inject
+ public Pop3MailReceiver(EmailSettings mailSettings) {
+ super(mailSettings);
+ }
+
+ /**
+ * handleEmails will open a connection to the mail server, remove emails
+ * where deletion is pending, read new email and close the connection.
+ */
+ @Override
+ public synchronized void handleEmails() {
+ POP3Client pop3;
+ if (mailSettings.encryption != Encryption.NONE) {
+ pop3 = new POP3SClient(mailSettings.encryption.name());
+ } else {
+ pop3 = new POP3Client();
+ }
+ if (mailSettings.port > 0) {
+ pop3.setDefaultPort(mailSettings.port);
+ }
+ try {
+ pop3.connect(mailSettings.host);
+ } catch (IOException e) {
+ log.error("Could not connect to POP3 email server", e);
+ return;
+ }
+ try {
+ try {
+ if (!pop3.login(mailSettings.username, mailSettings.password)) {
+ log.error("Could not login to POP3 email server."
+ + " Check username and password");
+ return;
+ }
+ try {
+ POP3MessageInfo[] messages = pop3.listMessages();
+ if (messages == null) {
+ log.error("Could not retrieve message list via POP3");
+ return;
+ }
+ log.info("Received " + messages.length + " messages via POP3");
+ // Fetch messages
+ List<MailMessage> mailMessages = new ArrayList<>();
+ for (POP3MessageInfo msginfo : messages) {
+ if (msginfo == null) {
+ // Message was deleted
+ continue;
+ }
+ try (BufferedReader reader =
+ (BufferedReader) pop3.retrieveMessage(msginfo.number)) {
+ if (reader == null) {
+ log.error(
+ "Could not retrieve POP3 message header for message {}",
+ msginfo.identifier);
+ return;
+ }
+ int[] message = fetchMessage(reader);
+ MailMessage mailMessage = RawMailParser.parse(message);
+ // Delete messages where deletion is pending. This requires
+ // knowing the integer message ID of the email. We therefore parse
+ // the message first and extract the Message-ID specified in RFC
+ // 822 and delete the message if deletion is pending.
+ if (pendingDeletion.contains(mailMessage.id())) {
+ if (pop3.deleteMessage(msginfo.number)) {
+ pendingDeletion.remove(mailMessage.id());
+ } else {
+ log.error("Could not delete message " + msginfo.number);
+ }
+ } else {
+ // Process message further
+ mailMessages.add(mailMessage);
+ }
+ } catch (MailParsingException e) {
+ log.error("Could not parse message " + msginfo.number);
+ }
+ }
+ // TODO(hiesel) Call processing logic with mailMessages
+ } finally {
+ pop3.logout();
+ }
+ } finally {
+ pop3.disconnect();
+ }
+ } catch (IOException e) {
+ log.error("Error while issuing POP3 command", e);
+ }
+ }
+
+ public final int[] fetchMessage(BufferedReader reader) throws IOException {
+ List<Integer> character = new ArrayList<>();
+ int ch;
+ while ((ch = reader.read()) != -1) {
+ character.add(ch);
+ }
+ return Ints.toArray(character);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java
new file mode 100644
index 0000000..e8311f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java
@@ -0,0 +1,19 @@
+// 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.mail.receive;
+
+public enum Protocol {
+ NONE, POP3, IMAP
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
new file mode 100644
index 0000000..ed35c9b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
@@ -0,0 +1,170 @@
+// 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.mail.receive;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.CharStreams;
+import com.google.gerrit.server.mail.Address;
+
+import org.apache.james.mime4j.MimeException;
+import org.apache.james.mime4j.dom.Entity;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.dom.MessageBuilder;
+import org.apache.james.mime4j.dom.Multipart;
+import org.apache.james.mime4j.dom.TextBody;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.message.DefaultMessageBuilder;
+import org.joda.time.DateTime;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * RawMailParser parses raw email content received through POP3 or IMAP into
+ * an internal {@link MailMessage}.
+ */
+public class RawMailParser {
+ private static final ImmutableSet<String> MAIN_HEADERS =
+ ImmutableSet.of("to", "from", "cc", "date", "message-id",
+ "subject", "content-type");
+
+ /**
+ * Parses a MailMessage from a string.
+ * @param raw String as received over the wire
+ * @return Parsed MailMessage
+ * @throws MailParsingException
+ */
+ public static MailMessage parse(String raw) throws MailParsingException {
+ MailMessage.Builder messageBuilder = MailMessage.builder();
+ Message mimeMessage;
+ try {
+ MessageBuilder builder = new DefaultMessageBuilder();
+ mimeMessage =
+ builder.parseMessage(new ByteArrayInputStream(raw.getBytes()));
+ } catch (IOException | MimeException e) {
+ throw new MailParsingException("Can't parse email", e);
+ }
+ // Add general headers
+ messageBuilder.id(mimeMessage.getMessageId());
+ messageBuilder.subject(mimeMessage.getSubject());
+ messageBuilder.dateReceived(new DateTime(mimeMessage.getDate()));
+
+ // Add From, To and Cc
+ if (mimeMessage.getFrom() != null && mimeMessage.getFrom().size() > 0) {
+ Mailbox from = mimeMessage.getFrom().get(0);
+ messageBuilder.from(new Address(from.getName(), from.getAddress()));
+ }
+ if (mimeMessage.getTo() != null) {
+ for (Mailbox m : mimeMessage.getTo().flatten()) {
+ messageBuilder.addTo(new Address(m.getName(), m.getAddress()));
+ }
+ }
+ if (mimeMessage.getCc() != null) {
+ for (Mailbox m : mimeMessage.getCc().flatten()) {
+ messageBuilder.addCc(new Address(m.getName(), m.getAddress()));
+ }
+ }
+
+ // Add additional headers
+ mimeMessage.getHeader().getFields().stream()
+ .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase()))
+ .forEach(f -> messageBuilder.addAdditionalHeader(
+ f.getName() + ": " + f.getBody()));
+
+ // Add text and html body parts
+ StringBuilder textBuilder = new StringBuilder();
+ StringBuilder htmlBuilder = new StringBuilder();
+ try {
+ handleMimePart(mimeMessage, textBuilder, htmlBuilder);
+ } catch (IOException e) {
+ throw new MailParsingException("Can't parse email", e);
+ }
+ messageBuilder.textContent(Strings.emptyToNull(textBuilder.toString()));
+ messageBuilder.htmlContent(Strings.emptyToNull(htmlBuilder.toString()));
+
+ try {
+ // build() will only succeed if all required attributes were set. We wrap
+ // the IllegalStateException in a MailParsingException indicating that
+ // required attributes are missing, so that the caller doesn't fall over.
+ return messageBuilder.build();
+ } catch (IllegalStateException e) {
+ throw new MailParsingException(
+ "Missing required attributes after email was parsed", e);
+ }
+ }
+
+ /**
+ * Parses a MailMessage from an array of characters. Note that the character
+ * array is int-typed. This method is only used by POP3, which specifies that
+ * all transferred characters are US-ASCII (RFC 6856). When reading the input
+ * in Java, io.Reader yields ints. These can be safely converted to chars
+ * as all US-ASCII characters fit in a char. If emails contain non-ASCII
+ * characters, such as UTF runes, these will be encoded in ASCII using either
+ * Base64 or quoted-printable encoding.
+ * @param chars Array as received over the wire
+ * @return Parsed MailMessage
+ * @throws MailParsingException
+ */
+ public static MailMessage parse(int[] chars) throws MailParsingException {
+ StringBuilder b = new StringBuilder(chars.length);
+ for (int c : chars) {
+ b.append((char) c);
+ }
+ return parse(b.toString());
+ }
+
+ /**
+ * Traverses a mime tree and parses out text and html parts. All other parts
+ * will be dropped.
+ * @param part MimePart to parse
+ * @param textBuilder StringBuilder to append all plaintext parts
+ * @param htmlBuilder StringBuilder to append all html parts
+ * @throws IOException
+ */
+ private static void handleMimePart(Entity part, StringBuilder textBuilder,
+ StringBuilder htmlBuilder) throws IOException {
+ if (isPlainOrHtml(part.getMimeType()) &&
+ !isAttachment(part.getDispositionType())) {
+ TextBody tb = (TextBody) part.getBody();
+ String result = CharStreams.toString(new InputStreamReader(
+ tb.getInputStream(), tb.getMimeCharset()));
+ if (part.getMimeType().equals("text/plain")) {
+ textBuilder.append(result);
+ } else if (part.getMimeType().equals("text/html")) {
+ htmlBuilder.append(result);
+ }
+ } else if (isMixedOrAlternative(part.getMimeType())) {
+ Multipart multipart = (Multipart) part.getBody();
+ for (Entity e : multipart.getBodyParts()) {
+ handleMimePart(e, textBuilder, htmlBuilder);
+ }
+ }
+ }
+
+ private static boolean isPlainOrHtml(String mimeType) {
+ return (mimeType.equals("text/plain") || mimeType.equals("text/html"));
+ }
+
+ private static boolean isMixedOrAlternative(String mimeType) {
+ return mimeType.equals("multipart/alternative") ||
+ mimeType.equals("multipart/mixed");
+ }
+
+ private static boolean isAttachment(String dispositionType) {
+ return dispositionType != null && dispositionType.equals("attachment");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index f27c45f..d5622bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.Encryption;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -61,10 +62,6 @@
}
}
- public enum Encryption {
- NONE, SSL, TLS
- }
-
private final boolean enabled;
private final int connectTimeout;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 3c669f0..c00035c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -109,16 +110,20 @@
}
protected final Args args;
+ protected final PrimaryStorage primaryStorage;
protected final boolean autoRebuild;
private final Change.Id changeId;
private ObjectId revision;
private boolean loaded;
- AbstractChangeNotes(Args args, Change.Id changeId, boolean autoRebuild) {
+ AbstractChangeNotes(Args args, Change.Id changeId,
+ @Nullable PrimaryStorage primaryStorage, boolean autoRebuild) {
this.args = checkNotNull(args);
this.changeId = checkNotNull(changeId);
- this.autoRebuild = autoRebuild;
+ this.primaryStorage = primaryStorage;
+ this.autoRebuild = primaryStorage == PrimaryStorage.REVIEW_DB
+ && autoRebuild;
}
public Change.Id getChangeId() {
@@ -135,6 +140,9 @@
return self();
}
boolean read = args.migration.readChanges();
+ if (!read && primaryStorage == PrimaryStorage.NOTE_DB) {
+ throw new OrmException("NoteDb is required to read change " + changeId);
+ }
boolean readOrWrite = read || args.migration.writeChanges();
if (!readOrWrite && !autoRebuild) {
loadDefaults();
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 eda50d7..d0b0c54 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
@@ -98,16 +98,7 @@
public static Change readOneReviewDbChange(ReviewDb db, Change.Id id)
throws OrmException {
- return checkNoteDbState(ReviewDbUtil.unwrapDb(db).changes().get(id));
- }
-
- private static Change checkNoteDbState(Change c) throws OrmException {
- NoteDbChangeState s = NoteDbChangeState.parse(c);
- if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
- throw new OrmException(
- "invalid NoteDbChangeState in " + c.getId() + ": " + s);
- }
- return c;
+ return ReviewDbUtil.unwrapDb(db).changes().get(id);
}
@Singleton
@@ -276,7 +267,6 @@
}
} else {
for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
- checkNoteDbState(change);
ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
if (predicate.test(notes)) {
m.put(change.getProject(), notes);
@@ -373,7 +363,7 @@
private ChangeNotes(Args args, Change change, boolean autoRebuild,
@Nullable RefCache refs) {
- super(args, change.getId(), autoRebuild);
+ super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
this.change = new Change(change);
this.refs = refs;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index e0f7920..d5abdee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -31,6 +31,7 @@
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -141,7 +142,10 @@
abstract Timestamp createdOn();
abstract Timestamp lastUpdatedOn();
abstract Account.Id owner();
- abstract String branch(); // Project not included.
+
+ // Project not included, as it's not stored anywhere in the meta ref.
+ abstract String branch();
+
@Nullable abstract PatchSet.Id currentPatchSetId();
abstract String subject();
@Nullable abstract String topic();
@@ -172,17 +176,35 @@
changeMessagesByPatchSet();
abstract ImmutableListMultimap<RevId, Comment> publishedComments();
+ Change newChange(Project.NameKey project) {
+ ChangeColumns c = checkNotNull(columns(), "columns are required");
+ Change change = new Change(
+ c.changeKey(),
+ changeId(),
+ c.owner(),
+ new Branch.NameKey(project, c.branch()),
+ c.createdOn());
+ copyNonConstructorColumnsTo(change);
+ change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+ return change;
+ }
+
void copyColumnsTo(Change change) {
- ChangeColumns c = checkNotNull(columns());
+ ChangeColumns c = checkNotNull(columns(), "columns are required");
+ change.setKey(c.changeKey());
+ change.setOwner(c.owner());
+ change.setDest(new Branch.NameKey(change.getProject(), c.branch()));
+ change.setCreatedOn(c.createdOn());
+ copyNonConstructorColumnsTo(change);
+ }
+
+ private void copyNonConstructorColumnsTo(Change change) {
+ ChangeColumns c = checkNotNull(columns(), "columns are required");
if (c.status() != null) {
change.setStatus(c.status());
}
- change.setKey(c.changeKey());
- change.setDest(new Branch.NameKey(change.getProject(), c.branch()));
change.setTopic(Strings.emptyToNull(c.topic()));
- change.setCreatedOn(c.createdOn());
change.setLastUpdatedOn(c.lastUpdatedOn());
- change.setOwner(c.owner());
change.setSubmissionId(c.submissionId());
change.setAssignee(c.assignee());
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 661112e..7ba90a2 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
@@ -31,6 +31,7 @@
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.StagedResult;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -85,7 +86,9 @@
Args args,
@Assisted Change.Id changeId,
@Assisted Account.Id author) {
- super(args, changeId, true);
+ // PrimaryStorage is unknown; this should only called by
+ // PatchLineCommentsUtil#draftByAuthor, which can live with this.
+ super(args, changeId, null, false);
this.change = null;
this.author = author;
this.rebuildResult = null;
@@ -97,7 +100,7 @@
Account.Id author,
boolean autoRebuild,
NoteDbUpdateManager.Result rebuildResult) {
- super(args, change.getId(), autoRebuild);
+ super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
this.change = change;
this.author = author;
this.rebuildResult = rebuildResult;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index ba9aca7..80f9eaf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -63,19 +63,21 @@
public static final String NOTE_DB_PRIMARY_STATE = "N";
public enum PrimaryStorage {
- REVIEW_DB('R', true),
- NOTE_DB('N', false);
+ REVIEW_DB('R'),
+ NOTE_DB('N');
private final char code;
- private final boolean writeToReviewDb;
- private PrimaryStorage(char code, boolean writeToReviewDb) {
+ private PrimaryStorage(char code) {
this.code = code;
- this.writeToReviewDb = writeToReviewDb;
}
- public boolean writeToReviewDb() {
- return writeToReviewDb;
+ public static PrimaryStorage of(Change c) {
+ return of(NoteDbChangeState.parse(c));
+ }
+
+ public static PrimaryStorage of(NoteDbChangeState s) {
+ return s != null ? s.getPrimaryStorage() : REVIEW_DB;
}
}
@@ -234,6 +236,9 @@
public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state,
RefCache changeRepoRefs, Change.Id changeId) throws IOException {
+ if (PrimaryStorage.of(state) == NOTE_DB) {
+ return true; // Primary storage is NoteDb, up to date by definition.
+ }
if (state == null) {
return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent();
}
@@ -243,6 +248,9 @@
public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state,
RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId)
throws IOException {
+ if (PrimaryStorage.of(state) == NOTE_DB) {
+ return true; // Primary storage is NoteDb, up to date by definition.
+ }
if (state == null) {
return !draftsRepoRefs.get(refsDraftComments(changeId, accountId))
.isPresent();
@@ -286,6 +294,9 @@
}
public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
+ if (primaryStorage == NOTE_DB) {
+ return true; // Primary storage is NoteDb, up to date by definition.
+ }
Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
if (!id.isPresent()) {
return getChangeMetaId().equals(ObjectId.zeroId());
@@ -295,6 +306,9 @@
public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
throws IOException {
+ if (primaryStorage == NOTE_DB) {
+ return true; // Primary storage is NoteDb, up to date by definition.
+ }
Optional<ObjectId> id =
draftsRepoRefs.get(refsDraftComments(changeId, accountId));
if (!id.isPresent()) {
@@ -305,6 +319,9 @@
public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
throws IOException {
+ if (primaryStorage == NOTE_DB) {
+ return true; // Primary storage is NoteDb, up to date by definition.
+ }
if (!isChangeUpToDate(changeRepoRefs)) {
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 1f64bcc..1e5cb1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.InsertedObject;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
@@ -489,6 +490,17 @@
}
}
+ public static class MismatchedStateException extends OrmException {
+ private static final long serialVersionUID = 1L;
+
+ private MismatchedStateException(Change.Id id, NoteDbChangeState expectedState) {
+ super(String.format(
+ "cannot apply NoteDb updates for change %s;"
+ + " change meta ref does not match %s",
+ id, expectedState.getChangeMetaId().name()));
+ }
+ }
+
private void checkExpectedState() throws OrmException, IOException {
if (!checkExpectedState) {
return;
@@ -513,15 +525,17 @@
// - We short-circuited before adding any commands that update this
// ref, and we won't stage a delta for this change either.
// Either way, it is safe to proceed here rather than throwing
- // OrmConcurrencyException.
+ // MismatchedStateException.
+ continue;
+ }
+
+ if (expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
+ // NoteDb is primary, no need to compare state to ReviewDb.
continue;
}
if (!expectedState.isChangeUpToDate(changeRepo.cmds.getRepoRefCache())) {
- throw new OrmConcurrencyException(String.format(
- "cannot apply NoteDb updates for change %s;"
- + " change meta ref does not match %s",
- u.getId(), expectedState.getChangeMetaId().name()));
+ throw new MismatchedStateException(u.getId(), expectedState);
}
}
@@ -529,7 +543,8 @@
ChangeDraftUpdate u = us.iterator().next();
NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange());
- if (expectedState == null) {
+ if (expectedState == null
+ || expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
continue; // See above.
}
@@ -539,7 +554,8 @@
throw new OrmConcurrencyException(String.format(
"cannot apply NoteDb updates for change %s;"
+ " draft ref for account %s does not match %s",
- u.getId(), accountId, expectedState.getChangeMetaId().name()));
+ u.getId(), accountId,
+ expectedState.getChangeMetaId().name()));
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index 4a26d7e..f60fd2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -22,6 +22,7 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
@@ -47,7 +48,7 @@
RobotCommentNotes(
Args args,
@Assisted Change change) {
- super(args, change.getId(), false);
+ super(args, change.getId(), PrimaryStorage.of(change), false);
this.change = change;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index b3aa420..20524cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -57,6 +57,7 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
@@ -185,7 +186,8 @@
public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
throws NoSuchChangeException, IOException, OrmException {
db = ReviewDbUtil.unwrapDb(db);
- Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
+ Change change =
+ checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
if (change == null) {
throw new NoSuchChangeException(changeId);
}
@@ -201,7 +203,8 @@
NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
IOException {
db = ReviewDbUtil.unwrapDb(db);
- Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
+ Change change =
+ checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
if (change == null) {
throw new NoSuchChangeException(changeId);
}
@@ -254,6 +257,16 @@
return r;
}
+ private static Change checkNoteDbState(Change c) throws OrmException {
+ // Can only rebuild a change if its primary storage is ReviewDb.
+ NoteDbChangeState s = NoteDbChangeState.parse(c);
+ if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
+ throw new OrmException(String.format(
+ "cannot rebuild change " + c.getId() + " with state " + s));
+ }
+ return c;
+ }
+
@Override
public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
throws IOException, OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 2da2031..893b2d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.schema;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -30,6 +31,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/** A version of the database schema. */
public abstract class SchemaVersion {
@@ -142,11 +144,14 @@
private void migrateData(List<SchemaVersion> pending, UpdateUI ui,
CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException {
for (SchemaVersion v : pending) {
+ Stopwatch sw = Stopwatch.createStarted();
ui.message(String.format(
"Migrating data to schema %d ...",
v.getVersionNbr()));
v.migrateData(db, ui);
v.finish(curr, db);
+ ui.message(String.format("\t> Done (%.3f s)",
+ sw.elapsed(TimeUnit.MILLISECONDS) / 1000d));
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
new file mode 100644
index 0000000..5ef077a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
@@ -0,0 +1,76 @@
+// 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.mail.receive;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.mail.receive.data.AttachmentMessage;
+import com.google.gerrit.server.mail.receive.data.Base64HeaderMessage;
+import com.google.gerrit.server.mail.receive.data.HtmlMimeMessage;
+import com.google.gerrit.server.mail.receive.data.NonUTF8Message;
+import com.google.gerrit.server.mail.receive.data.QuotedPrintableHeaderMessage;
+import com.google.gerrit.server.mail.receive.data.RawMailMessage;
+import com.google.gerrit.server.mail.receive.data.SimpleTextMessage;
+import com.google.gerrit.testutil.GerritBaseTests;
+
+import org.junit.Test;
+
+public class RawMailParserTest extends GerritBaseTests {
+ @Test
+ public void testParseEmail() throws Exception {
+ RawMailMessage[] messages = new RawMailMessage[] {
+ new SimpleTextMessage(),
+ new Base64HeaderMessage(),
+ new QuotedPrintableHeaderMessage(),
+ new HtmlMimeMessage(),
+ new AttachmentMessage(),
+ new NonUTF8Message(),
+ };
+ for (RawMailMessage rawMailMessage : messages) {
+ if (rawMailMessage.rawChars() != null) {
+ // Assert Character to Mail Parser
+ MailMessage parsedMailMessage =
+ RawMailParser.parse(rawMailMessage.rawChars());
+ assertMail(parsedMailMessage, rawMailMessage.expectedMailMessage());
+ }
+ if (rawMailMessage.raw() != null) {
+ // Assert String to Mail Parser
+ MailMessage parsedMailMessage = RawMailParser
+ .parse(rawMailMessage.raw());
+ assertMail(parsedMailMessage, rawMailMessage.expectedMailMessage());
+ }
+ }
+ }
+
+ /**
+ * This method makes it easier to debug failing tests by checking each
+ * property individual instead of calling equals as it will immediately
+ * reveal the property that diverges between the two objects.
+ * @param have MailMessage retrieved from the parser
+ * @param want MailMessage that would be expected
+ */
+ private void assertMail(MailMessage have, MailMessage want) {
+ assertThat(have.id()).isEqualTo(want.id());
+ assertThat(have.to()).isEqualTo(want.to());
+ assertThat(have.from()).isEqualTo(want.from());
+ assertThat(have.cc()).isEqualTo(want.cc());
+ assertThat(have.dateReceived().getMillis())
+ .isEqualTo(want.dateReceived().getMillis());
+ assertThat(have.additionalHeaders()).isEqualTo(want.additionalHeaders());
+ assertThat(have.subject()).isEqualTo(want.subject());
+ assertThat(have.textContent()).isEqualTo(want.textContent());
+ assertThat(have.htmlContent()).isEqualTo(want.htmlContent());
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
new file mode 100644
index 0000000..390209a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
@@ -0,0 +1,91 @@
+// 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.mail.receive.data;
+
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests that all mime parts that are neither text/plain, nor text/html are
+ * dropped.
+ */
+@Ignore
+public class AttachmentMessage extends RawMailMessage {
+ private static String raw = "MIME-Version: 1.0\n" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "Message-ID: <CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w" +
+ "@mail.gmail.com>\n" +
+ "Subject: Test Subject\n" +
+ "From: Patrick Hiesel <hiesel@google.com>\n" +
+ "To: Patrick Hiesel <hiesel@google.com>\n" +
+ "Content-Type: multipart/mixed; boundary=001a114e019a56962d054062708f\n" +
+ "\n" +
+ "--001a114e019a56962d054062708f\n" +
+ "Content-Type: multipart/alternative; boundary=001a114e019a5696250540" +
+ "62708d\n" +
+ "\n" +
+ "--001a114e019a569625054062708d\n" +
+ "Content-Type: text/plain; charset=UTF-8\n" +
+ "\n" +
+ "Contains unwanted attachment" +
+ "\n" +
+ "--001a114e019a569625054062708d\n" +
+ "Content-Type: text/html; charset=UTF-8\n" +
+ "\n" +
+ "<div dir=\"ltr\">Contains unwanted attachment</div>" +
+ "\n" +
+ "--001a114e019a569625054062708d--\n" +
+ "--001a114e019a56962d054062708f\n" +
+ "Content-Type: text/plain; charset=US-ASCII; name=\"test.txt\"\n" +
+ "Content-Disposition: attachment; filename=\"test.txt\"\n" +
+ "Content-Transfer-Encoding: base64\n" +
+ "X-Attachment-Id: f_iv264bt50\n" +
+ "\n" +
+ "VEVTVAo=\n" +
+ "--001a114e019a56962d054062708f--";
+
+ @Override
+ public String raw() {
+ return raw;
+ }
+
+ @Override
+ public int[] rawChars() {
+ return null;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ System.out.println("\uD83D\uDE1B test");
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w" +
+ "@mail.gmail.com>")
+ .from(new Address("Patrick Hiesel", "hiesel@google.com"))
+ .addTo(new Address("Patrick Hiesel", "hiesel@google.com"))
+ .textContent("Contains unwanted attachment")
+ .htmlContent("<div dir=\"ltr\">Contains unwanted attachment</div>")
+ .subject("Test Subject")
+ .addAdditionalHeader("MIME-Version: 1.0")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
new file mode 100644
index 0000000..5511e75
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
@@ -0,0 +1,64 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests parsing a Base64 encoded subject.
+ */
+@Ignore
+public class Base64HeaderMessage extends RawMailMessage {
+ private static String textContent = "Some Text";
+ private static String raw = "" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" +
+ "Subject: =?UTF-8?B?8J+YmyB0ZXN0?=\n" +
+ "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" +
+ "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" +
+ "To: ekempin <ekempin@google.com>\n" +
+ "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" +
+ "\n" + textContent;
+
+ @Override
+ public String raw() {
+ return raw;
+ }
+
+ @Override
+ public int[] rawChars() {
+ return null;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
+ .from(new Address("Jonathan Nieder (Gerrit)",
+ "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
+ .addTo(new Address("ekempin","ekempin@google.com"))
+ .textContent(textContent)
+ .subject("\uD83D\uDE1B test")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
new file mode 100644
index 0000000..2ed096e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
@@ -0,0 +1,105 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests a message containing mime/alternative (text + html) content.
+ */
+@Ignore
+public class HtmlMimeMessage extends RawMailMessage {
+ private static String textContent = "Simple test";
+
+ // htmlContent is encoded in quoted-printable
+ private static String htmlContent = "<div dir=3D\"ltr\">Test <span style" +
+ "=3D\"background-color:rgb(255,255,0)\">Messa=\n" +
+ "ge</span> in <u>HTML=C2=A0</u><a href=3D\"https://en.wikipedia.org/" +
+ "wiki/%C3%=\n9Cmlaut_(band)\" class=3D\"gmail-mw-redirect\" title=3D\"" +
+ "=C3=9Cmlaut (band)\" st=\nyle=3D\"text-decoration:none;color:rgb(11," +
+ "0,128);background-image:none;backg=\nround-position:initial;background" +
+ "-size:initial;background-repeat:initial;ba=\nckground-origin:initial;" +
+ "background-clip:initial;font-family:sans-serif;font=\n" +
+ "-size:14px\">=C3=9C</a></div>";
+
+ private static String unencodedHtmlContent = "" +
+ "<div dir=\"ltr\">Test <span style=\"background-color:rgb(255,255,0)\">" +
+ "Message</span> in <u>HTML </u><a href=\"https://en.wikipedia.org/wiki/" +
+ "%C3%9Cmlaut_(band)\" class=\"gmail-mw-redirect\" title=\"Ümlaut " +
+ "(band)\" style=\"text-decoration:none;color:rgb(11,0,128);" +
+ "background-image:none;background-position:initial;background-size:" +
+ "initial;background-repeat:initial;background-origin:initial;background" +
+ "-clip:initial;font-family:sans-serif;font-size:14px\">Ü</a></div>";
+
+ private static String raw = "" +
+ "MIME-Version: 1.0\n" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "Message-ID: <001a114cd8be55b4ab053face5cd@google.com>\n" +
+ "Subject: Change in gerrit[master]: Implement receiver class structure " +
+ "and bindings\n" +
+ "From: \"ekempin (Gerrit)\" <noreply-gerritcodereview-qUgXfQecoDLHwp0Ml" +
+ "dAzig@google.com>\n" +
+ "To: Patrick Hiesel <hiesel@google.com>\n" +
+ "Cc: ekempin <ekempin@google.com>\n" +
+ "Content-Type: multipart/alternative; boundary=001a114cd8b" +
+ "e55b486053face5ca\n" +
+ "\n" +
+ "--001a114cd8be55b486053face5ca\n" +
+ "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" +
+ "\n" +
+ textContent +
+ "\n" +
+ "--001a114cd8be55b486053face5ca\n" +
+ "Content-Type: text/html; charset=UTF-8\n" +
+ "Content-Transfer-Encoding: quoted-printable\n" +
+ "\n" +
+ htmlContent +
+ "\n" +
+ "--001a114cd8be55b486053face5ca--";
+
+ @Override
+ public String raw() {
+ return raw;
+ }
+
+ @Override
+ public int[] rawChars() {
+ return null;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<001a114cd8be55b4ab053face5cd@google.com>")
+ .from(new Address("ekempin (Gerrit)",
+ "noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com"))
+ .addCc(new Address("ekempin","ekempin@google.com"))
+ .addTo(new Address("Patrick Hiesel","hiesel@google.com"))
+ .textContent(textContent)
+ .htmlContent(unencodedHtmlContent)
+ .subject("Change in gerrit[master]: Implement " +
+ "receiver class structure and bindings")
+ .addAdditionalHeader("MIME-Version: 1.0")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
new file mode 100644
index 0000000..1472049
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
@@ -0,0 +1,68 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests that non-UTF8 encodings are handled correctly.
+ */
+@Ignore
+public class NonUTF8Message extends RawMailMessage {
+ private static String textContent = "Some Text";
+ private static String raw = "" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" +
+ "Subject: =?UTF-8?B?8J+YmyB0ZXN0?=\n" +
+ "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" +
+ "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" +
+ "To: ekempin <ekempin@google.com>\n" +
+ "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" +
+ "\n" + textContent;
+
+ @Override
+ public String raw() {
+ return null;
+ }
+
+ @Override
+ public int[] rawChars() {
+ int[] arr = new int[raw.length()];
+ int i = 0;
+ for (char c : raw.toCharArray()) {
+ arr[i++] = c;
+ }
+ return arr;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
+ .from(new Address("Jonathan Nieder (Gerrit)",
+ "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
+ .addTo(new Address("ekempin","ekempin@google.com"))
+ .textContent(textContent)
+ .subject("\uD83D\uDE1B test")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
new file mode 100644
index 0000000..f694447
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
@@ -0,0 +1,65 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests parsing a quoted printable encoded subject
+ */
+@Ignore
+public class QuotedPrintableHeaderMessage extends RawMailMessage {
+ private static String textContent = "Some Text";
+ private static String raw = "" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" +
+ "Subject: =?UTF-8?Q?=C3=A2me vulgaire?=\n" +
+ "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" +
+ "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" +
+ "To: ekempin <ekempin@google.com>\n" +
+ "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" +
+ "\n" + textContent;
+
+ @Override
+ public String raw() {
+ return raw;
+ }
+
+ @Override
+ public int[] rawChars() {
+ return null;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ System.out.println("\uD83D\uDE1B test");
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
+ .from(new Address("Jonathan Nieder (Gerrit)",
+ "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
+ .addTo(new Address("ekempin","ekempin@google.com"))
+ .textContent(textContent)
+ .subject("âme vulgaire")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.java
new file mode 100644
index 0000000..8afa8cc
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.java
@@ -0,0 +1,31 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.junit.Ignore;
+
+/**
+ * Base class for all email parsing tests.
+ */
+@Ignore
+public abstract class RawMailMessage {
+ // Raw content to feed the parser
+ public abstract String raw();
+ public abstract int[] rawChars();
+ // Parsed representation for asserting the expected parser output
+ public abstract MailMessage expectedMailMessage();
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
new file mode 100644
index 0000000..179c514
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
@@ -0,0 +1,136 @@
+// 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.mail.receive.data;
+
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.receive.MailMessage;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Ignore;
+
+/**
+ * Tests parsing a simple text message with different headers.
+ */
+@Ignore
+public class SimpleTextMessage extends RawMailMessage {
+ private static String textContent = "" +
+ "Jonathan Nieder has posted comments on this change. ( \n" +
+ "https://gerrit-review.googlesource.com/90018 )\n" +
+ "\n" +
+ "Change subject: (Re)enable voting buttons for merged changes\n" +
+ "...........................................................\n" +
+ "\n" +
+ "\n" +
+ "Patch Set 2:\n" +
+ "\n" +
+ "This is producing NPEs server-side and 500s for the client. \n" +
+ "when I try to load this change:\n" +
+ "\n" +
+ " Error in GET /changes/90018/detail?O=10004\n" +
+ " com.google.gwtorm.OrmException: java.lang.NullPointerException\n" +
+ "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:303)\n" +
+ "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:285)\n" +
+ "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:263)\n" +
+ "\tat com.google.gerrit.change.GetChange.apply(GetChange.java:50)\n" +
+ "\tat com.google.gerrit.change.GetDetail.apply(GetDetail.java:51)\n" +
+ "\tat com.google.gerrit.change.GetDetail.apply(GetDetail.java:26)\n" +
+ "\tat \n" +
+ "com.google.gerrit.RestApiServlet.service(RestApiServlet.java:367)\n" +
+ "\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:717)\n" +
+ "[...]\n" +
+ " Caused by: java.lang.NullPointerException\n" +
+ "\tat \n" +
+ "com.google.gerrit.ChangeJson.setLabelScores(ChangeJson.java:670)\n" +
+ "\tat \n" +
+ "com.google.gerrit.ChangeJson.labelsFor(ChangeJson.java:845)\n" +
+ "\tat \n" +
+ "com.google.gerrit.change.ChangeJson.labelsFor(ChangeJson.java:598)\n" +
+ "\tat \n" +
+ "com.google.gerrit.change.ChangeJson.toChange(ChangeJson.java:499)\n" +
+ "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:294)\n" +
+ "\t... 105 more\n" +
+ "-- \n" +
+ "To view, visit https://gerrit-review.googlesource.com/90018\n" +
+ "To unsubscribe, visit https://gerrit-review.googlesource.com\n" +
+ "\n" +
+ "Gerrit-MessageType: comment\n" +
+ "Gerrit-Change-Id: Iba501e00bee77be3bd0ced72f88fd04ba0accaed\n" +
+ "Gerrit-PatchSet: 2\n" +
+ "Gerrit-Project: gerrit\n" +
+ "Gerrit-Branch: master\n" +
+ "Gerrit-Owner: ekempin <ekempin@google.com>\n" +
+ "Gerrit-Reviewer: Dave Borowitz <dborowitz@google.com>\n" +
+ "Gerrit-Reviewer: Edwin Kempin <ekempin@google.com>\n" +
+ "Gerrit-Reviewer: GerritForge CI <gerritforge@gmail.com>\n" +
+ "Gerrit-Reviewer: Jonathan Nieder <jrn@google.com>\n" +
+ "Gerrit-Reviewer: Patrick Hiesel <hiesel@google.com>\n" +
+ "Gerrit-Reviewer: ekempin <ekempin@google.com>\n" +
+ "Gerrit-HasComments: No";
+
+ private static String raw = "" +
+ "Authentication-Results: mx.google.com; dkim=pass header.i=" +
+ "@google.com;\n" +
+ "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" +
+ "In-Reply-To: <gerrit.1477487889000.Iba501e00bee77be3bd0ced" +
+ "72f88fd04ba0accaed@gerrit-review.googlesource.com>\n" +
+ "References: <gerrit.1477487889000.Iba501e00bee77be3bd0ced72f8" +
+ "8fd04ba0accaed@gerrit-review.googlesource.com>\n" +
+ "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" +
+ "Subject: Change in gerrit[master]: (Re)enable voting buttons for " +
+ "merged changes\n" +
+ "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-CtTy0" +
+ "igsBrnvL7dKoWEIEg@google.com>\n" +
+ "To: ekempin <ekempin@google.com>\n" +
+ "Cc: Dave Borowitz <dborowitz@google.com>, Jonathan Nieder " +
+ "<jrn@google.com>, Patrick Hiesel <hiesel@google.com>\n" +
+ "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" +
+ "\n" + textContent;
+
+ @Override
+ public String raw() {
+ return raw;
+ }
+
+ @Override
+ public int[] rawChars() {
+ return null;
+ }
+
+ @Override
+ public MailMessage expectedMailMessage() {
+ MailMessage.Builder expect = MailMessage.builder();
+ expect
+ .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
+ .from(new Address("Jonathan Nieder (Gerrit)",
+ "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
+ .addTo(new Address("ekempin","ekempin@google.com"))
+ .addCc(new Address("Dave Borowitz", "dborowitz@google.com"))
+ .addCc(new Address("Jonathan Nieder", "jrn@google.com"))
+ .addCc(new Address("Patrick Hiesel", "hiesel@google.com"))
+ .textContent(textContent)
+ .subject("Change in gerrit[master]: (Re)enable voting"
+ + " buttons for merged changes")
+ .dateReceived(
+ new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC))
+ .addAdditionalHeader("Authentication-Results: mx.google.com; " +
+ "dkim=pass header.i=@google.com;")
+ .addAdditionalHeader("In-Reply-To: <gerrit.1477487889000.Iba501e00bee" +
+ "77be3bd0ced72f88fd04ba0accaed@gerrit-review.googlesource.com>")
+ .addAdditionalHeader("References: <gerrit.1477487889000.Iba501e00bee" +
+ "77be3bd0ced72f88fd04ba0accaed@gerrit-review.googlesource.com>");
+ return expect.build();
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1941fd4..3e3ec13 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -94,6 +94,7 @@
import org.junit.Ignore;
import org.junit.Test;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -970,15 +971,22 @@
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- // Queried by AgePredicate constructor.
+ long startMs = TestTimeUtil.START.getMillis();
+ Change change1 =
+ insert(repo, newChange(repo), null, new Timestamp(startMs));
+ Change change2 = insert(
+ repo, newChange(repo), null,
+ new Timestamp(startMs + thirtyHoursInMs));
+
+ // Stop time so age queries use the same endpoint.
TestTimeUtil.setClockStep(0, MILLISECONDS);
- long now = TimeUtil.nowMs();
+ TestTimeUtil.setClock(new Timestamp(startMs + 2 * thirtyHoursInMs));
+ long nowMs = TimeUtil.nowMs();
+
assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1))
.isEqualTo(thirtyHoursInMs);
- assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs);
- assertThat(TimeUtil.nowMs()).isEqualTo(now);
+ assertThat(nowMs - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs);
+ assertThat(TimeUtil.nowMs()).isEqualTo(nowMs);
assertQuery("-age:1d");
assertQuery("-age:" + (30 * 60 - 1) + "m");
@@ -991,10 +999,14 @@
@Test
public void byBefore() throws Exception {
- resetTimeWithClockStep(30, HOURS);
+ long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
+ resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ long startMs = TestTimeUtil.START.getMillis();
+ Change change1 =
+ insert(repo, newChange(repo), null, new Timestamp(startMs));
+ Change change2 = insert(
+ repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
assertQuery("before:2009-09-29");
@@ -1011,10 +1023,14 @@
@Test
public void byAfter() throws Exception {
- resetTimeWithClockStep(30, HOURS);
+ long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
+ resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ long startMs = TestTimeUtil.START.getMillis();
+ Change change1 =
+ insert(repo, newChange(repo), null, new Timestamp(startMs));
+ Change change2 = insert(
+ repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
assertQuery("after:2009-10-03");
@@ -1607,17 +1623,21 @@
}
protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception {
- return insert(repo, ins, null);
+ return insert(repo, ins, null, TimeUtil.nowTs());
}
protected Change insert(TestRepository<Repo> repo, ChangeInserter ins,
@Nullable Account.Id owner) throws Exception {
+ return insert(repo, ins, owner, TimeUtil.nowTs());
+ }
+
+ protected Change insert(TestRepository<Repo> repo, ChangeInserter ins,
+ @Nullable Account.Id owner, Timestamp createdOn) throws Exception {
Project.NameKey project = new Project.NameKey(
repo.getRepository().getDescription().getRepositoryName());
Account.Id ownerId = owner != null ? owner : userId;
IdentifiedUser user = userFactory.create(ownerId);
- try (BatchUpdate bu =
- updateFactory.create(db, project, user, TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(db, project, user, createdOn)) {
bu.insertChange(ins);
bu.execute();
return ins.getChange();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
index efb2b19..32f0af8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
@@ -28,6 +28,9 @@
/** Static utility methods for dealing with dates and times in tests. */
public class TestTimeUtil {
+ public static final DateTime START =
+ new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4));
+
private static Long clockStepMs;
private static AtomicLong clockMs;
@@ -42,9 +45,7 @@
public static synchronized void resetWithClockStep(
long clockStep, TimeUnit clockStepUnit) {
// Set an arbitrary start point so tests are more repeatable.
- clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4))
- .getMillis());
+ clockMs = new AtomicLong(START.getMillis());
setClockStep(clockStep, clockStepUnit);
}
diff --git a/gerrit-util-http/BUILD b/gerrit-util-http/BUILD
index 0e3ac0e..c5d72ba 100644
--- a/gerrit-util-http/BUILD
+++ b/gerrit-util-http/BUILD
@@ -20,6 +20,7 @@
'//lib/jgit/org.eclipse.jgit:jgit',
],
visibility = ['//visibility:public'],
+ testonly = 1,
)
junit_tests(
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 25aa5bc..6c46acd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -53,6 +53,7 @@
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.server.mail.send.SmtpEmailSender;
import com.google.gerrit.server.mime.MimeUtil2Module;
import com.google.gerrit.server.notedb.ConfigNotesMigration;
@@ -310,6 +311,7 @@
modules.add(new SearchingChangeCacheImpl.Module());
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultCacheFactory.Module());
+ modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PluginRestApiModule());
diff --git a/lib/greenmail/BUCK b/lib/greenmail/BUCK
new file mode 100644
index 0000000..9a49c4d
--- /dev/null
+++ b/lib/greenmail/BUCK
@@ -0,0 +1,21 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '1.5.2'
+
+java_library(
+ name = 'greenmail',
+ exported_deps = [
+ ':greenmail_library',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'greenmail_library',
+ id = 'com.icegreen:greenmail:' + VERSION,
+ sha1 = '6b4862a09f8642da58c109117b24ccc19a4a6d39',
+ license = 'Apache2.0',
+ exclude_java_sources = True,
+ visibility = ['PUBLIC'],
+)
+
diff --git a/lib/greenmail/BUILD b/lib/greenmail/BUILD
new file mode 100644
index 0000000..9dd45bc
--- /dev/null
+++ b/lib/greenmail/BUILD
@@ -0,0 +1,7 @@
+package(default_visibility = ['//visibility:public'])
+java_library(
+ name = 'greenmail',
+ exports = ['@greenmail//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 74338de..3f463df 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -9,7 +9,7 @@
license = 'jgit',
repository = REPO,
unsign = True,
- deps = [':ewah'],
+ deps = [':javaewah'],
exclude = [
'META-INF/eclipse.inf',
'about.html',
@@ -18,7 +18,7 @@
)
maven_jar(
- name = 'ewah',
+ name = 'javaewah',
id = 'com.googlecode.javaewah:JavaEWAH:0.7.9',
sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a',
license = 'Apache2.0',
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index bfebb7e..ac8db97 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -3,14 +3,14 @@
java_library(
name = 'jgit-signed',
exports = ['@jgit//jar'],
- runtime_deps = [':ewah'],
+ runtime_deps = [':javaewah'],
visibility = ['//visibility:public'],
data = ['//lib:LICENSE-jgit'],
)
java_library(
- name = 'ewah',
- exports = ['@ewah//jar'],
+ name = 'javaewah',
+ exports = ['@javaewah//jar'],
visibility = ['//visibility:public'],
data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/mail/BUCK b/lib/mail/BUCK
new file mode 100644
index 0000000..07c78d8
--- /dev/null
+++ b/lib/mail/BUCK
@@ -0,0 +1,21 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '1.5.6'
+
+java_library(
+ name = 'mail',
+ exported_deps = [
+ ':mail_library',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'mail_library',
+ id = 'com.sun.mail:javax.mail:' + VERSION,
+ sha1 = 'ab5daef2f881c42c8e280cbe918ec4d7fdfd7efe',
+ license = 'DO_NOT_DISTRIBUTE',
+ exclude_java_sources = True,
+ visibility = ['PUBLIC'],
+)
+
diff --git a/lib/mail/BUILD b/lib/mail/BUILD
new file mode 100644
index 0000000..40dd302
--- /dev/null
+++ b/lib/mail/BUILD
@@ -0,0 +1,6 @@
+java_library(
+ name = 'mail',
+ exports = ['@mail//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
+)
diff --git a/lib/mime4j/BUCK b/lib/mime4j/BUCK
new file mode 100644
index 0000000..13fc42a
--- /dev/null
+++ b/lib/mime4j/BUCK
@@ -0,0 +1,35 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '0.8.0'
+
+java_library(
+ name = 'core',
+ exported_deps = [
+ ':core_library',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'core_library',
+ id = 'org.apache.james:apache-mime4j-core:' + VERSION,
+ sha1 = 'd54f45fca44a2f210569656b4ca3574b42911c95',
+ license = 'Apache2.0',
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'dom',
+ exported_deps = [
+ ':dom_library',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'dom_library',
+ id = 'org.apache.james:apache-mime4j-dom:' + VERSION,
+ sha1 = '6720c93d14225c3e12c4a69768a0370c80e376a3',
+ license = 'Apache2.0',
+ visibility = ['PUBLIC'],
+)
diff --git a/lib/mime4j/BUILD b/lib/mime4j/BUILD
new file mode 100644
index 0000000..e7b85ef
--- /dev/null
+++ b/lib/mime4j/BUILD
@@ -0,0 +1,13 @@
+java_library(
+ name = "core",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = ["@mime4j_core//jar"],
+)
+
+java_library(
+ name = "dom",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = ["@mime4j_dom//jar"],
+)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 1e548d5..28d028b 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -111,6 +111,14 @@
Then visit http://localhost:8081/elements/foo/bar_test.html
+## Running tests (bazel)
+
+Run
+
+```sh
+WCT_ARGS='--some-flag' sh polygerrit-ui/app/run_test.sh
+```
+
## Style guide
We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index b6b74b7..d390b58 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -2,17 +2,10 @@
default_visibility = ["//visibility:public"])
load('//tools/bzl:genrule2.bzl', 'genrule2')
-load("//tools/bzl:js.bzl", "bower_component_bundle", "vulcanize")
-
-bower_component_bundle(
- name = 'test_components',
- deps = [
- '//polygerrit-ui:polygerrit_components',
- '//lib/js:iron-test-helpers',
- '//lib/js:test-fixture',
- '//lib/js:web-component-tester',
- ],
-)
+load(
+ "//tools/bzl:js.bzl",
+ "bower_component_bundle", "vulcanize",
+ "bower_component", "js_component")
vulcanize(
name = "gr-app",
@@ -30,10 +23,10 @@
filegroup(
name = "top_sources",
- srcs = glob([
+ srcs = [
'favicon.ico',
'index.html',
- ]),
+ ],
)
filegroup(
@@ -67,3 +60,46 @@
],
outs = [ "polygerrit_ui.zip" ],
)
+
+bower_component_bundle(
+ name = 'test_components',
+ deps = [
+ '//polygerrit-ui:polygerrit_components',
+ '//lib/js:iron-test-helpers',
+ '//lib/js:test-fixture',
+ '//lib/js:web-component-tester',
+ ],
+)
+
+filegroup(
+ name = "pg_code",
+ srcs = glob([
+ '**/*.html',
+ '**/*.js',
+ ], exclude = [
+ 'bower_components/**',
+ ])
+)
+
+genrule2(
+ name = "pg_code_zip",
+ outs = [ "pg_code.zip", ],
+ srcs = [ ":pg_code" ],
+ cmd = " && ".join([
+ ("tar -cf- --mtime=1980-01-01\\ 00:00 $(locations :pg_code) |"
+ + " tar --strip-components=2 -C $$TMP/ -xf-"),
+ "cd $$TMP",
+ "zip -rq $$ROOT/$@ *"])
+)
+
+sh_test(
+ name = "wct_test",
+ srcs = [ "wct_test.sh" ],
+ data = [
+ ":pg_code.zip",
+ ":test_components.zip",
+ "test/index.html",
+ ],
+ # Should not run sandboxed.
+ tags = ["local", "manual"],
+)
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 96c6193..30e9e86 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -112,6 +112,19 @@
on-confirm="_handleAbandonDialogConfirm"
on-cancel="_handleConfirmDialogCancel"
hidden></gr-confirm-abandon-dialog>
+ <gr-confirm-dialog
+ id="confirmDeleteDialog"
+ class="confirmDialog"
+ confirm-label="Delete"
+ on-cancel="_handleConfirmDialogCancel"
+ on-confirm="_handleDeleteConfirm">
+ <div class="header">
+ Delete Change
+ </div>
+ <div class="main">
+ Do you really want to delete the change?
+ </div>
+ </gr-confirm-dialog>
</gr-overlay>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 9fdc2a9..ef2a3b4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -336,6 +336,8 @@
var type = el.getAttribute('data-action-type');
if (type === ActionType.REVISION) {
this._handleRevisionAction(key);
+ } else if (key === ChangeActions.DELETE) {
+ this._showActionDialog(this.$.confirmDeleteDialog);
} else if (key === ChangeActions.REVERT) {
this.showRevertDialog();
} else if (key === ChangeActions.ABANDON) {
@@ -441,6 +443,10 @@
{message: el.message});
},
+ _handleDeleteConfirm: function() {
+ this._fireAction('/', this.actions[ChangeActions.DELETE], false);
+ },
+
_setLoadingOnButtonWithKey: function(key) {
var buttonEl = this.$$('[data-action-key="' + key + '"]');
buttonEl.setAttribute('loading', true);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index c180f46..00e61d9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -434,5 +434,59 @@
populateRevertMsgStub.restore();
});
});
+
+ suite('delete change', function() {
+ var fireActionStub;
+ var deleteAction;
+
+ var tapDeleteAction = function() {
+ var deleteButton = element.$$('gr-button[data-action-key=\'/\']');
+ MockInteractions.tap(deleteButton);
+ flushAsynchronousOperations();
+ };
+
+ setup(function() {
+ fireActionStub = sinon.stub(element, '_fireAction');
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ deleteAction = {
+ method: 'DELETE',
+ label: 'Delete Change',
+ title: 'Delete change X_X',
+ enabled: true,
+ };
+ element.actions = {
+ '/': deleteAction,
+ };
+ });
+
+ teardown(function() {
+ fireActionStub.restore();
+ });
+
+ test('does not delete on action', function() {
+ tapDeleteAction();
+ assert.isFalse(fireActionStub.called);
+ });
+
+ test('shows confirm dialog', function() {
+ tapDeleteAction();
+ assert.isFalse(element.$$('#confirmDeleteDialog').hidden);
+ MockInteractions.tap(
+ element.$$('#confirmDeleteDialog').$$('gr-button[primary]'));
+ flushAsynchronousOperations();
+ assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
+ });
+
+ test('hides delete confirm on cancel', function() {
+ tapDeleteAction();
+ MockInteractions.tap(
+ element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.$$('#confirmDeleteDialog').hidden);
+ assert.isFalse(fireActionStub.called);
+ });
+ });
});
</script>
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 6eeb6df..1c703fd 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
@@ -99,7 +99,7 @@
}
.commitMessage {
font-family: var(--monospace-font-family);
- flex: 0 0 72ch;
+ flex: 1 0 72ch;
margin-right: 2em;
margin-bottom: 1em;
}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index cac45c6..c066b17 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -60,14 +60,18 @@
this._getSubmittedTogether().then(function(response) {
this._submittedTogether = response;
}.bind(this)),
- this._getConflicts().then(function(response) {
- this._conflicts = response;
- }.bind(this)),
this._getCherryPicks().then(function(response) {
this._cherryPicks = response;
}.bind(this)),
];
+ // Get conflicts if change is open and is mergeable.
+ if (this.changeIsOpen(this.change.status) && this.change.mergeable) {
+ promises.push(this._getConflicts().then(function(response) {
+ this._conflicts = response;
+ }.bind(this)));
+ }
+
promises.push(this._getServerConfig().then(function(config) {
if (this.change.topic && !config.change.submit_whole_topic) {
return this._getChangesWithSameTopic().then(function(response) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index f7864ce..21903d2 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -33,9 +33,15 @@
<script>
suite('gr-related-changes-list tests', function() {
var element;
+ var sandbox;
setup(function() {
element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(function() {
+ sandbox.restore();
});
test('connected revisions', function() {
@@ -223,5 +229,64 @@
assert.equal(element._computeChangeContainerClass(
change1, change2).indexOf('thisChange'), -1);
});
+
+ suite('get conflicts tests', function() {
+ var element;
+ var conflictsStub;
+
+ setup(function() {
+ element = fixture('basic');
+
+ sandbox.stub(element, '_getRelatedChanges',
+ function() { return Promise.resolve(); });
+ sandbox.stub(element, '_getSubmittedTogether',
+ function() { return Promise.resolve(); });
+ sandbox.stub(element, '_getCherryPicks',
+ function() { return Promise.resolve(); });
+ conflictsStub = sandbox.stub(element, '_getConflicts',
+ function() { return Promise.resolve(); });
+ });
+
+ test('request conflicts if open and mergeable', function() {
+ element.patchNum = 7;
+ element.change = {
+ status: 'NEW',
+ mergeable: true,
+ };
+ element.reload();
+ assert.isTrue(conflictsStub.called);
+ });
+
+ test('does not request conflicts if closed and mergeable', function() {
+ element.patchNum = 7;
+ element.change = {
+ status: 'MERGED',
+ mergeable: true,
+ };
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('does not request conflicts if open and not mergeable', function() {
+ element.patchNum = 7;
+ element.change = {
+ status: 'NEW',
+ mergeable: false,
+ };
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('does not request conflicts if closed and not mergeable',
+ function() {
+ element.patchNum = 7;
+ element.change = {
+ status: 'MERGED',
+ mergeable: false,
+ };
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 1cca110..44204fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -193,6 +193,10 @@
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
+ case 27: // escape
+ e.preventDefault();
+ this.$.diff.displayLine = false;
+ break;
case 37: // left
if (e.shiftKey) {
e.preventDefault();
@@ -208,11 +212,13 @@
case 40: // down
case 74: // 'j'
e.preventDefault();
+ this.$.diff.displayLine = true;
this.$.cursor.moveDown();
break;
case 38: // up
case 75: // 'k'
e.preventDefault();
+ this.$.diff.displayLine = true;
this.$.cursor.moveUp();
break;
case 67: // 'c'
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 99da821..e423e45 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -131,6 +131,16 @@
'moveToPreviousCommentThread');
MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']); // 'P'
assert(scrollStub.calledOnce);
+
+ var computeContainerClassStub = sandbox.stub(element.$.diff,
+ '_computeContainerClass');
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', true));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 27); // 'escape'
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', false));
});
test('saving diff preferences', function() {
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 855f45a..2b89a7a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -150,6 +150,9 @@
.contextControl td:not(.lineNum) {
text-align: center;
}
+ .displayLine .diff-row.target-row {
+ border-bottom: 1px solid #bbb;
+ }
.br:after {
/* Line feed */
content: '\A';
@@ -164,7 +167,7 @@
}
</style>
<style include="gr-theme-default"></style>
- <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
+ <div class$="[[_computeContainerClass(_loggedIn, viewMode, displayLine)]]"
on-tap="_handleTap">
<gr-diff-selection diff="[[_diff]]">
<gr-diff-highlight
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index eb95a77..4c9d021 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -51,6 +51,10 @@
},
project: String,
commit: String,
+ displayLine: {
+ type: Boolean,
+ value: false,
+ },
isImageDiff: {
type: Boolean,
computed: '_computeIsImageDiff(_diff)',
@@ -176,7 +180,7 @@
return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
},
- _computeContainerClass: function(loggedIn, viewMode) {
+ _computeContainerClass: function(loggedIn, viewMode, displayLine) {
var classes = ['diffContainer'];
switch (viewMode) {
case DiffViewMode.UNIFIED:
@@ -191,6 +195,9 @@
if (loggedIn) {
classes.push('canComment');
}
+ if (displayLine) {
+ classes.push('displayLine');
+ }
return classes.join(' ');
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 7e20a5b..306f283 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -50,6 +50,21 @@
assert.isFalse(element.classList.contains('no-left'));
});
+ test('view does not start with displayLine classList', function() {
+ assert.isFalse(
+ element.$$('.diffContainer').classList.contains('displayLine'));
+ });
+
+ test('displayLine class added called when displayLine is true',
+ function() {
+ var spy = sinon.spy(element, '_computeContainerClass');
+ element.displayLine = true;
+ assert.isTrue(spy.called);
+ assert.isTrue(
+ element.$$('.diffContainer').classList.contains('displayLine'));
+ spy.restore();
+ });
+
test('get drafts', function(done) {
element.patchRange = {basePatchNum: 0, patchNum: 0};
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
new file mode 100644
index 0000000..b557db8
--- /dev/null
+++ b/polygerrit-ui/app/run_test.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+wct_bin=$(which wct)
+if [[ -z "$wct_bin" ]]; then
+ echo "WCT must be on the path."
+ exit 1
+fi
+
+npm_bin=$(which npm)
+if [[ -z "$npm_bin" ]]; then
+ echo "NPM must be on the path."
+ exit 1
+fi
+
+# WCT tests are not hermetic, and need extra environment variables.
+# TODO(hanwen): does $DISPLAY even work on OSX?
+bazel test \
+ --test_env="HOME=$HOME" \
+ --test_env="WCT=${wct_bin}" \
+ --test_env="WCT_ARGS=${WCT_ARGS}" \
+ --test_env="NPM=${npm_bin}" \
+ --test_env="DISPLAY=${DISPLAY}" \
+ "$@" \
+ //polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
new file mode 100755
index 0000000..10de424
--- /dev/null
+++ b/polygerrit-ui/app/wct_test.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+set -ex
+
+t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX)
+components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip
+code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/pg_code.zip
+
+echo $t
+unzip -qd $t $components
+unzip -qd $t $code
+mkdir -p $t/test
+cp $TEST_SRCDIR/gerrit/polygerrit-ui/app/test/index.html $t/test/
+
+# For some reason wct tries to install selenium into its node_modules
+# directory on first run. If you've installed into /usr/local and
+# aren't running wct as root, you're screwed. Turning this option off
+# through skipSeleniumInstall seems to still work, so there's that.
+
+# Sauce tests are disabled by default in order to run local tests
+# only. Run it with (saucelabs.com account required; free for open
+# source): WCT_ARGS='--plugin sauce' buck test --no-results-cache
+# --include web
+
+cat <<EOF > $t/wct.conf.js
+module.exports = {
+ 'suites': ['test'],
+ 'webserver': {
+ 'pathMappings': [
+ {'/components/bower_components': 'bower_components'}
+ ]
+ },
+ 'plugins': {
+ 'local': {
+ 'skipSeleniumInstall': true
+ },
+ 'sauce': {
+ 'disabled': true,
+ 'browsers': [
+ 'OS X 10.11/chrome',
+ 'Windows 10/chrome',
+ 'Linux/firefox',
+ 'OS X 10.11/safari',
+ 'Windows 10/microsoftedge'
+ ]
+ }
+ }
+ };
+EOF
+
+export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+
+cd $t
+test -n "${WCT}"
+
+$(basename ${WCT}) ${WCT_ARGS}
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
index 6f17754..cbc263e 100755
--- a/tools/workspace-status.sh
+++ b/tools/workspace-status.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
# This script will be run by bazel when the build process starts to
# generate key-value information that represents the status of the