Merge "Add asciidoc checks in the documentation makefile"
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index 6685ea1..58e9e02 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -26,13 +26,13 @@
 * link:error-no-new-changes.html[no new changes]
 * link:error-non-fast-forward.html[non-fast forward]
 * link:error-not-a-gerrit-administrator.html[Not a Gerrit administrator]
-* link:error-not-a-gerrit-project.html[not a Gerrit project]
 * link:error-not-permitted-to-create.html[Not permitted to create ...]
 * link:error-not-signed-off-by.html[not Signed-off-by author/committer/uploader in commit message footer]
 * link:error-not-valid-ref.html[not valid ref]
 * link:error-change-upload-blocked.html[One or more refs/for/ names blocks change upload]
 * link:error-permission-denied.html[Permission denied (publickey)]
 * link:error-prohibited-by-gerrit.html[prohibited by Gerrit]
+* link:error-project-not-found.html[Project not found: ...]
 * link:error-squash-commits-first.html[squash commits first]
 * link:error-upload-denied.html[Upload denied for project \'...']
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-project-not-found.txt
similarity index 96%
rename from Documentation/error-not-a-gerrit-project.txt
rename to Documentation/error-project-not-found.txt
index 58919d5..3fc0141 100644
--- a/Documentation/error-not-a-gerrit-project.txt
+++ b/Documentation/error-project-not-found.txt
@@ -1,5 +1,5 @@
-not a Gerrit project
-====================
+Project not found: ...
+======================
 
 With this error message Gerrit rejects to push a commit if the git
 repository to which the push is done does not exist as a project in
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 73f4c90..522cc77 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -427,6 +427,9 @@
 
 The new owner group must be provided in the request body.
 
+The new owner can be specified by name, by group UUID or by the legacy
+numeric group ID.
+
 .Request
 ----
   PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
