Merge "commit-msg: Adapt to awk behavior change on Cygwin/MSYS" into stable-2.14
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index c777327..072c22c 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -95,6 +95,51 @@
   )
 ----
 
+=== Bundle custom plugin in release.war ===
+
+To bundle custom plugin(s) in the link:dev-bazel.html#release[release.war] artifact,
+add them to the CUSTOM_PLUGINS list in `tools/bzl/plugins.bzl`.
+
+Example of `tools/bzl/plugins.bzl` with custom plugin `my-plugin`:
+
+----
+CORE_PLUGINS = [
+    "commit-message-length-validator",
+    "download-commands",
+    "hooks",
+    "replication",
+    "reviewnotes",
+    "singleusergroup",
+]
+
+CUSTOM_PLUGINS = [
+    "my-plugin",
+]
+
+CUSTOM_PLUGINS_TEST_DEPS = [
+    # Add custom core plugins with tests deps here
+]
+----
+
+If the plugin(s) being bundled in the release have external dependencies, include them
+in `plugins/external_plugin_deps`. You should alias `external_plugin_deps()` so it
+can be imported for multiple plugins. For example:
+
+----
+load(":my-plugin/external_plugin_deps.bzl", my_plugin="external_plugin_deps")
+load(":my-other-plugin/external_plugin_deps.bzl", my_other_plugin="external_plugin_deps")
+
+def external_plugin_deps():
+  my_plugin()
+  my_other_plugin()
+----
+
+[NOTE]
+Since `tools/bzl/plugins.bzl` and `plugins/external_plugin_deps.bzl` are part of
+Gerrit's source code and the version of the war is based on the state of the git
+repository that is built; you should commit this change before building, otherwise
+the version will be marked as 'dirty'.
+
 == Bazel standalone driven
 
 Only few plugins support that mode for now:
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 72452b3..e175a7e 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1028,6 +1028,14 @@
     return createGroup(name, "Administrators");
   }
 
+  protected String createGroupWithRealName(String name) throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.ownerId = "Administrators";
+    gApi.groups().create(in);
+    return name;
+  }
+
   protected String createGroup(String name, String owner) throws Exception {
     name = name(name);
     GroupInput in = new GroupInput();
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 5c5d15d..1442bed 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
@@ -62,6 +62,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -1017,6 +1018,98 @@
   }
 
   @Test
+  public void addReviewerThatIsNotPerfectMatch() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+    PushOneCommit.Result r = createChange();
+    ChangeResource rsrc = parseResource(r);
+    String oldETag = rsrc.getETag();
+    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+
+    //create a group named "ab" with one user: testUser
+    TestAccount testUser = accounts.create("abcd", "abcd@test.com", "abcd");
+    String testGroup = createGroupWithRealName("ab");
+    GroupApi groupApi = gApi.groups().id(testGroup);
+    groupApi.description("test group");
+    groupApi.addMembers(user.fullName);
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = "abc";
+    gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(testUser.emailAddress);
+    assertThat(m.body()).contains("Hello " + testUser.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailReplyTo(m, testUser.email);
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+
+    // When NoteDb is enabled adding a reviewer records that user as reviewer
+    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
+    // approval on the change which is treated as CC when the ChangeInfo is
+    // created.
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.getId().get());
+
+    // Ensure ETag and lastUpdatedOn are updated.
+    rsrc = parseResource(r);
+    assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
+    assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
+  }
+
+  @Test
+  public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+    PushOneCommit.Result r = createChange();
+    ChangeResource rsrc = parseResource(r);
+    String oldETag = rsrc.getETag();
+    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+
+    //create a group named "us" with one user: testUser
+    TestAccount testUser = accounts.create("testUser", "testUser@test.com", "testUser");
+    String testGroup =
+        createGroupWithRealName(user.fullName.substring(0, user.fullName.length() / 2));
+    GroupApi groupApi = gApi.groups().id(testGroup);
+    groupApi.description("test group");
+    groupApi.addMembers(testUser.fullName);
+
+    //ensure that user "user" is not in the group
+    groupApi.removeMembers(user.fullName);
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = testGroup;
+    gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(testUser.emailAddress);
+    assertThat(m.body()).contains("Hello " + testUser.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailReplyTo(m, testUser.email);
+    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+
+    // When NoteDb is enabled adding a reviewer records that user as reviewer
+    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
+    // approval on the change which is treated as CC when the ChangeInfo is
+    // created.
+    Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.getId().get());
+
+    // Ensure ETag and lastUpdatedOn are updated.
+    rsrc = parseResource(r);
+    assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
+    assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
+  }
+
+  @Test
   public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists() throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
     assume().that(notesMigration.changePrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
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 7031d51..debd32a 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
@@ -170,25 +170,37 @@
   public Addition prepareApplication(
       ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
       throws OrmException, RestApiException, IOException {
-    Account.Id accountId;
+    IdentifiedUser user = null;
+    boolean accountFound = true;
+    boolean isExactMatch = false;
     try {
-      accountId = accounts.parse(input.reviewer).getAccountId();
+      user = accounts.parse(input.reviewer);
+      if (input.reviewer.equalsIgnoreCase(user.getName())
+          || input.reviewer.equals(String.valueOf(user.getAccountId()))) {
+        isExactMatch = true;
+      }
     } catch (UnprocessableEntityException e) {
-      if (allowGroup) {
-        try {
-          return putGroup(rsrc, input);
-        } catch (UnprocessableEntityException e2) {
+      accountFound = false;
+    }
+
+    if (allowGroup && !isExactMatch) {
+      try {
+        return putGroup(rsrc, input);
+      } catch (UnprocessableEntityException e2) {
+        if (!accountFound) {
           throw new UnprocessableEntityException(
               MessageFormat.format(
                   ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
         }
       }
+    }
+    if (!accountFound) {
       throw new UnprocessableEntityException(
           MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
     }
     return putAccount(
         input.reviewer,
-        reviewerFactory.create(rsrc, accountId),
+        reviewerFactory.create(rsrc, user.getAccountId()),
         input.state(),
         input.notify,
         notifyUtil.resolveAccounts(input.notifyDetails));