Merge "AccountIT: Add asserts to check reindexing of accounts"
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
index 553ac5b..1fb871a 100644
--- a/Documentation/dev-stars.txt
+++ b/Documentation/dev-stars.txt
@@ -61,6 +61,19 @@
 
 The ignore star is represented by the special star label 'ignore'.
 
+[[mute-star]]
+== Mute Star
+
+If the "mute/<patchset_id>"-star is set by a user, and <patchset_id>
+matches the current patch set, the change is always reported as "reviewed"
+in the ChangeInfo.
+
+This allows users to "de-highlight" changes in a dashboard until a new
+patchset has been uploaded.
+
+The ChangeInfo muted-field will show if the change is currently in a
+mute state.
+
 [[query-stars]]
 == Query Stars
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 577f2e1..3c3699c 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2117,7 +2117,7 @@
 
 .Request
 ----
-  Set /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
 ----
 
 .Response
@@ -2148,6 +2148,67 @@
 
 If the change was already not private, the response is "`409 Conflict`".
 
+[[ignore]]
+=== Ignore
+--
+'PUT /changes/link:#change-id[\{change-id\}]/ignore'
+--
+
+Marks a change as ignored. The change will not be shown in the incoming
+reviews dashboard, and email notifications will be suppressed.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/ignore HTTP/1.0
+----
+
+[[unignore]]
+=== Unignore
+--
+'PUT /changes/link:#change-id[\{change-id\}]/unignore'
+--
+
+Un-marks a change as ignored.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0
+----
+
+[[mute]]
+=== Mute
+--
+'PUT /changes/link:#change-id[\{change-id\}]/mute'
+--
+
+Marks a change as muted.
+
+This allows users to "de-highlight" changes in their dashboard until a new
+patch set is uploaded.
+
+This differs from the link:#ignore[ignore] endpoint, which will mute
+emails and hide the change from dashboard completely until it is
+link:#unignore[unignored] again.
+
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0
+----
+
+[[unmute]]
+=== Unmute
+--
+'PUT /changes/link:#change-id[\{change-id\}]/unmute'
+--
+
+Unmutes a change.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0
+----
+
 [[edit-endpoints]]
 == Change Edit Endpoints
 
@@ -5395,6 +5456,8 @@
 |`problems`           |optional|
 A list of link:#problem-info[ProblemInfo] entities describing potential
 problems with this change. Only set if link:#check[CHECK] is set.
+|`is_private`         |optional, not set if `false`|
+When present, change is marked as private.
 |==================================
 
 [[change-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index a0e9954..16b76b7 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -183,6 +183,48 @@
   }
 ----
 
+[[check-access]]
+=== Check Access
+--
+'POST /config/server/check.access'
+--
+
+Runs access checks for other users.
+
+Input for the access checks that should be run must be provided in
+the request body inside a
+link:#access-check-input[AccessCheckInput] entity.
+
+.Request
+----
+  POST /config/server/check HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "project": "medium",
+    "account": "Kristen.Burns@gerritcodereview.com",
+    "ref": "refs/heads/secret/bla"
+  }
+----
+
+The result is a link:#access-check-info[AccessCheckInfo] entity
+detailing the read access of the given user for the given project (or
+project-ref combination).
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "result": {
+      "message": "user Kristen Burns \u003cKristen.Burns@gerritcodereview.com\u003e (1000098) cannot see ref refs/heads/secret/master in project medium",
+      "status": 403
+    }
+  }
+----
+
 [[confirm-email]]
 === Confirm Email
 --
@@ -1266,6 +1308,34 @@
 [[json-entities]]
 == JSON Entities
 
+[[access-check-info]]
+=== AccessCheckInfo
+The `AccessCheckInfo` entity is the result of a
+an access check.
+
+[options="header",cols="1,^1,5"]
+|=========================================
+|Field Name                  ||Description
+|`status`||The HTTP status code for the access.
+200 means success, 403 means denied and 404 means the project does not
+exist.
+|`message`|optional|A clarifying message if `status` is not 200.
+=========================================
+
+[[access-check-input]]
+=== AccessCheckInput
+The `AccessCheckInput` entity is a tuple of (account, project)  or
+(account, project, ref) for which we want to check access.
+
+[options="header",cols="1,^1,5"]
+|=========================================
+|Field Name                  ||Description
+|`account`||The account for which to check access
+|`project`||The project for which to check access
+|`ref`|optional|The refname for which to check access
+=========================================
+
+
 [[auth-info]]
 === AuthInfo
 The `AuthInfo` entity contains information about the authentication
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 7928512..8e2bff6 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -78,6 +78,12 @@
 `Accept-Encoding` request header is set to `gzip`. This may
 save on network transfer time for larger responses.
 
+[[input]]
+=== Input Format
+Unknown JSON parameters will simply be ignored by Gerrit without causing
+an exception. This also applies to case-sensitive parameters, such as
+map keys.
+
 [[timestamp]]
 === Timestamp
 Timestamps are given in UTC and have the format
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 90e1979..d49ed3f 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -367,6 +367,10 @@
 Mergeability of abandoned changes is not computed. This operator will
 not find any abandoned but mergeable changes.
 