new file mode 100644
index 0000000..e76687e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+
+import com.google.common.reflect.TypeToken;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetGroupIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testGetGroup() throws IOException {
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+
+    // by UUID
+    testGetGroup("/groups/" + adminGroup.getGroupUUID().get(), adminGroup);
+
+    // by name
+    testGetGroup("/groups/" + adminGroup.getName(), adminGroup);
+
+    // by legacy numeric ID
+    testGetGroup("/groups/" + adminGroup.getId().get(), adminGroup);
+  }
+
+  private void testGetGroup(String url, AccountGroup expectedGroup) throws IOException {
+    RestResponse r = session.get(url);
+    @SuppressWarnings("serial")
+    GroupInfo group = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertGroupInfo(expectedGroup, group);
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java
index f9dba0c..0fba41e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java
@@ -14,23 +14,20 @@
 
 package com.google.gerrit.common;
 
-import org.eclipse.jgit.lib.Constants;
-
 public class ProjectUtil {
   public static String stripGitSuffix(String name) {
-    String nameWithoutSuffix = name;
-
-    if (nameWithoutSuffix.endsWith(Constants.DOT_GIT_EXT)) {
+    if (name.endsWith(".git")) {
       // Be nice and drop the trailing ".git" suffix, which we never keep
       // in our database, but clients might mistakenly provide anyway.
       //
-      nameWithoutSuffix = nameWithoutSuffix.substring(0, //
-          nameWithoutSuffix.length() - Constants.DOT_GIT_EXT.length());
-      while (nameWithoutSuffix.endsWith("/")) {
-        nameWithoutSuffix =
-            nameWithoutSuffix.substring(0, nameWithoutSuffix.length() - 1);
+      name = name.substring(0, name.length() - 4);
+      while (name.endsWith("/")) {
+        name = name.substring(0, name.length() - 1);
       }
     }
-    return nameWithoutSuffix;
+    return name;
+  }
+
+  private ProjectUtil() {
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
index d3988ff..ea20e2e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
@@ -18,13 +18,9 @@
 public class NameAlreadyUsedException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public static final String MESSAGE = "Name Already Used";
-
-  public NameAlreadyUsedException() {
-    super(MESSAGE);
-  }
+  public static final String MESSAGE = "Name Already Used: ";
 
   public NameAlreadyUsedException(String name) {
-    super(MESSAGE + ": " + name);
+    super(MESSAGE + name);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 8a57515..0b57bcb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -44,7 +44,6 @@
 
   String notFoundTitle();
   String notFoundBody();
-  String nameAlreadyUsedBody();
   String noSuchAccountTitle();
 
   String noSuchGroupTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 3eee2be..f7033e4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -27,7 +27,6 @@
 
 notFoundTitle = Not Found
 notFoundBody = The page you requested was not found, or you do not have permission to view this page.
-nameAlreadyUsedBody = The name is already in use.
 noSuchAccountTitle = Code Review - Unknown User
 
 noSuchGroupTitle = Code Review - Unknown Group
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
index 12e7402..8fc196a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
@@ -23,6 +23,7 @@
 
   String noSuchAccountMessage(String who);
   String noSuchGroupMessage(String who);
+  String nameAlreadyUsedBody(String alreadyUsedName);
 
   String branchCreationFailed(String branchName, String error);
   String invalidBranchName(String branchName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 84cf476..41caa44 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -4,6 +4,7 @@
 
 noSuchAccountMessage = {0} is not a registered user.
 noSuchGroupMessage = Group {0} does not exist or is not visible to you.
+nameAlreadyUsedBody = The name {0} is already in use.
 
 branchCreationFailed = Creating branch {0} failed. Error: {1}
 invalidBranchName = The branch name {0} is not valid.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index fe9a834..2ae6005 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -23,8 +23,6 @@
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
@@ -80,6 +78,9 @@
     searchBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
+        if (searchBox.getVisibleLength() == SMALL_SIZE) {
+          sizeAnimation.run(true);
+        }
         if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
           if (!suggestionDisplay.isSuggestionSelected) {
             doSearch();
@@ -87,14 +88,6 @@
         }
       }
     });
-    searchBox.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        if(searchBox.getVisibleLength() == SMALL_SIZE) {
-          sizeAnimation.run(true);
-        }
-      }
-    });
     searchBox.addBlurHandler(new BlurHandler() {
       @Override
       public void onBlur(BlurEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 555d023..7c880a9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -235,20 +236,7 @@
         new GerritCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
-            String nameWithoutSuffix = projectName;
-            if (nameWithoutSuffix.endsWith(".git")) {
-              // Be nice and drop the trailing ".git" suffix, which we never
-              // keep in our database, but clients might mistakenly provide
-              // anyway.
-              //
-              nameWithoutSuffix = nameWithoutSuffix.substring(0, //
-                  nameWithoutSuffix.length() - 4);
-              while (nameWithoutSuffix.endsWith("/")) {
-                nameWithoutSuffix = nameWithoutSuffix.substring(//
-                    0, nameWithoutSuffix.length() - 1);
-              }
-            }
-
+            String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
             History.newItem(Dispatcher.toProjectAdmin(new Project.NameKey(
                 nameWithoutSuffix), ProjectScreen.INFO));
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index eebbd5a..06d1f0b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -54,7 +54,9 @@
       d.center();
 
     } else if (isNameAlreadyUsed(caught)) {
-      new ErrorDialog(Gerrit.C.nameAlreadyUsedBody()).center();
+      final String msg = caught.getMessage();
+      final String alreadyUsedName = msg.substring(NameAlreadyUsedException.MESSAGE.length());
+      new ErrorDialog(Gerrit.M.nameAlreadyUsedBody(alreadyUsedName)).center();
 
     } else if (isNoSuchGroup(caught)) {
       final String msg = caught.getMessage();
@@ -101,7 +103,7 @@
 
   private static boolean isNameAlreadyUsed(final Throwable caught) {
     return caught instanceof RemoteJsonException
-        && caught.getMessage().equals(NameAlreadyUsedException.MESSAGE);
+        && caught.getMessage().startsWith(NameAlreadyUsedException.MESSAGE);
   }
 
   private static boolean isNoSuchGroup(final Throwable caught) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 79b05c2..8bd76ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -294,6 +294,10 @@
 
   private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard,
       boolean detailed) throws OrmException {
+    if (!standard && !detailed) {
+      return null;
+    }
+
     ChangeControl ctl = control(cd);
     if (ctl == null) {
       return Collections.emptyMap();
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 a7e5254..a121248 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
@@ -77,7 +77,7 @@
         new Predicate<PatchSetApproval>() {
           @Override
           public boolean apply(PatchSetApproval input) {
-            return input.getAccountId().equals(rsrc.getAccount().getId());
+            return input.getAccountId().equals(rsrc.getUser().getAccountId());
           }
         });
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
index 28f397c..8c41be8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -27,7 +27,7 @@
   }
 
   @Override
-  public Object apply(ReviewerResource reviewerResource) throws OrmException {
-    return json.format(reviewerResource);
+  public Object apply(ReviewerResource rsrc) throws OrmException {
+    return json.format(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 1c68e94..48c054a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -32,20 +32,23 @@
   private final AccountCache accountCache;
   private final Provider<ReviewDb> dbProvider;
   private final ReviewerJson json;
+  private final ReviewerResource.Factory resourceFactory;
 
   @Inject
   ListReviewers(AccountCache accountCache,
       Provider<ReviewDb> dbProvider,
+      ReviewerResource.Factory resourceFactory,
       ReviewerJson json) {
     this.accountCache = accountCache;
     this.dbProvider = dbProvider;
+    this.resourceFactory = resourceFactory;
     this.json = json;
   }
 
   @Override
   public Object apply(ChangeResource rsrc) throws BadRequestException,
       OrmException {
-    Map<Account.Id, Object> reviewers = Maps.newLinkedHashMap();
+    Map<Account.Id, ReviewerResource> reviewers = Maps.newLinkedHashMap();
     ReviewDb db = dbProvider.get();
     Change.Id changeId = rsrc.getChange().getId();
     for (PatchSetApproval patchSetApproval
@@ -53,10 +56,9 @@
       Account.Id accountId = patchSetApproval.getAccountId();
       if (!reviewers.containsKey(accountId)) {
         Account account = accountCache.get(accountId).getAccount();
-        reviewers.put(accountId,
-                      json.format(new ReviewerResource(rsrc, account)));
+        reviewers.put(accountId, resourceFactory.create(rsrc, account));
       }
     }
-    return reviewers.values();
+    return json.format(reviewers.values());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 6586725..b00b3d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -71,6 +71,7 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
+        factory(ReviewerResource.Factory.class);
         factory(AccountInfo.Loader.Factory.class);
         factory(EmailReviewComments.Factory.class);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index d474386..9479e67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -14,25 +14,104 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.workflow.CategoryFunction;
+import com.google.gerrit.server.workflow.FunctionState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 
 public class ReviewerJson {
-  ReviewerJson() {
+  private final Provider<ReviewDb> db;
+  private final ApprovalTypes approvalTypes;
+  private final FunctionState.Factory functionState;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+
+  @Inject
+  ReviewerJson(Provider<ReviewDb> db,
+      ApprovalTypes approvalTypes,
+      FunctionState.Factory functionState,
+      AccountInfo.Loader.Factory accountLoaderFactory) {
+    this.db = db;
+    this.approvalTypes = approvalTypes;
+    this.functionState = functionState;
+    this.accountLoaderFactory = accountLoaderFactory;
   }
 
-  public ReviewerInfo format(ReviewerResource reviewerResource) {
-    ReviewerInfo reviewerInfo = new ReviewerInfo();
-    Account account = reviewerResource.getAccount();
-    reviewerInfo.id = account.getId().toString();
-    reviewerInfo.email = account.getPreferredEmail();
-    reviewerInfo.name = account.getFullName();
-    return reviewerInfo;
+  public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs) throws OrmException {
+    List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
+    AccountInfo.Loader loader = accountLoaderFactory.create(true);
+    for (ReviewerResource rsrc : rsrcs) {
+      ReviewerInfo info = formatOne(rsrc);
+      loader.put(info);
+      infos.add(info);
+    }
+    loader.fill();
+    return infos;
   }
 
-  public static class ReviewerInfo {
+  public List<ReviewerInfo> format(ReviewerResource rsrc) throws OrmException {
+    return format(ImmutableList.<ReviewerResource> of(rsrc));
+  }
+
+  private ReviewerInfo formatOne(ReviewerResource rsrc) throws OrmException {
+    Account.Id id = rsrc.getUser().getAccountId();
+    ReviewerInfo out = new ReviewerInfo(id);
+
+    Change change = rsrc.getChange();
+    PatchSet.Id psId = change.currentPatchSetId();
+
+    List<PatchSetApproval> approvals = db.get().patchSetApprovals()
+        .byPatchSetUser(psId, id).toList();
+
+    ChangeControl control = rsrc.getControl().forUser(rsrc.getUser());
+    FunctionState fs = functionState.create(control, psId, approvals);
+    for (ApprovalType at : approvalTypes.getApprovalTypes()) {
+      CategoryFunction.forCategory(at.getCategory()).run(at, fs);
+    }
+
+    out.approvals = Maps.newHashMapWithExpectedSize(approvals.size());
+    for (PatchSetApproval ca : approvals) {
+      for (PermissionRange pr : control.getLabelRanges()) {
+        if (pr.getMin() != 0 || pr.getMax() != 0) {
+          // TODO: Support arbitrary labels.
+          ApprovalType at = approvalTypes.byId(ca.getCategoryId());
+          if (at != null) {
+            out.approvals.put(at.getCategory().getLabelName(),
+                ApprovalCategoryValue.formatValue(ca.getValue()));
+          }
+        }
+      }
+    }
+    if (out.approvals.isEmpty()) {
+      out.approvals = null;
+    }
+
+    return out;
+  }
+
+  public static class ReviewerInfo extends AccountInfo {
     final String kind = "gerritcodereview#reviewer";
-    String id;
-    String email;
-    String name;
+    Map<String, String> approvals;
+
+    protected ReviewerInfo(Account.Id id) {
+      super(id);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
index e93c175..23fba47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -16,20 +16,37 @@
 
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 public class ReviewerResource extends ChangeResource {
   public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
       new TypeLiteral<RestView<ReviewerResource>>() {};
 
-  private final Account account;
-
-  public ReviewerResource(ChangeResource changeResource, Account account) {
-    super(changeResource);
-    this.account = account;
+  static interface Factory {
+    ReviewerResource create(ChangeResource rsrc, IdentifiedUser user);
+    ReviewerResource create(ChangeResource rsrc, Account account);
   }
 
-  public Account getAccount() {
-    return account;
+  private final IdentifiedUser user;
+
+  @AssistedInject
+  ReviewerResource(@Assisted ChangeResource rsrc,
+      @Assisted IdentifiedUser user) {
+    super(rsrc);
+    this.user = user;
+  }
+
+  @AssistedInject
+  ReviewerResource(IdentifiedUser.GenericFactory userFactory,
+      @Assisted ChangeResource rsrc,
+      @Assisted Account account) {
+    this(rsrc, userFactory.create(account.getId()));
+  }
+
+  public IdentifiedUser getUser() {
+    return user;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index 267f357..e75f54c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -38,15 +40,21 @@
     ChildCollection<ChangeResource, ReviewerResource> {
   private final DynamicMap<RestView<ReviewerResource>> views;
   private final Provider<ReviewDb> dbProvider;
+  private final AccountResolver resolver;
+  private final ReviewerResource.Factory resourceFactory;
   private final AccountCache accountCache;
   private final Provider<ListReviewers> list;
 
   @Inject
   Reviewers(Provider<ReviewDb> dbProvider,
-            DynamicMap<RestView<ReviewerResource>> views,
-            AccountCache accountCache,
-            Provider<ListReviewers> list) {
+      AccountResolver resolver,
+      ReviewerResource.Factory resourceFactory,
+      DynamicMap<RestView<ReviewerResource>> views,
+      AccountCache accountCache,
+      Provider<ListReviewers> list) {
     this.dbProvider = dbProvider;
+    this.resolver = resolver;
+    this.resourceFactory = resourceFactory;
     this.views = views;
     this.accountCache = accountCache;
     this.list = list;
@@ -69,22 +77,24 @@
     if (id.equals("self")) {
       CurrentUser user = rsrc.getControl().getCurrentUser();
       if (user instanceof IdentifiedUser) {
-        accountId = ((IdentifiedUser)user).getAccountId();
+        accountId = ((IdentifiedUser) user).getAccountId();
       } else if (user instanceof AnonymousUser) {
         throw new AuthException("Authentication required");
       } else {
         throw new ResourceNotFoundException(id);
       }
-    } else if (id.get().matches("^[0-9]+$")) {
-      accountId = Account.Id.parse(id.get());
     } else {
-      throw new ResourceNotFoundException(id);
+      Set<Account.Id> matches = resolver.findAll(id.get());
+      if (matches.size() != 1) {
+        throw new ResourceNotFoundException(id);
+      }
+      accountId = Iterables.getOnlyElement(matches);
     }
 
     // See if the id exists as a reviewer for this change
     if (fetchAccountIds(rsrc).contains(accountId)) {
       Account account = accountCache.get(accountId).getAccount();
-      return new ReviewerResource(rsrc, account);
+      return resourceFactory.create(rsrc, account);
     }
     throw new ResourceNotFoundException(id);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index a7dcce7..9ce47a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -15,11 +15,15 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.concurrent.TimeUnit.DAYS;
 
+import com.google.common.base.Objects;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ApprovalType;
@@ -116,6 +120,9 @@
   private static final long LOCK_FAILURE_RETRY_DELAY =
       MILLISECONDS.convert(15, SECONDS);
 
+  private static final long DUPLICATE_MESSAGE_INTERVAL =
+      MILLISECONDS.convert(1, DAYS);
+
   private final GitRepositoryManager repoManager;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ProjectCache projectCache;
@@ -798,25 +805,20 @@
       // dependencies are also submitted. Perhaps the user just
       // forgot to submit those.
       //
-      String txt =
-          "Change could not be merged because of a missing dependency.";
-      if (!isAlreadySent(c, txt)) {
-        StringBuilder m = new StringBuilder();
-        m.append(txt);
-        m.append("\n");
+      StringBuilder m = new StringBuilder();
+      m.append("Change could not be merged because of a missing dependency.");
+      m.append("\n");
 
-        m.append("\n");
+      m.append("\n");
 
-        m.append("The following changes must also be submitted:\n");
+      m.append("The following changes must also be submitted:\n");
+      m.append("\n");
+      for (CodeReviewCommit missingCommit : commit.missing) {
+        m.append("* ");
+        m.append(missingCommit.change.getKey().get());
         m.append("\n");
-        for (CodeReviewCommit missingCommit : commit.missing) {
-          m.append("* ");
-          m.append(missingCommit.change.getKey().get());
-          m.append("\n");
-        }
-        txt = m.toString();
       }
-      capable = new Capable(txt);
+      capable = new Capable(m.toString());
     } else {
       // It is impossible to submit this change as-is. The author
       // needs to rebase it in order to work around the missing
@@ -867,30 +869,6 @@
     }
   }
 
-  private boolean isAlreadySent(final Change c, final String prefix) {
-    try {
-      final List<ChangeMessage> msgList =
-          db.changeMessages().byChange(c.getId()).toList();
-      if (msgList.size() > 0) {
-        final ChangeMessage last = msgList.get(msgList.size() - 1);
-        if (last.getAuthor() == null && last.getMessage().startsWith(prefix)) {
-          // The last message was written by us, and it said this
-          // same message already. Its unlikely anything has changed
-          // that would cause us to need to repeat ourselves.
-          //
-          return true;
-        }
-      }
-
-      // The last message was not sent by us, or doesn't match the text
-      // we are about to send.
-      //
-      return false;
-    } catch (OrmException e) {
-      return true;
-    }
-  }
-
   private ChangeMessage message(final Change c, final String body) {
     final String uuid;
     try {
@@ -1039,8 +1017,31 @@
     sendMergeFail(c, msg, true);
   }
 
+  private boolean isDuplicate(ChangeMessage msg) {
+    try {
+      ChangeMessage last = Iterables.getLast(db.changeMessages().byChange(
+          msg.getPatchSetId().getParentKey()), null);
+      if (last != null) {
+        long lastMs = last.getWrittenOn().getTime();
+        long msgMs = msg.getWrittenOn().getTime();
+        if (Objects.equal(last.getAuthor(), msg.getAuthor())
+            && Objects.equal(last.getMessage(), msg.getMessage())
+            && msgMs - lastMs < DUPLICATE_MESSAGE_INTERVAL) {
+          return true;
+        }
+      }
+    } catch (OrmException err) {
+      log.warn("Cannot check previous merge failure message", err);
+    }
+    return false;
+  }
+
   private void sendMergeFail(final Change c, final ChangeMessage msg,
       final boolean makeNew) {
+    if (isDuplicate(msg)) {
+      return;
+    }
+
     try {
       db.changeMessages().insert(Collections.singleton(msg));
     } catch (OrmException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
index ce6bc25..483cd07 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -25,13 +25,12 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.PutOwner.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import java.util.Collections;
 
@@ -41,15 +40,15 @@
     String owner;
   }
 
-  private final GroupBackend groupBackend;
+  private final Provider<GroupsCollection> groupsCollection;
   private final GroupCache groupCache;
   private final GroupControl.Factory controlFactory;
   private final ReviewDb db;
 
   @Inject
-  PutOwner(GroupBackend groupBackend, GroupCache groupCache,
+  PutOwner(Provider<GroupsCollection> groupsCollection, GroupCache groupCache,
       GroupControl.Factory controlFactory, ReviewDb db) {
-    this.groupBackend = groupBackend;
+    this.groupsCollection = groupsCollection;
     this.groupCache = groupCache;
     this.controlFactory = controlFactory;
     this.db = db;
@@ -70,21 +69,22 @@
       throw new BadRequestException("owner is required");
     }
 
-    GroupReference owner =
-        GroupBackends.findExactSuggestion(groupBackend, input.owner);
-    if (owner == null) {
+    GroupDescription.Basic owner;
+    try {
+      owner = groupsCollection.get().parse(input.owner);
+    } catch (ResourceNotFoundException e) {
       throw new BadRequestException(String.format("No such group: %s", input.owner));
     }
 
     try {
-      GroupControl c = controlFactory.validateFor(owner.getUUID());
+      GroupControl c = controlFactory.validateFor(owner.getGroupUUID());
       group = db.accountGroups().get(group.getId());
       if (group == null) {
         throw new ResourceNotFoundException();
       }
 
-      if (!group.getOwnerGroupUUID().equals(owner.getUUID())) {
-        group.setOwnerGroupUUID(owner.getUUID());
+      if (!group.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
+        group.setOwnerGroupUUID(owner.getGroupUUID());
         db.accountGroups().update(Collections.singleton(group));
         groupCache.evict(group);
       }
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
index a67c38c..acec1d1 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
@@ -31,7 +31,7 @@
 ## The ChangeSubject.vm template will determine the contents of the email
 ## subject line for ALL emails related to changes.
 ##
-#macro(elipses $length $str)
-#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end
+#macro(ellipsis $length $str)
+#if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end
 #end
-Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #elipses(60, $change.subject)
+Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #ellipsis(63, $change.subject)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
index 8208062..22e29e8 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
@@ -32,9 +32,6 @@
 ## a change successfully merged to the head.  It is a ChangeEmail: see
 ## ChangeSubject.vm and ChangeFooter.vm.
 ##
-#macro(elipses $length $str)
-#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end
-#end
 $fromName has submitted this change and it was merged.
 
 Change subject: $change.subject