Merge "Add an 'Ack' button next to the 'Done' button"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 41c464f..87e5031 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -482,6 +482,14 @@
 Certain operations in Gerrit can be validated by plugins by
 implementing the corresponding link:config-validation.html[listeners].
 
+[[change-message-modifier]]
+== Change Message Modifier
+
+`com.google.gerrit.server.git.ChangeMessageModifier`:
+plugins implementing this can modify commit message of the change being
+submitted by Rebase Always and Cherry Pick submit strategies as well as
+change being queried with COMMIT_FOOTERS option.
+
 [[receive-pack]]
 == Receive Pack Initializers
 
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 5a6c229..c2618f5 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
@@ -73,6 +73,8 @@
 import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -91,6 +93,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ChangeControl;
@@ -129,6 +132,9 @@
   @Inject
   private BatchUpdate.Factory updateFactory;
 
+  @Inject
+  private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+
   @Before
   public void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
@@ -1810,6 +1816,40 @@
   }
 
   @Test
+  public void customCommitFooters() throws Exception {
+    PushOneCommit.Result change = createChange();
+    RegistrationHandle handle =
+        changeMessageModifiers.add(new ChangeMessageModifier() {
+          @Override
+          public String onSubmit(String newCommitMessage, RevCommit original,
+              RevCommit mergeTip, Branch.NameKey destination) {
+            return newCommitMessage + "Custom: " + destination.get();
+          }
+        });
+    ChangeInfo actual;
+    try {
+      EnumSet<ListChangesOption> options = EnumSet.of(
+          ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS);
+      actual = gApi.changes().id(change.getChangeId()).get(options);
+    } finally {
+      handle.remove();
+    }
+    List<String> footers = new ArrayList<>(Arrays.asList(
+        actual.revisions.get(change.getCommit().getName()).commitWithFooters
+            .split("\\n")));
+    // remove subject + blank line
+    footers.remove(0);
+    footers.remove(0);
+
+    List<String> expectedFooters =
+        Arrays.asList(
+            "Change-Id: " + change.getChangeId(), "Reviewed-on: "
+                + canonicalWebUrl.get() + change.getChange().getId(),
+            "Custom: refs/heads/master");
+    assertThat(footers).containsExactlyElementsIn(expectedFooters);
+  }
+
+  @Test
   public void defaultSearchDoesNotTouchDatabase() throws Exception {
     setApiUser(admin);
     PushOneCommit.Result r1 = createChange();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index af43373..0a3b217 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -19,17 +19,23 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.Submit.TestSubmitInput;
+import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.strategy.CommitMergeStatus;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -40,6 +46,8 @@
 import java.util.List;
 
 public class SubmitByCherryPickIT extends AbstractSubmit {
+  @Inject
+  private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -89,6 +97,31 @@
   }
 
   @Test
+  public void changeMessageOnSubmit() throws Exception {
+    PushOneCommit.Result change = createChange();
+    RegistrationHandle handle =
+        changeMessageModifiers.add(new ChangeMessageModifier() {
+          @Override
+          public String onSubmit(String newCommitMessage, RevCommit original,
+              RevCommit mergeTip, Branch.NameKey destination) {
+            return newCommitMessage + "Custom: " + destination.get();
+          }
+        });
+    try {
+      submit(change.getChangeId());
+    } finally {
+      handle.remove();
+    }
+    testRepo.git().fetch().setRemote("origin").call();
+    ChangeInfo info = get(change.getChangeId());
+    RevCommit c = testRepo.getRevWalk()
+        .parseCommit(ObjectId.fromString(info.currentRevision));
+    testRepo.getRevWalk().parseBody(c);
+    assertThat(c.getFooterLines("Custom")).containsExactly("refs/heads/master");
+    assertThat(c.getFooterLines(FooterConstants.REVIEWED_ON)).hasSize(1);
+  }
+
+  @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
     RevCommit initialHead = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index d1c9577..0389417 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -22,12 +22,21 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
+import java.util.List;
+
 public class SubmitByRebaseAlwaysIT extends AbstractSubmitByRebase {
+  @Inject
+  private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -76,6 +85,41 @@
     assertLatestRevisionHasFooters(change2);
   }
 
+  @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
+  public void changeMessageOnSubmit() throws Exception {
+    PushOneCommit.Result change1 = createChange();
+    PushOneCommit.Result change2 = createChange();
+
+    RegistrationHandle handle =
+        changeMessageModifiers.add(new ChangeMessageModifier() {
+          @Override
+          public String onSubmit(String newCommitMessage, RevCommit original,
+              RevCommit mergeTip, Branch.NameKey destination) {
+            List<String> custom = mergeTip.getFooterLines("Custom");
+            if (!custom.isEmpty()) {
+              newCommitMessage += "Custom-Parent: " + custom.get(0) + "\n";
+            }
+            return newCommitMessage + "Custom: " + destination.get();
+          }
+        });
+    try {
+      // change1 is a fast-forward, but should be rebased in cherry pick style
+      // anyway, making change2 not a fast-forward, requiring a rebase.
+      approve(change1.getChangeId());
+      submit(change2.getChangeId());
+    } finally {
+      handle.remove();
+    }
+    // ... but both changes should get custom footers.
+    assertThat(getCurrentCommit(change1).getFooterLines("Custom"))
+        .containsExactly("refs/heads/master");
+    assertThat(getCurrentCommit(change2).getFooterLines("Custom"))
+        .containsExactly("refs/heads/master");
+    assertThat(getCurrentCommit(change2).getFooterLines("Custom-Parent"))
+        .containsExactly("refs/heads/master");
+  }
+
   private void assertLatestRevisionHasFooters(PushOneCommit.Result change)
       throws Exception {
     RevCommit c = getCurrentCommit(change);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
index f1489bb..7d6b1c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -113,6 +113,8 @@
     if (currentAssignee != null) {
       suggestBox.setText(FormatUtil.nameEmail(currentAssignee));
       suggestBox.selectAll();
+    } else {
+      suggestBox.setText("");
     }
   }
 
@@ -137,7 +139,7 @@
   }
 
   private void editAssignee(final String assignee) {
-    if (assignee.isEmpty()) {
+    if (assignee.trim().isEmpty()) {
       ChangeApi.deleteAssignee(changeId.get(),
           new GerritCallback<AccountInfo>() {
             @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index a008149..84c2403 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -122,7 +122,7 @@
       AsyncCallback<AccountInfo> cb) {
     AssigneeInput input = AssigneeInput.create();
     input.assignee(user);
-    change(id).view("assignee").put(user, cb);
+    change(id).view("assignee").put(input, cb);
   }
 
   public static RestApi comments(int id) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index faddd6c..e6d395c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -21,7 +21,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
-
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Throwables;
diff --git a/gerrit-patch-jgit/BUILD b/gerrit-patch-jgit/BUILD
index eff4e5a..f908d83 100644
--- a/gerrit-patch-jgit/BUILD
+++ b/gerrit-patch-jgit/BUILD
@@ -27,12 +27,12 @@
 genrule2(
   name = 'jgit_edit_src',
   cmd = ' && '.join([
-    'unzip -qd $$TMP $(location @jgit//src) ' +
+    'unzip -qd $$TMP $(location @jgit//jar:src) ' +
       'org/eclipse/jgit/diff/Edit.java',
     'cd $$TMP',
     'zip -Dq $$ROOT/$@ org/eclipse/jgit/diff/Edit.java',
   ]),
-  tools = ['@jgit//src'],
+  tools = ['@jgit//jar:src'],
   outs = [ 'edit.srcjar' ],
 )
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index e210d5b..78ea5a0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -87,6 +87,12 @@
   /** Prompt the user for a password, returning the string; null if blank. */
   public abstract String password(String fmt, Object... args);
 
+  /** Display an error message on the system stderr. */
+  public void error(String format, Object... args) {
+    System.err.println(String.format(format, args));
+    System.err.flush();
+  }
+
   /** Prompt the user to make a choice from an enumeration's values. */
   public abstract <T extends Enum<?>> T readEnum(T def, String fmt,
       Object... args);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 0f3d45c..fbaabc6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -143,6 +143,9 @@
     }
 
     public static Id fromRef(String ref) {
+      if (RefNames.isRefsEdit(ref)) {
+        return fromEditRefPart(ref);
+      }
       int cs = startIndex(ref);
       if (cs < 0) {
         return null;
@@ -156,6 +159,42 @@
       return null;
     }
 
+    public static Id fromAllUsersRef(String ref) {
+      if (ref == null) {
+        return null;
+      }
+      String prefix;
+      if (ref.startsWith(RefNames.REFS_STARRED_CHANGES)) {
+        prefix = RefNames.REFS_STARRED_CHANGES;
+      } else if (ref.startsWith(RefNames.REFS_DRAFT_COMMENTS)) {
+        prefix = RefNames.REFS_DRAFT_COMMENTS;
+      } else {
+        return null;
+      }
+      int cs = startIndex(ref, prefix);
+      if (cs < 0) {
+        return null;
+      }
+      int ce = nextNonDigit(ref, cs);
+      if (ce < ref.length() && ref.charAt(ce) == '/'
+          && isNumeric(ref, ce + 1)) {
+        return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
+      }
+      return null;
+    }
+
+    private static boolean isNumeric(String s, int off) {
+      if (off >= s.length()) {
+        return false;
+      }
+      for (int i = off; i < s.length(); i++) {
+        if (!Character.isDigit(s.charAt(i))) {
+          return false;
+        }
+      }
+      return true;
+    }
+
     public static Id fromEditRefPart(String ref) {
       int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) +
           RefNames.EDIT_PREFIX.length();
@@ -173,12 +212,16 @@
     }
 
     static int startIndex(String ref) {
-      if (ref == null || !ref.startsWith(REFS_CHANGES)) {
+      return startIndex(ref, REFS_CHANGES);
+    }
+
+    static int startIndex(String ref, String expectedPrefix) {
+      if (ref == null || !ref.startsWith(expectedPrefix)) {
         return -1;
       }
 
       // Last 2 digits.
-      int ls = REFS_CHANGES.length();
+      int ls = expectedPrefix.length();
       int le = nextNonDigit(ref, ls);
       if (le - ls != 2 || le >= ref.length() || ref.charAt(le) != '/') {
         return -1;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 33479d4..c7c870e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -195,7 +195,8 @@
   }
 
   public static boolean isRefsEdit(String ref) {
-    return ref.startsWith(REFS_USERS) && ref.contains(EDIT_PREFIX);
+    return ref != null && ref.startsWith(REFS_USERS)
+        && ref.contains(EDIT_PREFIX);
   }
 
   public static boolean isRefsUsers(String ref) {
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
index cf2d289..2aa863e 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -64,6 +64,15 @@
   }
 
   @Test
+  public void parseEditRefNames() {
+    assertRef(5, "refs/users/34/1234/edit-5/1");
+    assertRef(5, "refs/users/34/1234/edit-5");
+    assertNotRef("refs/changes/34/1234/edit-5/1");
+    assertNotRef("refs/users/34/1234/EDIT-5/1");
+    assertNotRef("refs/users/34/1234");
+  }
+
+  @Test
   public void parseChangeMetaRefNames() {
     assertRef(1, "refs/changes/01/1/meta");
     assertRef(1234, "refs/changes/34/1234/meta");
@@ -74,6 +83,44 @@
   }
 
   @Test
+  public void parseRobotCommentRefNames() {
+    assertRef(1, "refs/changes/01/1/robot-comments");
+    assertRef(1234, "refs/changes/34/1234/robot-comments");
+
+    assertNotRef("refs/changes/01/1/robot-comment");
+    assertNotRef("refs/changes/01/1/ROBOT-COMMENTS");
+    assertNotRef("refs/changes/01/1/1/robot-comments");
+  }
+
+  @Test
+  public void parseStarredChangesRefNames() {
+    assertAllUsersRef(1, "refs/starred-changes/01/1/1001");
+    assertAllUsersRef(1234, "refs/starred-changes/34/1234/1001");
+
+    assertNotRef("refs/starred-changes/01/1/1001");
+    assertNotAllUsersRef(null);
+    assertNotAllUsersRef("refs/starred-changes/01/1/1xx1");
+    assertNotAllUsersRef("refs/starred-changes/01/1/");
+    assertNotAllUsersRef("refs/starred-changes/01/1");
+    assertNotAllUsersRef("refs/starred-changes/35/1234/1001");
+    assertNotAllUsersRef("refs/starred-changeS/01/1/1001");
+  }
+
+  @Test
+  public void parseDraftRefNames() {
+    assertAllUsersRef(1, "refs/draft-comments/01/1/1001");
+    assertAllUsersRef(1234, "refs/draft-comments/34/1234/1001");
+
+    assertNotRef("refs/draft-comments/01/1/1001");
+    assertNotAllUsersRef(null);
+    assertNotAllUsersRef("refs/draft-comments/01/1/1xx1");
+    assertNotAllUsersRef("refs/draft-comments/01/1/");
+    assertNotAllUsersRef("refs/draft-comments/01/1");
+    assertNotAllUsersRef("refs/draft-comments/35/1234/1001");
+    assertNotAllUsersRef("refs/draft-commentS/01/1/1001");
+  }
+
+  @Test
   public void toRefPrefix() {
     assertThat(new Change.Id(1).toRefPrefix())
         .isEqualTo("refs/changes/01/1/");
@@ -110,6 +157,15 @@
     assertThat(Change.Id.fromRef(refName)).isNull();
   }
 
+  private static void assertAllUsersRef(int changeId, String refName) {
+    assertThat(Change.Id.fromAllUsersRef(refName))
+        .isEqualTo(new Change.Id(changeId));
+  }
+
+  private static void assertNotAllUsersRef(String refName) {
+    assertThat(Change.Id.fromAllUsersRef(refName)).isNull();
+  }
+
   private static void assertRefPart(int changeId, String refName) {
     assertEquals(new Change.Id(changeId), Change.Id.fromRefPart(refName));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
index c0d81cb..24a0dae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -16,15 +16,14 @@
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
-
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
@@ -169,7 +168,6 @@
       VersionedAccountPreferences v, VersionedAccountPreferences d) {
     r.changeTable = changeTable(v);
 
-    Config cfg = v.getConfig();
     if (r.changeTable.isEmpty() && !v.isDefaults()) {
       r.changeTable = changeTable(d);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index 08b7b0b..3714cee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.account;
 
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
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 3f3afa5..680234c 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
@@ -121,6 +121,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -1111,9 +1112,15 @@
           out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
         }
         if (addFooters) {
+          Ref ref = repo.exactRef(in.getRefName());
+          RevCommit mergeTip = null;
+          if (ref != null){
+            mergeTip = rw.parseCommit(ref.getObjectId());
+            rw.parseBody(mergeTip);
+          }
           out.commitWithFooters = mergeUtilFactory
               .create(projectCache.get(project))
-              .createDetailedCommitMessage(commit, ctl, in.getId());
+              .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId());
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
index 8236d3d..92b4150 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -23,7 +23,8 @@
   }
 
   public String revertChangeDefaultMessage;
-  public String reviewerNotFound;
+  public String reviewerNotFoundUser;
+  public String reviewerNotFoundUserOrGroup;
 
   public String groupIsNotAllowed;
   public String groupHasTooManyMembers;
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 cd62e45..ae440c8 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
@@ -209,7 +209,7 @@
         reviewerInput.notify = NotifyHandling.NONE;
 
         PostReviewers.Addition result = postReviewers.prepareApplication(
-            revision.getChangeResource(), reviewerInput);
+            revision.getChangeResource(), reviewerInput, true);
         reviewerJsonResults.put(reviewerInput.reviewer, result.result);
         if (result.result.error != null) {
           hasError = true;
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 f0af5da..0cdddca 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
@@ -147,7 +147,7 @@
       throw new BadRequestException("missing reviewer field");
     }
 
-    Addition addition = prepareApplication(rsrc, input);
+    Addition addition = prepareApplication(rsrc, input, true);
     if (addition.op == null) {
       return addition.result;
     }
@@ -161,18 +161,24 @@
     return addition.result;
   }
 
-  public Addition prepareApplication(ChangeResource rsrc, AddReviewerInput input)
-      throws OrmException, RestApiException, IOException {
+  public Addition prepareApplication(ChangeResource rsrc,
+      AddReviewerInput input, boolean allowGroup)
+          throws OrmException, RestApiException, IOException {
     Account.Id accountId;
     try {
       accountId = accounts.parse(input.reviewer).getAccountId();
     } catch (UnprocessableEntityException e) {
-      try {
-        return putGroup(rsrc, input);
-      } catch (UnprocessableEntityException e2) {
-        throw new UnprocessableEntityException(MessageFormat
-            .format(ChangeMessages.get().reviewerNotFound, input.reviewer));
+      if (allowGroup) {
+        try {
+          return putGroup(rsrc, input);
+        } catch (UnprocessableEntityException e2) {
+          throw new UnprocessableEntityException(MessageFormat.format(
+              ChangeMessages.get().reviewerNotFoundUserOrGroup,
+              input.reviewer));
+        }
       }
+      throw new UnprocessableEntityException(MessageFormat
+          .format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
     }
     return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId),
         input.state(), input.notify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index 5002436..271fd33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -65,7 +64,7 @@
     if (!rsrc.getControl().canEditAssignee()) {
       throw new AuthException("Changing Assignee not permitted");
     }
-    if (Strings.isNullOrEmpty(input.assignee)) {
+    if (input.assignee == null || input.assignee.trim().isEmpty()) {
       throw new BadRequestException("missing assignee field");
     }
 
@@ -91,7 +90,7 @@
     reviewerInput.state = ReviewerState.CC;
     reviewerInput.confirmed = true;
     reviewerInput.notify = NotifyHandling.NONE;
-    return postReviewers.prepareApplication(rsrc, reviewerInput);
+    return postReviewers.prepareApplication(rsrc, reviewerInput, false);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 7ece10f..9c51425 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -142,14 +142,6 @@
     RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get()));
     rw.parseBody(original);
 
-    String newCommitMessage;
-    if (detailedCommitMessage) {
-      newCommitMessage = newMergeUtil().createDetailedCommitMessage(original,
-          ctl, originalPatchSet.getId());
-    } else {
-      newCommitMessage = original.getFullMessage();
-    }
-
     RevCommit baseCommit;
     if (baseCommitish != null) {
        baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish));
@@ -159,6 +151,15 @@
            ctx.getRepository(), ctx.getRevWalk()));
     }
 
+    String newCommitMessage;
+    if (detailedCommitMessage) {
+      rw.parseBody(baseCommit);
+      newCommitMessage = newMergeUtil().createCommitMessageOnSubmit(original,
+          baseCommit, ctl, originalPatchSet.getId());
+    } else {
+      newCommitMessage = original.getFullMessage();
+    }
+
     rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage);
 
     RevId baseRevId = new RevId((baseCommitish != null) ? baseCommitish
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6dd0fab..acae738 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -107,6 +107,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.AbandonOp;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.GitModules;
@@ -342,6 +343,7 @@
     DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
     DynamicSet.setOf(binder(), UserScopedEventListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
+    DynamicSet.setOf(binder(), ChangeMessageModifier.class);
     DynamicSet.setOf(binder(), RefOperationValidationListener.class);
     DynamicSet.setOf(binder(), MergeValidationListener.class);
     DynamicSet.setOf(binder(), ProjectCreationValidationListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.java
new file mode 100644
index 0000000..75911f3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Branch;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Allows to modify the commit message for new commits generated by Rebase
+ * Always submit strategy.
+ *
+ * Invoked by Gerrit when all information about new commit is already known such
+ * as parent(s), tree hash, etc, but commit's message can still be modified.
+ */
+@ExtensionPoint
+public interface ChangeMessageModifier {
+
+  /**
+   * Implementation must return non-Null commit message.
+   *
+   * mergeTip and original commit are guaranteed to have their body parsed,
+   * meaning that their commit messages and footers can be accessed.
+   *
+   * @param newCommitMessage the new commit message that was result of either
+   *        <ul>
+   *        <li>{@link MergeUtil#createDetailedCommitMessage} called before</li>
+   *        <li>other extensions or plugins implementing the same point and
+   *        called before.</li>
+   *        </ul>
+   * @param original the commit of the change being submitted. <b>Note that its
+   *        commit message may be different than newCommitMessage argument.</b>
+   * @param mergeTip the current HEAD of the destination branch, which will be a
+   *        parent of a new commit being generated
+   * @param destination the branch onto which the change is being submitted
+   * @return a new not null commit message.
+   */
+  String onSubmit(String newCommitMessage, RevCommit original,
+      RevCommit mergeTip, Branch.NameKey destination);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 795bec8..0a4a430 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
@@ -25,6 +26,7 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -44,6 +47,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -98,6 +102,32 @@
  */
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
+
+  static class PluggableCommitMessageGenerator {
+    private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+
+    @Inject
+    public PluggableCommitMessageGenerator(
+        DynamicSet<ChangeMessageModifier> changeMessageModifiers) {
+      this.changeMessageModifiers = changeMessageModifiers;
+    }
+
+    public String generate(RevCommit original, RevCommit mergeTip,
+        ChangeControl ctl, String current) {
+      checkNotNull(original.getRawBuffer());
+      if (mergeTip != null) {
+        checkNotNull(mergeTip.getRawBuffer());
+      }
+      for (ChangeMessageModifier changeMessageModifier : changeMessageModifiers) {
+        current = changeMessageModifier.onSubmit(current, original,
+            mergeTip, ctl.getChange().getDest());
+        checkNotNull(current, changeMessageModifier.getClass().getName()
+            + ".OnSubmit returned null instead of new commit message");
+      }
+      return current;
+    }
+  }
+
   private static final String R_HEADS_MASTER =
       Constants.R_HEADS + Constants.MASTER;
 
@@ -123,25 +153,28 @@
   private final ProjectState project;
   private final boolean useContentMerge;
   private final boolean useRecursiveMerge;
+  private final PluggableCommitMessageGenerator commitMessageGenerator;
 
   @AssistedInject
   MergeUtil(@GerritServerConfig Config serverConfig,
-      final Provider<ReviewDb> db,
-      final IdentifiedUser.GenericFactory identifiedUserFactory,
-      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final ApprovalsUtil approvalsUtil,
-      @Assisted final ProjectState project) {
+      Provider<ReviewDb> db,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      ApprovalsUtil approvalsUtil,
+      PluggableCommitMessageGenerator commitMessageGenerator,
+      @Assisted ProjectState project) {
     this(serverConfig, db, identifiedUserFactory, urlProvider, approvalsUtil,
-        project, project.isUseContentMerge());
+        project, commitMessageGenerator, project.isUseContentMerge());
   }
 
   @AssistedInject
   MergeUtil(@GerritServerConfig Config serverConfig,
-      final Provider<ReviewDb> db,
-      final IdentifiedUser.GenericFactory identifiedUserFactory,
-      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final ApprovalsUtil approvalsUtil,
-      @Assisted final ProjectState project,
+      Provider<ReviewDb> db,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      ApprovalsUtil approvalsUtil,
+      @Assisted ProjectState project,
+      PluggableCommitMessageGenerator commitMessageGenerator,
       @Assisted boolean useContentMerge) {
     this.db = db;
     this.identifiedUserFactory = identifiedUserFactory;
@@ -150,6 +183,7 @@
     this.project = project;
     this.useContentMerge = useContentMerge;
     this.useRecursiveMerge = useRecursiveMerge(serverConfig);
+    this.commitMessageGenerator = commitMessageGenerator;
   }
 
   public CodeReviewCommit getFirstFastForward(
@@ -264,7 +298,7 @@
    * @param psId
    * @return new message
    */
-  public String createDetailedCommitMessage(RevCommit n, ChangeControl ctl,
+  private String createDetailedCommitMessage(RevCommit n, ChangeControl ctl,
       PatchSet.Id psId) {
     Change c = ctl.getChange();
     final List<FooterLine> footers = n.getFooterLines();
@@ -368,12 +402,32 @@
         msgbuf.append('\n');
       }
     }
-
     return msgbuf.toString();
   }
 
-  public String createDetailedCommitMessage(final CodeReviewCommit n) {
-    return createDetailedCommitMessage(n, n.getControl(), n.getPatchsetId());
+  public String createCommitMessageOnSubmit(CodeReviewCommit n,
+      RevCommit mergeTip) {
+    return createCommitMessageOnSubmit(n, mergeTip, n.getControl(),
+        n.getPatchsetId());
+  }
+
+  /**
+   * Creates a commit message for a change, which can be customized by plugins.
+   *
+   * By default, adds footers to existing commit message based on the state of
+   * the change. Plugins implementing {@link ChangeMessageModifier} can modify
+   * the resulting commit message arbitrarily.
+   *
+   * @param n
+   * @param mergeTip
+   * @param ctl
+   * @param id
+   * @return new message
+   */
+  public String createCommitMessageOnSubmit(RevCommit n, RevCommit mergeTip,
+      ChangeControl ctl, Id id) {
+    return commitMessageGenerator.generate(n, mergeTip, ctl,
+        createDetailedCommitMessage(n, ctl, id));
   }
 
   private static boolean isCodeReview(LabelId id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index e9772e5..af08b63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -33,6 +33,7 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
@@ -99,8 +100,10 @@
       args.rw.parseBody(toMerge);
       psId = ChangeUtil.nextPatchSetId(
           args.repo, toMerge.change().currentPatchSetId());
+      RevCommit mergeTip = args.mergeTip.getCurrentTip();
+      args.rw.parseBody(mergeTip);
       String cherryPickCmtMsg =
-          args.mergeUtil.createDetailedCommitMessage(toMerge);
+          args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
 
       PersonIdent committer = args.caller.newCommitterIdent(
           ctx.getWhen(), args.serverIdent.getTimeZone());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index 34d3a80..135db95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -137,10 +137,10 @@
         args.rw.parseBody(toMerge);
         newPatchSetId = ChangeUtil.nextPatchSetId(
             args.repo, toMerge.change().currentPatchSetId());
-        // TODO(tandrii): add extension point to customize this commit message.
+        RevCommit mergeTip = args.mergeTip.getCurrentTip();
+        args.rw.parseBody(mergeTip);
         String cherryPickCmtMsg =
-            args.mergeUtil.createDetailedCommitMessage(toMerge);
-
+            args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
         PersonIdent committer = args.caller.newCommitterIdent(ctx.getWhen(),
             args.serverIdent.getTimeZone());
         try {
@@ -162,8 +162,6 @@
         // Stale read of patch set is ok; see comments in RebaseChangeOp.
         PatchSet origPs = args.psUtil.get(ctx.getDb(),
             toMerge.getControl().getNotes(), toMerge.getPatchsetId());
-        // TODO(tandrii): add extension point to customize commit message while
-        // rebasing.
         rebaseOp = args.rebaseFactory.create(
               toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name())
             .setFireRevisionCreated(false)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 1672eea..1d376d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -971,7 +971,7 @@
           input.editRefs().values().forEach(
               r -> result.add(RefState.of(r).toByteArray(project)));
           input.starRefs().values().forEach(
-              r -> result.add(RefState.of(r.ref()).toByteArray(project)));
+              r -> result.add(RefState.of(r.ref()).toByteArray(args.allUsers)));
 
           if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
             ChangeNotes notes = input.notes();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
index 0c3d89c..7c841c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -62,7 +62,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(StalenessChecker.class);
 
-  private final ImmutableSet<String> FIELDS = ImmutableSet.of(
+  public static final ImmutableSet<String> FIELDS = ImmutableSet.of(
       ChangeField.CHANGE.getName(),
       ChangeField.REF_STATE.getName(),
       ChangeField.REF_STATE_PATTERN.getName());
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
index f05f23b..f34c992 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,7 +1,8 @@
 # Changes to this file should also be made in
 # gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
-reviewerNotFound = {0} does not identify a registered user or group
+reviewerNotFoundUser = {0} does not identify a registered user
+reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group
 
 groupIsNotAllowed =  The group {0} cannot be added as reviewer.
 groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
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 d22e7a8..5e5a047 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
@@ -21,6 +21,7 @@
 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.stream.Collectors.toList;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.FluentIterable;
@@ -38,6 +39,7 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -54,6 +56,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
@@ -66,10 +69,15 @@
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.QueryOptions;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.index.change.IndexedChangeQuery;
+import com.google.gerrit.server.index.change.StalenessChecker;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
@@ -100,6 +108,7 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -125,6 +134,7 @@
   @Inject protected ChangeEditModifier changeEditModifier;
   @Inject protected ChangeIndexCollection indexes;
   @Inject protected ChangeIndexer indexer;
+  @Inject protected IndexConfig indexConfig;
   @Inject protected InMemoryDatabase schemaFactory;
   @Inject protected InMemoryRepositoryManager repoManager;
   @Inject protected InternalChangeQuery internalChangeQuery;
@@ -1638,6 +1648,97 @@
     assertQuery("has:edit");
   }
 
+  @Test
+  public void refStateFields() throws Exception {
+    Account.Id user = createAccount("user");
+    Project.NameKey project = new Project.NameKey("repo");
+    TestRepository<Repo> repo = createProject(project.get());
+    String path = "file";
+    RevCommit commit = repo.parseBody(
+        repo.commit().message("one").add(path, "contents").create());
+    Change change = insert(repo, newChangeForCommit(repo, commit));
+    Change.Id id = change.getId();
+    int c = id.get();
+    PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+    requestContext.setContext(newRequestContext(user));
+
+    // Ensure one of each type of supported ref is present for the change. If
+    // any more refs are added, update this test to reflect them.
+
+    // Edit
+    assertThat(changeEditModifier.createEdit(change, ps))
+        .isEqualTo(RefUpdate.Result.NEW);
+
+    // Star
+    gApi.accounts()
+        .self()
+        .starChange(change.getId().toString());
+
+    if (notesMigration.readChanges()) {
+      // Robot comment.
+      ReviewInput rin = new ReviewInput();
+      RobotCommentInput rcin = new RobotCommentInput();
+      rcin.robotId = "happyRobot";
+      rcin.robotRunId = "1";
+      rcin.line = 1;
+      rcin.message = "nit: trailing whitespace";
+      rcin.path = path;
+      rin.robotComments = ImmutableMap.of(path, ImmutableList.of(rcin));
+      gApi.changes().id(c).current().review(rin);
+    }
+
+    // Draft.
+    DraftInput din = new DraftInput();
+    din.path = path;
+    din.line = 1;
+    din.message = "draft";
+    gApi.changes().id(c).current().createDraft(din);
+
+    if (notesMigration.readChanges()) {
+      // Force NoteDb primary.
+      change = ReviewDbUtil.unwrapDb(db).changes().get(id);
+      change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+      ReviewDbUtil.unwrapDb(db).changes().update(Collections.singleton(change));
+      indexer.index(db, change);
+    }
+
+    QueryOptions opts = IndexedChangeQuery.createOptions(
+        indexConfig, 0, 1, StalenessChecker.FIELDS);
+    ChangeData cd = indexes.getSearchIndex().get(id, opts).get();
+
+    String cs = RefNames.shard(c);
+    int u = user.get();
+    String us = RefNames.shard(u);
+
+    List<String> expectedStates = Lists.newArrayList(
+        "repo:refs/users/" + us + "/edit-" + c + "/1",
+        "All-Users:refs/starred-changes/" + cs + "/" + u);
+    if (notesMigration.readChanges()) {
+      expectedStates.add("repo:refs/changes/" + cs + "/meta");
+      expectedStates.add("repo:refs/changes/" + cs + "/robot-comments");
+      expectedStates.add("All-Users:refs/draft-comments/" + cs + "/" + u);
+    }
+    assertThat(
+            cd.getRefStates().stream()
+                .map(String::new)
+                // Omit SHA-1, we're just concerned with the project/ref names.
+                .map(s -> s.substring(0, s.lastIndexOf(':')))
+                .collect(toList()))
+        .containsExactlyElementsIn(expectedStates);
+
+    List<String> expectedPatterns = Lists.newArrayList(
+        "repo:refs/users/*/edit-" + c + "/*");
+    if (notesMigration.readChanges()) {
+      expectedPatterns.add("All-Users:refs/starred-changes/" + cs + "/*");
+      expectedPatterns.add("All-Users:refs/draft-comments/" + cs + "/*");
+    }
+    assertThat(
+            cd.getRefStatePatterns().stream()
+                .map(String::new)
+                .collect(toList()))
+        .containsExactlyElementsIn(expectedPatterns);
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo)
       throws Exception {
     return newChange(repo, null, null, null, null);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 85b1f32..d3065df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -44,7 +44,7 @@
     try {
       changeArgumentParser.addChange(token, changes, null, false);
     } catch (UnloggedFailure e) {
-      throw new IllegalArgumentException(e.getMessage(), e);
+      writeError("warning", e.getMessage());
     } catch (OrmException e) {
       throw new IllegalArgumentException("database is down", e);
     }
diff --git a/lib/BUILD b/lib/BUILD
index f159a45..a4f1d51 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -32,7 +32,7 @@
 
 java_library(
   name = 'gwtjsonrpc_src',
-  exports = ['@gwtjsonrpc//src'],
+  exports = ['@gwtjsonrpc//jar:src'],
   visibility = ['//visibility:public'],
   data = ['//lib:LICENSE-Apache2.0'],
 )
@@ -53,7 +53,7 @@
 
 java_library(
   name = 'gwtorm_client_src',
-  exports = ['@gwtorm_client//src'],
+  exports = ['@gwtorm_client//jar:src'],
   visibility = ['//visibility:public'],
   data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/gwt/BUILD b/lib/gwt/BUILD
index 116f86c..5d826d2 100644
--- a/lib/gwt/BUILD
+++ b/lib/gwt/BUILD
@@ -32,15 +32,14 @@
 
 java_library(
   name = 'javax-validation_src',
-  exports = ['@javax_validation//src'],
+  exports = ['@javax_validation//jar:src'],
   visibility = ['//visibility:public'],
   data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'jsinterop-annotations_src',
-  exports = ['@jsinterop_annotations//src'],
+  exports = ['@jsinterop_annotations//jar:src'],
   visibility = ['//visibility:public'],
   data = ['//lib:LICENSE-Apache2.0'],
 )
-
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index a98b00b..c187cf2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -393,7 +393,8 @@
           ListChangesOption.CURRENT_ACTIONS,
           ListChangesOption.CURRENT_COMMIT,
           ListChangesOption.DOWNLOAD_COMMANDS,
-          ListChangesOption.SUBMITTABLE
+          ListChangesOption.SUBMITTABLE,
+          ListChangesOption.WEB_LINKS
       );
       return this._getChangeDetail(
           changeNum, options, opt_errFn, opt_cancelCondition);
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index d14e96a..4866b23 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -81,7 +81,7 @@
   '//gerrit-gwtexpui:CSS',
   '//lib:gwtjsonrpc',
   '//lib/gwt:dev',
-  '@jgit//src',
+  '@jgit//jar:src',
 ]
 
 USER_AGENT_XML = """<module rename-to='gerrit_ui'>
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 41a5ef7..17d1423 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -49,24 +49,29 @@
       version = version,
   )
 
-# Provides the syntax "@jar_name//jar" for bin classifier
-# and "@jar_name//src" for sources
-def _generate_build_file(ctx, classifier, filename):
+def _generate_build_file(ctx, binjar, srcjar):
+  srcjar_attr = ""
+  if srcjar:
+    srcjar_attr = 'srcjar = "%s",' % srcjar
   contents = """
 # DO NOT EDIT: automatically generated BUILD file for maven_archive rule {rule_name}
+package(default_visibility = ['//visibility:public'])
 java_import(
-    name = '{classifier}',
-    jars = ['{filename}'],
-    visibility = ['//visibility:public']
+    name = 'jar',
+    {srcjar_attr}
+    jars = ['{binjar}'],
 )
-filegroup(
-    name = 'file',
-    srcs = ['{filename}'],
-    visibility = ['//visibility:public']
-)\n""".format(classifier = classifier,
+\n""".format(srcjar_attr = srcjar_attr,
               rule_name = ctx.name,
-              filename = filename)
-  ctx.file('%s/BUILD' % ctx.path(classifier), contents, False)
+              binjar = binjar)
+  if srcjar:
+    contents += """
+java_import(
+    name = 'src',
+    jars = ['{srcjar}'],
+)
+""".format(srcjar = srcjar)
+  ctx.file('%s/BUILD' % ctx.path("jar"), contents, False)
 
 def _maven_jar_impl(ctx):
   """rule to download a Maven archive."""
@@ -83,10 +88,6 @@
   binjar_path = ctx.path('/'.join(['jar', binjar]))
   binurl = url + '.jar'
 
-  srcjar = jar + '-src.jar'
-  srcjar_path = ctx.path('/'.join(['src', srcjar]))
-  srcurl = url + '-sources.jar'
-
   python = ctx.which("python")
   script = ctx.path(ctx.attr._download_script)
 
@@ -100,16 +101,20 @@
 
   if out.return_code:
     fail("failed %s: %s" % (' '.join(args), out.stderr))
-  _generate_build_file(ctx, "jar", binjar)
 
+  srcjar = None
   if ctx.attr.src_sha1 or ctx.attr.attach_source:
+    srcjar = jar + '-src.jar'
+    srcurl = url + '-sources.jar'
+    srcjar_path = ctx.path('jar/' + srcjar)
     args = [python, script, "-o", srcjar_path, "-u", srcurl]
     if ctx.attr.src_sha1:
       args.extend(['-v', ctx.attr.src_sha1])
     out = ctx.execute(args)
     if out.return_code:
       fail("failed %s: %s" % (args, out.stderr))
-    _generate_build_file(ctx, "src", srcjar)
+
+  _generate_build_file(ctx, binjar, srcjar)
 
 maven_jar=repository_rule(
   implementation=_maven_jar_impl,
diff --git a/tools/eclipse/project_bzl.py b/tools/eclipse/project_bzl.py
index 86fb527..6bc7398 100755
--- a/tools/eclipse/project_bzl.py
+++ b/tools/eclipse/project_bzl.py
@@ -210,7 +210,7 @@
       if m:
         prefix = m.group(1)
         suffix = m.group(2)
-        p = path.join(prefix, "src", "%s-src.jar" % suffix)
+        p = path.join(prefix, "jar", "%s-src.jar" % suffix)
         if path.exists(p):
           s = p
       # TODO(davido): make plugins actually work