+[[ignored]]
+is:ignored::
++
+True if the change is ignored. Same as `star:ignore`.
 
 [[private]]
 is:private::
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 2be0000..498286c 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
@@ -65,9 +65,11 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.Comment.Range;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
@@ -1095,6 +1097,31 @@
   }
 
   @Test
+  public void addReviewerThatIsInactiveEmailFallback() throws Exception {
+    assume().that(notesMigration.enabled()).isTrue();
+
+    ConfigInput conf = new ConfigInput();
+    conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+    gApi.projects().name(project.get()).config(conf);
+
+    PushOneCommit.Result result = createChange();
+
+    String username = "user@domain.com";
+    gApi.accounts().create(username).setActive(false);
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = username;
+    in.state = ReviewerState.CC;
+    AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+
+    assertThat(r.input).isEqualTo(username);
+    assertThat(r.error).isNull();
+    // When adding by email, the reviewers field is also empty because we can't
+    // render a ReviewerInfo object for a non-account.
+    assertThat(r.reviewers).isNull();
+  }
+
+  @Test
   public void addReviewer() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java
new file mode 100644
index 0000000..cda5347
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2017 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckAccessIT extends AbstractDaemonTest {
+
+  private Project.NameKey normalProject;
+  private Project.NameKey secretProject;
+  private Project.NameKey secretRefProject;
+  private TestAccount privilegedUser;
+  private AccountGroup privilegedGroup;
+
+  @Before
+  public void setUp() throws Exception {
+    normalProject = createProject("normal");
+    secretProject = createProject("secret");
+    secretRefProject = createProject("secretRef");
+    privilegedGroup = groupCache.get(new AccountGroup.NameKey(createGroup("privilegedGroup")));
+
+    privilegedUser = accounts.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
+    gApi.groups().id(privilegedGroup.getGroupUUID().get()).addMembers(privilegedUser.username);
+
+    assertThat(gApi.groups().id(privilegedGroup.getGroupUUID().get()).members().get(0).email)
+        .contains("snowden");
+
+    // deny(secretProject, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/*");
+    grant(Permission.READ, secretProject, "refs/*", false, privilegedGroup.getGroupUUID());
+    block(Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/*", secretProject);
+
+    // deny/grant/block arg ordering is screwy.
+    deny(secretRefProject, Permission.READ, SystemGroupBackend.ANONYMOUS_USERS, "refs/*");
+    grant(
+        Permission.READ,
+        secretRefProject,
+        "refs/heads/secret/*",
+        false,
+        privilegedGroup.getGroupUUID());
+    block(
+        Permission.READ,
+        SystemGroupBackend.REGISTERED_USERS,
+        "refs/heads/secret/*",
+        secretRefProject);
+    grant(
+        Permission.READ,
+        secretRefProject,
+        "refs/heads/*",
+        false,
+        SystemGroupBackend.REGISTERED_USERS);
+  }
+
+  @Test
+  public void invalidInputs() {
+    List<AccessCheckInput> inputs =
+        ImmutableList.of(
+            new AccessCheckInput(),
+            new AccessCheckInput(user.email, null, null),
+            new AccessCheckInput(null, normalProject.toString(), null),
+            new AccessCheckInput("doesnotexist@invalid.com", normalProject.toString(), null));
+    for (AccessCheckInput input : inputs) {
+      try {
+        gApi.config().server().checkAccess(input);
+        fail(String.format("want RestApiException for %s", newGson().toJson(input)));
+      } catch (RestApiException e) {
+
+      }
+    }
+  }
+
+  @Test
+  public void accessible() {
+    Map<AccessCheckInput, Integer> inputs =
+        ImmutableMap.of(
+            new AccessCheckInput(user.email, normalProject.get(), null), 200,
+            new AccessCheckInput(user.email, secretProject.get(), null), 403,
+            new AccessCheckInput(user.email, "nonexistent", null), 404,
+            new AccessCheckInput(privilegedUser.email, normalProject.get(), null), 200,
+            new AccessCheckInput(privilegedUser.email, secretProject.get(), null), 200);
+
+    for (Map.Entry<AccessCheckInput, Integer> entry : inputs.entrySet()) {
+      String in = newGson().toJson(entry.getKey());
+      AccessCheckInfo info = null;
+
+      try {
+        info = gApi.config().server().checkAccess(entry.getKey());
+      } catch (RestApiException e) {
+        fail(String.format("check.check(%s): exception %s", in, e));
+      }
+
+      int want = entry.getValue();
+      if (want != info.result.status) {
+        fail(String.format("check.access(%s) = %d, want %d", in, info.result.status, want));
+      }
+
+      switch (want) {
+        case 403:
+          assertThat(info.result.message).contains("cannot see");
+          break;
+        case 404:
+          assertThat(info.result.message).contains("does not exist");
+          break;
+        case 200:
+          assertThat(info.result.message).isNull();
+          break;
+        default:
+          fail(String.format("unknown code %d", want));
+      }
+    }
+  }
+}
diff --git a/gerrit-elasticsearch/BUILD b/gerrit-elasticsearch/BUILD
index 8c94830..0affa12 100644
--- a/gerrit-elasticsearch/BUILD
+++ b/gerrit-elasticsearch/BUILD
@@ -52,10 +52,8 @@
     name = "elasticsearch_tests",
     size = "large",
     srcs = glob(["src/test/java/**/*Test.java"]),
-    flaky = 1,
     tags = [
         "elastic",
-        "flaky",
     ],
     deps = [
         ":elasticsearch",
diff --git a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 3a0e20b..ece9edd 100644
--- a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -68,10 +68,9 @@
 
   static void configure(Config config, String port) {
     config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
-    config.setString("index", null, "protocol", "http");
-    config.setString("index", null, "hostname", "localhost");
-    config.setString("index", null, "port", port);
-    config.setBoolean("index", "elasticsearch", "test", true);
+    config.setString("elasticsearch", "test", "protocol", "http");
+    config.setString("elasticsearch", "test", "hostname", "localhost");
+    config.setString("elasticsearch", "test", "port", port);
   }
 
   static ElasticNodeInfo startElasticsearchNode() throws InterruptedException, ExecutionException {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 6064faa..83d36b1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -88,6 +88,20 @@
   void setPrivate(boolean value) throws RestApiException;
 
   /**
+   * Ignore or un-ignore this change.
+   *
+   * @param ignore ignore the change if true
+   */
+  void ignore(boolean ignore) throws RestApiException;
+
+  /**
+   * Mute or un-mute this change.
+   *
+   * @param mute mute the change if true
+   */
+  void mute(boolean mute) throws RestApiException;
+
+  /**
    * Create a new change that reverts this change.
    *
    * @see Changes#id(int)
@@ -483,5 +497,15 @@
     public ChangeInfo createMergePatchSet(MergePatchSetInput in) {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void ignore(boolean ignore) {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void mute(boolean mute) {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java
new file mode 100644
index 0000000..4ff5172
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+public class AccessCheckInfo {
+  public static class Result {
+    public String message;
+
+    // HTTP status code.
+    public int status;
+  }
+
+  public Result result;
+  // for future extension, we may add inputs / results for bulk checks.
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
new file mode 100644
index 0000000..80a537c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+import com.google.gerrit.common.Nullable;
+
+public class AccessCheckInput {
+  public String account;
+  public String project;
+
+  @Nullable public String ref;
+
+  public AccessCheckInput(String account, String project, @Nullable String ref) {
+    this.account = account;
+    this.project = project;
+    this.ref = ref;
+  }
+
+  public AccessCheckInput() {}
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index ee0960c..2280396 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -36,6 +36,8 @@
 
   ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
 
+  AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -75,5 +77,10 @@
     public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) {
       throw new NotImplementedException();
     }
+
+    @Override
+    public AccessCheckInfo checkAccess(AccessCheckInput in) {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 2cb8384..49e4a05 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -36,6 +36,7 @@
   public Timestamp updated;
   public Timestamp submitted;
   public Boolean starred;
+  public Boolean muted;
   public Collection<String> stars;
   public Boolean reviewed;
   public SubmitType submitType;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 3cac62c..abcfe31 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -136,6 +136,8 @@
 
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
 
+  public final native boolean muted() /*-{ return this.muted ? true : false; }-*/;
+
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
 
   public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index db8301a..52da8e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -136,6 +136,7 @@
     suggestions.add("is:merged");
     suggestions.add("is:abandoned");
     suggestions.add("is:mergeable");
+    suggestions.add("is:ignored");
 
     suggestions.add("status:");
     suggestions.add("status:open");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index f37cbc2..f8bda64 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -101,7 +101,7 @@
         + who
         + " -owner:"
         + who
-        + " -star:ignore) OR assignee:"
+        + " -is:ignored) OR assignee:"
         + who
         + ")";
   }
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
index 57bc7f4..64d16c5 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
@@ -65,13 +65,16 @@
       var login_form = document.getElementById('login_form');
       var f_user = document.getElementById('f_user');
       var f_pass = document.getElementById('f_pass');
-      f_user.onkeydown = function(e) {
+
+      // Keyup event must be used to avoid issue with Firefox autocomplete
+      // Issue #6083
+      f_user.onkeyup = function(e) {
         if (e.keyCode == 13) {
           f_pass.focus();
           return false;
         }
       }
-      f_pass.onkeydown = function(e) {
+      f_pass.onkeyup = function(e) {
         if (e.keyCode == 13) {
           login_form.submit();
           return false;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 268bb12..9c1541c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -75,6 +75,7 @@
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
+import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.H2AccountPatchReviewStore;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
@@ -367,6 +368,7 @@
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new SearchingChangeCacheImpl.Module(slave));
     modules.add(new InternalAccountDirectory.Module());
+    modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
     if (emailModule != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 8d2289a..6c342c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
@@ -56,6 +57,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.stream.StreamSupport;
@@ -125,6 +127,8 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final NotesMigration migration;
+  private final PatchListCache patchListCache;
+  private final PatchSetUtil psUtil;
   private final String serverId;
 
   @Inject
@@ -132,10 +136,14 @@
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
       NotesMigration migration,
+      PatchListCache patchListCache,
+      PatchSetUtil psUtil,
       @GerritServerId String serverId) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.migration = migration;
+    this.patchListCache = patchListCache;
+    this.psUtil = psUtil;
     this.serverId = serverId;
   }
 
@@ -500,4 +508,35 @@
     return COMMENT_ORDER.sortedCopy(
         FluentIterable.from(comments).transform(plc -> plc.asComment(serverId)));
   }
+
+  public void publish(
+      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag)
+      throws OrmException {
+    ChangeNotes notes = ctx.getNotes();
+    checkArgument(notes != null);
+    if (drafts.isEmpty()) {
+      return;
+    }
+
+    Map<PatchSet.Id, PatchSet> patchSets =
+        psUtil.getAsMap(
+            ctx.getDb(), notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
+    for (Comment d : drafts) {
+      PatchSet ps = patchSets.get(psId(notes, d));
+      if (ps == null) {
+        throw new OrmException("patch set " + ps + " not found");
+      }
+      d.writtenOn = ctx.getWhen();
+      d.tag = tag;
+      // Draft may have been created by a different real user; copy the current real user. (Only
+      // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
+      ctx.getUser().updateRealAccountId(d::setRealAuthor);
+      setCommentRevId(d, patchListCache, notes.getChange(), ps);
+    }
+    putComments(ctx.getDb(), ctx.getUpdate(psId), PatchLineComment.Status.PUBLISHED, drafts);
+  }
+
+  private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
+    return new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
index ab942ca..c23d990 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
@@ -16,11 +16,17 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
 import static com.google.gerrit.server.notedb.PatchSetState.DRAFT;
 import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED;
+import static java.util.function.Function.identity;
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -36,6 +42,7 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -63,8 +70,7 @@
   public ImmutableCollection<PatchSet> byChange(ReviewDb db, ChangeNotes notes)
       throws OrmException {
     if (!migration.readChanges()) {
-      return ChangeUtil.PS_ID_ORDER.immutableSortedCopy(
-          db.patchSets().byChange(notes.getChangeId()));
+      return PS_ID_ORDER.immutableSortedCopy(db.patchSets().byChange(notes.getChangeId()));
     }
     return notes.load().getPatchSets().values();
   }
@@ -73,8 +79,7 @@
       throws OrmException {
     if (!migration.readChanges()) {
       ImmutableMap.Builder<PatchSet.Id, PatchSet> result = ImmutableMap.builder();
-      for (PatchSet ps :
-          ChangeUtil.PS_ID_ORDER.sortedCopy(db.patchSets().byChange(notes.getChangeId()))) {
+      for (PatchSet ps : PS_ID_ORDER.sortedCopy(db.patchSets().byChange(notes.getChangeId()))) {
         result.put(ps.getId(), ps);
       }
       return result.build();
@@ -82,6 +87,17 @@
     return notes.load().getPatchSets();
   }
 
+  public ImmutableMap<PatchSet.Id, PatchSet> getAsMap(
+      ReviewDb db, ChangeNotes notes, Set<PatchSet.Id> patchSetIds) throws OrmException {
+    if (!migration.readChanges()) {
+      patchSetIds = Sets.filter(patchSetIds, p -> p.getParentKey().equals(notes.getChangeId()));
+      return Streams.stream(db.patchSets().get(patchSetIds))
+          .sorted(PS_ID_ORDER)
+          .collect(toImmutableMap(PatchSet::getId, identity()));
+    }
+    return ImmutableMap.copyOf(Maps.filterKeys(notes.load().getPatchSets(), patchSetIds::contains));
+  }
+
   public PatchSet insert(
       ReviewDb db,
       RevWalk rw,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 5ca9e19..cbaae1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -147,6 +147,7 @@
 
   public static final String DEFAULT_LABEL = "star";
   public static final String IGNORE_LABEL = "ignore";
+  public static final String MUTE_LABEL = "mute";
   public static final ImmutableSortedSet<String> DEFAULT_LABELS =
       ImmutableSortedSet.of(DEFAULT_LABEL);
 
@@ -341,6 +342,48 @@
     }
   }
 
+  public void ignore(Account.Id accountId, Project.NameKey project, Change.Id changeId)
+      throws OrmException {
+    star(accountId, project, changeId, ImmutableSet.of(IGNORE_LABEL), ImmutableSet.of());
+  }
+
+  public void unignore(Account.Id accountId, Project.NameKey project, Change.Id changeId)
+      throws OrmException {
+    star(accountId, project, changeId, ImmutableSet.of(), ImmutableSet.of(IGNORE_LABEL));
+  }
+
+  public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) throws OrmException {
+    return byChange(changeId, IGNORE_LABEL).contains(accountId);
+  }
+
+  private static String getMuteLabel(Change change) {
+    return MUTE_LABEL + "/" + change.currentPatchSetId().get();
+  }
+
+  public void mute(Account.Id accountId, Project.NameKey project, Change change)
+      throws OrmException {
+    star(
+        accountId,
+        project,
+        change.getId(),
+        ImmutableSet.of(getMuteLabel(change)),
+        ImmutableSet.of());
+  }
+
+  public void unmute(Account.Id accountId, Project.NameKey project, Change change)
+      throws OrmException {
+    star(
+        accountId,
+        project,
+        change.getId(),
+        ImmutableSet.of(),
+        ImmutableSet.of(getMuteLabel(change)));
+  }
+
+  public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException {
+    return byChange(change.getId(), getMuteLabel(change)).contains(accountId);
+  }
+
   private static StarRef readLabels(Repository repo, String refName) throws IOException {
     Ref ref = repo.exactRef(refName);
     if (ref == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 937d1eb..c6fc67e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -57,11 +57,13 @@
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetPastAssignees;
 import com.google.gerrit.server.change.GetTopic;
+import com.google.gerrit.server.change.Ignore;
 import com.google.gerrit.server.change.Index;
 import com.google.gerrit.server.change.ListChangeComments;
 import com.google.gerrit.server.change.ListChangeDrafts;
 import com.google.gerrit.server.change.ListChangeRobotComments;
 import com.google.gerrit.server.change.Move;
+import com.google.gerrit.server.change.Mute;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
@@ -75,6 +77,8 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestChangeReviewers;
+import com.google.gerrit.server.change.Unignore;
+import com.google.gerrit.server.change.Unmute;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.update.UpdateException;
@@ -128,6 +132,10 @@
   private final Move move;
   private final PutPrivate putPrivate;
   private final DeletePrivate deletePrivate;
+  private final Ignore ignore;
+  private final Unignore unignore;
+  private final Mute mute;
+  private final Unmute unmute;
 
   @Inject
   ChangeApiImpl(
@@ -165,6 +173,10 @@
       Move move,
       PutPrivate putPrivate,
       DeletePrivate deletePrivate,
+      Ignore ignore,
+      Unignore unignore,
+      Mute mute,
+      Unmute unmute,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -200,6 +212,10 @@
     this.move = move;
     this.putPrivate = putPrivate;
     this.deletePrivate = deletePrivate;
+    this.ignore = ignore;
+    this.unignore = unignore;
+    this.mute = mute;
+    this.unmute = unmute;
     this.change = change;
   }
 
@@ -586,4 +602,22 @@
       throw new RestApiException("Cannot index change", e);
     }
   }
+
+  @Override
+  public void ignore(boolean ignore) throws RestApiException {
+    if (ignore) {
+      this.ignore.apply(change, new Ignore.Input());
+    } else {
+      unignore.apply(change, new Unignore.Input());
+    }
+  }
+
+  @Override
+  public void mute(boolean mute) throws RestApiException {
+    if (mute) {
+      this.mute.apply(change, new Mute.Input());
+    } else {
+      unmute.apply(change, new Unmute.Input());
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index d3c5135..21b42dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.api.config;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 import com.google.gerrit.extensions.api.config.Server;
@@ -22,6 +24,7 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.CheckAccess;
 import com.google.gerrit.server.config.CheckConsistency;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetDiffPreferences;
@@ -29,6 +32,8 @@
 import com.google.gerrit.server.config.GetServerInfo;
 import com.google.gerrit.server.config.SetDiffPreferences;
 import com.google.gerrit.server.config.SetPreferences;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -43,6 +48,7 @@
   private final SetDiffPreferences setDiffPreferences;
   private final GetServerInfo getServerInfo;
   private final Provider<CheckConsistency> checkConsistency;
+  private final Provider<CheckAccess> checkAccess;
 
   @Inject
   ServerImpl(
@@ -51,13 +57,15 @@
       GetDiffPreferences getDiffPreferences,
       SetDiffPreferences setDiffPreferences,
       GetServerInfo getServerInfo,
-      Provider<CheckConsistency> checkConsistency) {
+      Provider<CheckConsistency> checkConsistency,
+      Provider<CheckAccess> checkAccess) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
     this.getServerInfo = getServerInfo;
     this.checkConsistency = checkConsistency;
+    this.checkAccess = checkAccess;
   }
 
   @Override
@@ -120,4 +128,13 @@
       throw new RestApiException("Cannot check consistency", e);
     }
   }
+
+  @Override
+  public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
+    try {
+      return checkAccess.get().apply(new ConfigResource(), in);
+    } catch (OrmException | IOException | PermissionBackendException e) {
+      throw new RestApiException("Cannot check access", e);
+    }
+  }
 }
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 4724ea1..4dc7d36 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
@@ -514,6 +514,11 @@
     if (user.isIdentifiedUser()) {
       Collection<String> stars = cd.stars(user.getAccountId());
       out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
+      out.muted =
+          stars.contains(
+                  StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId())
+              ? true
+              : null;
       if (!stars.isEmpty()) {
         out.stars = stars;
       }
@@ -521,7 +526,11 @@
 
     if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
       Account.Id accountId = user.getAccountId();
-      out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
+      if (out.muted != null) {
+        out.reviewed = true;
+      } else {
+        out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
+      }
     }
 
     out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java
new file mode 100644
index 0000000..83ab811
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Ignore
+    implements RestModifyView<ChangeResource, Ignore.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Ignore.class);
+
+  public static class Input {}
+
+  private final Provider<IdentifiedUser> self;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  Ignore(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
+    this.self = self;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Ignore")
+        .setTitle("Ignore the change")
+        .setVisible(!rsrc.isUserOwner() && !isIgnored(rsrc));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException {
+    try {
+      if (rsrc.isUserOwner() || isIgnored(rsrc)) {
+        // early exit for own changes and already ignored changes
+        return Response.ok("");
+      }
+      stars.ignore(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange().getId());
+    } catch (OrmException e) {
+      throw new RestApiException("failed to ignore change", e);
+    }
+    return Response.ok("");
+  }
+
+  private boolean isIgnored(ChangeResource rsrc) {
+    try {
+      return stars.isIgnoredBy(rsrc.getChange().getId(), self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check ignored star", e);
+    }
+    return false;
+  }
+}
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 6dd4570..5aee90c 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
@@ -87,6 +87,10 @@
     post(CHANGE_KIND, "move").to(Move.class);
     put(CHANGE_KIND, "private").to(PutPrivate.class);
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
+    put(CHANGE_KIND, "ignore").to(Ignore.class);
+    put(CHANGE_KIND, "unignore").to(Unignore.class);
+    put(CHANGE_KIND, "mute").to(Mute.class);
+    put(CHANGE_KIND, "unmute").to(Unmute.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
     get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
new file mode 100644
index 0000000..d14fec8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Mute.class);
+
+  public static class Input {}
+
+  private final Provider<IdentifiedUser> self;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  Mute(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
+    this.self = self;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Mute")
+        .setTitle("Mute the change to unhighlight it in the dashboard")
+        .setVisible(!rsrc.isUserOwner() && isMuteable(rsrc.getChange()));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException {
+    try {
+      if (rsrc.isUserOwner() || isMuted(rsrc.getChange())) {
+        // early exit for own changes and already muted changes
+        return Response.ok("");
+      }
+      stars.mute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange());
+    } catch (OrmException e) {
+      throw new RestApiException("failed to mute change", e);
+    }
+    return Response.ok("");
+  }
+
+  private boolean isMuted(Change change) {
+    try {
+      return stars.isMutedBy(change, self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check muted star", e);
+    }
+    return false;
+  }
+
+  private boolean isMuteable(Change change) {
+    try {
+      return !isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check ignored star", e);
+    }
+    return false;
+  }
+}
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 1322d9c..69aa19d 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
@@ -29,7 +29,6 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -871,12 +870,9 @@
           toDel.addAll(drafts.values());
           break;
         case PUBLISH:
-          for (Comment e : drafts.values()) {
-            toPublish.add(publishComment(ctx, e, ps));
-          }
-          break;
         case PUBLISH_ALL_REVISIONS:
-          publishAllRevisions(ctx, drafts, toPublish);
+          commentsUtil.publish(ctx, psId, drafts.values(), in.tag);
+          comments.addAll(drafts.values());
           break;
       }
       ChangeUpdate u = ctx.getUpdate(psId);
@@ -1009,37 +1005,6 @@
       return labels;
     }
 
-    private Comment publishComment(ChangeContext ctx, Comment c, PatchSet ps) throws OrmException {
-      c.writtenOn = ctx.getWhen();
-      c.tag = in.tag;
-      // Draft may have been created by a different real user; copy the current
-      // real user. (Only applies to X-Gerrit-RunAs, since modifying drafts via
-      // on_behalf_of is not allowed.)
-      ctx.getUser().updateRealAccountId(c::setRealAuthor);
-      setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps));
-      return c;
-    }
-
-    private void publishAllRevisions(
-        ChangeContext ctx, Map<String, Comment> drafts, List<Comment> ups) throws OrmException {
-      boolean needOtherPatchSets = false;
-      for (Comment c : drafts.values()) {
-        if (c.key.patchSetId != psId.get()) {
-          needOtherPatchSets = true;
-          break;
-        }
-      }
-      Map<PatchSet.Id, PatchSet> patchSets =
-          needOtherPatchSets
-              ? psUtil.byChangeAsMap(ctx.getDb(), ctx.getNotes())
-              : ImmutableMap.of(psId, ps);
-      for (Comment e : drafts.values()) {
-        ups.add(
-            publishComment(
-                ctx, e, patchSets.get(new PatchSet.Id(ctx.getChange().getId(), e.key.patchSetId))));
-      }
-    }
-
     private Map<String, Short> getAllApprovals(
         LabelTypes labelTypes, Map<String, Short> current, Map<String, Short> input) {
       Map<String, Short> allApprovals = new HashMap<>();
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 eec0641..13d5271 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
@@ -240,6 +240,9 @@
           reviewer, rsrc, ImmutableSet.of(member.getId()), null, state, notify, accountsToNotify);
     }
     if (!member.isActive()) {
+      if (allowByEmail && state == CC) {
+        return null;
+      }
       return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInactive, reviewer));
     }
     return fail(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
new file mode 100644
index 0000000..081fc22
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Unignore
+    implements RestModifyView<ChangeResource, Unignore.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Unignore.class);
+
+  public static class Input {}
+
+  private final Provider<IdentifiedUser> self;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  Unignore(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
+    this.self = self;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Unignore")
+        .setTitle("Unignore the change")
+        .setVisible(!rsrc.isUserOwner() && isIgnored(rsrc));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException {
+    try {
+      if (rsrc.isUserOwner() || !isIgnored(rsrc)) {
+        // early exit for own changes and not ignored changes
+        return Response.ok("");
+      }
+      stars.unignore(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange().getId());
+    } catch (OrmException e) {
+      throw new RestApiException("failed to unignore change", e);
+    }
+    return Response.ok("");
+  }
+
+  private boolean isIgnored(ChangeResource rsrc) {
+    try {
+      return stars.isIgnoredBy(rsrc.getChange().getId(), self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check ignored star", e);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
new file mode 100644
index 0000000..49b41cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Unmute
+    implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(Unmute.class);
+
+  public static class Input {}
+
+  private final Provider<IdentifiedUser> self;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  Unmute(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
+    this.self = self;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Unmute")
+        .setTitle("Unmute the change")
+        .setVisible(!rsrc.isUserOwner() && isUnMuteable(rsrc.getChange()));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException {
+    try {
+      if (rsrc.isUserOwner() || !isMuted(rsrc.getChange())) {
+        // early exit for own changes and not muted changes
+        return Response.ok("");
+      }
+      stars.unmute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange());
+    } catch (OrmException e) {
+      throw new RestApiException("failed to unmute change", e);
+    }
+    return Response.ok("");
+  }
+
+  private boolean isMuted(Change change) {
+    try {
+      return stars.isMutedBy(change, self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check muted star", e);
+    }
+    return false;
+  }
+
+  private boolean isUnMuteable(Change change) {
+    try {
+      return isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check ignored star", e);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
new file mode 100644
index 0000000..a80d298
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2017 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.config;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo.Result;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class CheckAccess implements RestModifyView<ConfigResource, AccessCheckInput> {
+  private final Provider<IdentifiedUser> currentUser;
+  private final AccountResolver accountResolver;
+  private final Provider<ReviewDb> db;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ProjectCache projectCache;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  CheckAccess(
+      Provider<IdentifiedUser> currentUser,
+      AccountResolver resolver,
+      Provider<ReviewDb> db,
+      IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      PermissionBackend permissionBackend) {
+    this.currentUser = currentUser;
+    this.accountResolver = resolver;
+    this.db = db;
+    this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.permissionBackend = permissionBackend;
+  }
+
+  @Override
+  public AccessCheckInfo apply(ConfigResource unused, AccessCheckInput input)
+      throws OrmException, PermissionBackendException, RestApiException, IOException {
+    permissionBackend.user(currentUser.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
+
+    if (input == null) {
+      throw new BadRequestException("input is required");
+    }
+    if (Strings.isNullOrEmpty(input.account)) {
+      throw new BadRequestException("input requires 'account'");
+    }
+    if (Strings.isNullOrEmpty(input.project)) {
+      throw new BadRequestException("input requires 'project'");
+    }
+
+    Account match = accountResolver.find(db.get(), input.account);
+    if (match == null) {
+      throw new BadRequestException(String.format("cannot find account %s", input.account));
+    }
+
+    AccessCheckInfo info = new AccessCheckInfo();
+    info.result = new Result();
+
+    Project.NameKey key = new Project.NameKey(input.project);
+    if (projectCache.get(key) == null) {
+      info.result.message = String.format("project %s does not exist", key);
+      info.result.status = HttpServletResponse.SC_NOT_FOUND;
+      return info;
+    }
+
+    IdentifiedUser user = userFactory.create(match.getId());
+    try {
+      permissionBackend.user(user).project(key).check(ProjectPermission.ACCESS);
+    } catch (AuthException | PermissionBackendException e) {
+      info.result.message =
+          String.format(
+              "user %s (%s) cannot see project %s",
+              user.getNameEmail(), user.getAccount().getId(), key);
+      info.result.status = HttpServletResponse.SC_FORBIDDEN;
+      return info;
+    }
+
+    if (!Strings.isNullOrEmpty(input.ref)) {
+      try {
+        permissionBackend
+            .user(user)
+            .ref(new Branch.NameKey(key, input.ref))
+            .check(RefPermission.READ);
+      } catch (AuthException | PermissionBackendException e) {
+        info.result.status = HttpServletResponse.SC_FORBIDDEN;
+        info.result.message =
+            String.format(
+                "user %s (%s) cannot see ref %s in project %s",
+                user.getNameEmail(), user.getAccount().getId(), input.ref, key);
+        return info;
+      }
+    }
+
+    info.result.status = HttpServletResponse.SC_OK;
+    return info;
+  }
+}
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 c41d696..8eaa6ec 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
@@ -160,7 +160,6 @@
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.CommentLinkProvider;
-import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectNode;
@@ -230,7 +229,6 @@
 
     install(new AccessControlModule());
     install(new CmdLineParserModule());
-    install(new DefaultPermissionBackendModule());
     install(new EmailModule());
     install(new ExternalIdModule());
     install(new GitModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 7bf5ad5..4f93a1a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -37,6 +37,7 @@
     get(CONFIG_KIND, "version").to(GetVersion.class);
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
     post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
+    post(CONFIG_KIND, "check.access").to(CheckAccess.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
     get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index 0e62c3a..07be8fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -32,7 +32,7 @@
 import java.util.Set;
 
 @Singleton
-class DefaultPermissionBackend extends PermissionBackend {
+public class DefaultPermissionBackend extends PermissionBackend {
   private final ProjectCache projectCache;
 
   @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
index 7a30863..4916353 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
@@ -16,18 +16,26 @@
 
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
 
 /** Binds the default {@link PermissionBackend}. */
-public class DefaultPermissionBackendModule extends FactoryModule {
+public class DefaultPermissionBackendModule extends AbstractModule {
   @Override
   protected void configure() {
     bind(PermissionBackend.class).to(DefaultPermissionBackend.class).in(Scopes.SINGLETON);
+    install(new LegacyControlsModule());
+  }
 
-    // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
-    bind(ProjectControl.GenericFactory.class);
-    factory(ProjectControl.AssistedFactory.class);
-    bind(ChangeControl.GenericFactory.class);
-    bind(ChangeControl.Factory.class);
+  /** Binds legacy ProjectControl, RefControl, ChangeControl. */
+  public static class LegacyControlsModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
+      bind(ProjectControl.GenericFactory.class);
+      factory(ProjectControl.AssistedFactory.class);
+      bind(ChangeControl.GenericFactory.class);
+      bind(ChangeControl.Factory.class);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 1ae54ee..f4bc629 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -598,6 +598,10 @@
       return new SubmittablePredicate(SubmitRecord.Status.OK);
     }
 
+    if ("ignored".equalsIgnoreCase(value)) {
+      return star("ignore");
+    }
+
     try {
       return status(value);
     } catch (IllegalArgumentException e) {
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 a596ab3..f023eb5 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
@@ -1411,7 +1411,8 @@
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
-    insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+    Change change3 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+    Change change4 = insert(repo, newChange(repo));
 
     gApi.accounts()
         .self()
@@ -1425,16 +1426,42 @@
             new StarsInput(
                 new HashSet<>(Arrays.asList(StarredChangesUtil.DEFAULT_LABEL, "green", "blue"))));
 
+    gApi.accounts()
+        .self()
+        .setStars(
+            change4.getId().toString(), new StarsInput(new HashSet<>(Arrays.asList("ignore"))));
+
     // check labeled stars
     assertQuery("star:red", change1);
     assertQuery("star:blue", change2, change1);
-    assertQuery("has:stars", change2, change1);
+    assertQuery("has:stars", change4, change2, change1);
 
     // check default star
     assertQuery("has:star", change2);
     assertQuery("is:starred", change2);
     assertQuery("starredby:self", change2);
     assertQuery("star:" + StarredChangesUtil.DEFAULT_LABEL, change2);
+
+    // check ignored
+    assertQuery("is:ignored", change4);
+    assertQuery("-is:ignored", change3, change2, change1);
+  }
+
+  @Test
+  public void byIgnore() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Account.Id user2 =
+        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+    Change change1 = insert(repo, newChange(repo), user2);
+    Change change2 = insert(repo, newChange(repo), user2);
+
+    gApi.changes().id(change1.getId().toString()).ignore(true);
+    assertQuery("is:ignored", change1);
+    assertQuery("-is:ignored", change2);
+
+    gApi.changes().id(change1.getId().toString()).ignore(false);
+    assertQuery("is:ignored");
+    assertQuery("-is:ignored", change2, change1);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 4ab7018..bce0a0f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -57,6 +57,7 @@
 import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffExecutor;
+import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.H2AccountPatchReviewStore;
 import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
@@ -149,6 +150,7 @@
             });
     bind(MetricMaker.class).to(DisabledMetricMaker.class);
     install(cfgInjector.getInstance(GerritGlobalModule.class));
+    install(new DefaultPermissionBackendModule());
     install(new SearchingChangeCacheImpl.Module());
     factory(GarbageCollection.Factory.class);
 
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index a4e8e3c..f5cd9fc 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
+import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
@@ -317,6 +318,7 @@
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new SearchingChangeCacheImpl.Module());
     modules.add(new InternalAccountDirectory.Module());
+    modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
     modules.add(new SmtpEmailSender.Module());
diff --git a/plugins/replication b/plugins/replication
index a6cba7b..8fcaee0 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit a6cba7b3ab4aa1e1ad0f0deb95078aacaa29b37d
+Subproject commit 8fcaee07b6d457b3fc6ed44d9e9d441e3cd174ac
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 30d72cb..7c12fa2 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -53,7 +53,7 @@
         "--polymer_pass",
         "--jscomp_off=duplicate",
     ],
-    language = "ECMASCRIPT5_STRICT",
+    language = "ECMASCRIPT5",
     deps = [":closure_lib"],
 )
 
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index d4c8380..d4e7ae2 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -44,6 +44,7 @@
     'is:abandoned',
     'is:closed',
     'is:draft',
+    'is:ignored',
     'is:mergeable',
     'is:merged',
     'is:open',
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 f340184..dab22d6 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
@@ -443,7 +443,7 @@
         O: options,
         q: [
           'is:open owner:self',
-          'is:open ((reviewer:self -owner:self -star:ignore) OR assignee:self)',
+          'is:open ((reviewer:self -owner:self -is:ignored) OR assignee:self)',
           'is:closed (owner:self OR reviewer:self OR assignee:self) -age:4w ' +
             'limit:10',
         ],