Merge "Don't show next arrow when there are not more changes on next page"
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index f7431ca..7c35b12 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -229,7 +229,7 @@
 To run one or more specific groups of tests:
 
 ----
-  bazel test --test_tag_filters=api,git //..
+  bazel test --test_tag_filters=api,git //...
 ----
 
 The following values are currently supported for the group name:
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index f69b955..e2be6e6 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2862,6 +2862,8 @@
 the signature.
 |`tagger`|Only set for annotated tags, if present in the tag.|The tagger as a
 link:rest-api-changes.html#git-person-info[GitPersonInfo] entity.
+|`can_delete`|`false` if not set|
+Whether the calling user can delete this tag.
 |=========================
 
 [[tag-input]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 2b5702e..9775c40 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -118,6 +118,12 @@
 special case of `reviewer:self` will find changes where the caller
 has been added as a reviewer.
 
+[[cc]]
+cc:'USER'::
++
+Changes that have the given user CC'ed on them. The special case of `cc:self`
+will find changes where the caller has been CC'ed.
+
 [[reviewerin]]
 reviewerin:'GROUP'::
 +
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index db5a300..e6f2a79 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -48,6 +48,7 @@
         "//lib/httpcomponents:httpcore",
         "//lib/jetty:servlet",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib:jimfs",
         "//lib/log:impl_log4j",
         "//lib/log:log4j",
     ],
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2cc64d8..1e0065f 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -29,8 +29,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Iterators;
 import com.google.common.collect.Sets;
+import com.google.common.jimfs.Jimfs;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.common.Nullable;
@@ -107,10 +107,14 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -121,8 +125,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -997,6 +999,7 @@
 
   protected void grantTagPermissions() throws Exception {
     grant(Permission.CREATE, project, R_TAGS + "*");
+    grant(Permission.DELETE, project, R_TAGS + "");
     grant(Permission.CREATE_TAG, project, R_TAGS + "*");
     grant(Permission.CREATE_SIGNED_TAG, project, R_TAGS + "*");
   }
@@ -1033,33 +1036,51 @@
     return ca;
   }
 
+  protected BinaryResult submitPreview(String changeId) throws Exception {
+    return gApi.changes().id(changeId).current().submitPreview();
+  }
+
+  protected BinaryResult submitPreview(String changeId, String format) throws Exception {
+    return gApi.changes().id(changeId).current().submitPreview(format);
+  }
+
+  protected Map<Branch.NameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
+    try (BinaryResult result = submitPreview(changeId)) {
+      return fetchFromBundles(result);
+    }
+  }
+
   /**
    * Fetches each bundle into a newly cloned repository, then it applies the bundle, and returns the
    * resulting tree id.
    */
-  protected Map<Branch.NameKey, RevTree> fetchFromBundles(BinaryResult bundles) throws Exception {
-
+  protected Map<Branch.NameKey, ObjectId> fetchFromBundles(BinaryResult bundles) throws Exception {
     assertThat(bundles.getContentType()).isEqualTo("application/x-zip");
 
-    File tempfile = File.createTempFile("test", null);
-    bundles.writeTo(new FileOutputStream(tempfile));
-
-    Map<Branch.NameKey, RevTree> ret = new HashMap<>();
-    try (ZipFile readback = new ZipFile(tempfile); ) {
-      for (ZipEntry entry : ImmutableList.copyOf(Iterators.forEnumeration(readback.entries()))) {
-        String bundleName = entry.getName();
-        InputStream bundleStream = readback.getInputStream(entry);
-
+    FileSystem fs = Jimfs.newFileSystem();
+    Path previewPath = fs.getPath("preview.zip");
+    try (OutputStream out = Files.newOutputStream(previewPath)) {
+      bundles.writeTo(out);
+    }
+    Map<Branch.NameKey, ObjectId> ret = new HashMap<>();
+    try (FileSystem zipFs = FileSystems.newFileSystem(previewPath, null);
+        DirectoryStream<Path> dirStream =
+            Files.newDirectoryStream(Iterables.getOnlyElement(zipFs.getRootDirectories()))) {
+      for (Path p : dirStream) {
+        if (!Files.isRegularFile(p)) {
+          continue;
+        }
+        String bundleName = p.getFileName().toString();
         int len = bundleName.length();
         assertThat(bundleName).endsWith(".git");
         String repoName = bundleName.substring(0, len - 4);
         Project.NameKey proj = new Project.NameKey(repoName);
         TestRepository<?> localRepo = cloneProject(proj);
 
-        try (TransportBundleStream tbs =
-            new TransportBundleStream(
-                localRepo.getRepository(), new URIish(bundleName), bundleStream); ) {
-
+        try (InputStream bundleStream = Files.newInputStream(p);
+            TransportBundleStream tbs =
+                new TransportBundleStream(
+                    localRepo.getRepository(), new URIish(bundleName), bundleStream)) {
           FetchResult fr =
               tbs.fetch(
                   NullProgressMonitor.INSTANCE,
@@ -1069,16 +1090,17 @@
             Branch.NameKey n = new Branch.NameKey(proj, branchName);
 
             RevCommit c = localRepo.getRevWalk().parseCommit(r.getObjectId());
-            ret.put(n, c.getTree());
+            ret.put(n, c.getTree().copy());
           }
         }
       }
     }
+    assertThat(ret).isNotEmpty();
     return ret;
   }
 
   /** Assert that the given branches have the given tree ids. */
-  protected void assertRevTrees(Project.NameKey proj, Map<Branch.NameKey, RevTree> trees)
+  protected void assertTrees(Project.NameKey proj, Map<Branch.NameKey, ObjectId> trees)
       throws Exception {
     TestRepository<?> localRepo = cloneProject(proj);
     GitUtil.fetch(localRepo, "refs/*:refs/*");
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD
index 3154b1f..4c1a62d 100644
--- a/gerrit-acceptance-tests/BUILD
+++ b/gerrit-acceptance-tests/BUILD
@@ -27,6 +27,7 @@
         "//lib:gwtjsonrpc",
         "//lib:gwtorm",
         "//lib:h2",
+        "//lib:jimfs",
         "//lib:jsch",
         "//lib:servlet-api-3_1-without-neverlink",
         "//lib/bouncycastle:bcpg",
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 0559cb0..8fa80dd 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
@@ -88,6 +88,7 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -328,8 +329,22 @@
     gApi.changes().id(r.getChangeId()).revert();
   }
 
+  @FunctionalInterface
+  private interface Rebase {
+    void call(String id) throws RestApiException;
+  }
+
   @Test
-  public void rebase() throws Exception {
+  public void rebaseViaRevisionApi() throws Exception {
+    testRebase(id -> gApi.changes().id(id).current().rebase());
+  }
+
+  @Test
+  public void rebaseViaChangeApi() throws Exception {
+    testRebase(id -> gApi.changes().id(id).rebase());
+  }
+
+  private void testRebase(Rebase rebase) throws Exception {
     // Create two changes both with the same parent
     PushOneCommit.Result r = createChange();
     testRepo.reset("HEAD~1");
@@ -342,7 +357,7 @@
 
     String changeId = r2.getChangeId();
     // Rebase the second change
-    gApi.changes().id(changeId).current().rebase();
+    rebase.call(changeId);
 
     // Second change should have 2 patch sets
     ChangeInfo c2 = gApi.changes().id(changeId).get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index cfc5937..5dceff9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testutil.ConfigSuite;
@@ -31,7 +30,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
@@ -136,10 +134,7 @@
     gApi.changes().id(id2).current().review(ReviewInput.approve());
     gApi.changes().id(id3).current().review(ReviewInput.approve());
 
-    Map<Branch.NameKey, RevTree> preview;
-    try (BinaryResult request = gApi.changes().id(id1).current().submitPreview()) {
-      preview = fetchFromBundles(request);
-    }
+    Map<Branch.NameKey, ObjectId> preview = fetchFromSubmitPreview(id1);
     gApi.changes().id(id1).current().submit();
     ObjectId subRepoId =
         subRepo
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 81f136d..6344cbe 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -93,7 +93,6 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
@@ -150,27 +149,21 @@
   public void submitToEmptyRepo() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
-    Map<Branch.NameKey, RevTree> actual;
-    try (BinaryResult request = submitPreview(change.getChangeId())) {
-      actual = fetchFromBundles(request);
-    }
+    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
     RevCommit headAfterSubmitPreview = getRemoteHead();
     assertThat(headAfterSubmitPreview).isEqualTo(initialHead);
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
     assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
-    assertRevTrees(project, actual);
+    assertTrees(project, actual);
   }
 
   @Test
   public void submitSingleChange() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change = createChange();
-    Map<Branch.NameKey, RevTree> actual;
-    try (BinaryResult request = submitPreview(change.getChangeId())) {
-      actual = fetchFromBundles(request);
-    }
+    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
     RevCommit headAfterSubmit = getRemoteHead();
     assertThat(headAfterSubmit).isEqualTo(initialHead);
     assertRefUpdatedEvents();
@@ -185,7 +178,7 @@
     }
 
     submit(change.getChangeId());
-    assertRevTrees(project, actual);
+    assertTrees(project, actual);
   }
 
   @Test
@@ -205,57 +198,59 @@
 
     try (BinaryResult request = submitPreview(change4.getChangeId())) {
       assertThat(getSubmitType()).isEqualTo(SubmitType.CHERRY_PICK);
-      Map<Branch.NameKey, RevTree> s = fetchFromBundles(request);
       submit(change4.getChangeId());
-      assertRevTrees(project, s);
     } catch (RestApiException e) {
       switch (getSubmitType()) {
         case FAST_FORWARD_ONLY:
-          assertThat(e.getMessage()).isEqualTo(
-              "Failed to submit 3 changes due to the following problems:\n"
-                  + "Change "
-                  + change2.getChange().getId()
-                  + ": internal error: "
-                  + "change not processed by merge strategy\n"
-                  + "Change "
-                  + change3.getChange().getId()
-                  + ": internal error: "
-                  + "change not processed by merge strategy\n"
-                  + "Change "
-                  + change4.getChange().getId()
-                  + ": Project policy "
-                  + "requires all submissions to be a fast-forward. Please "
-                  + "rebase the change locally and upload again for review.");
+          assertThat(e.getMessage())
+              .isEqualTo(
+                  "Failed to submit 3 changes due to the following problems:\n"
+                      + "Change "
+                      + change2.getChange().getId()
+                      + ": internal error: "
+                      + "change not processed by merge strategy\n"
+                      + "Change "
+                      + change3.getChange().getId()
+                      + ": internal error: "
+                      + "change not processed by merge strategy\n"
+                      + "Change "
+                      + change4.getChange().getId()
+                      + ": Project policy "
+                      + "requires all submissions to be a fast-forward. Please "
+                      + "rebase the change locally and upload again for review.");
           break;
         case REBASE_IF_NECESSARY:
         case REBASE_ALWAYS:
           String change2hash = change2.getChange().currentPatchSet().getRevision().get();
-          assertThat(e.getMessage()).isEqualTo(
-              "Cannot rebase "
-                  + change2hash
-                  + ": The change could "
-                  + "not be rebased due to a conflict during merge.");
+          assertThat(e.getMessage())
+              .isEqualTo(
+                  "Cannot rebase "
+                      + change2hash
+                      + ": The change could "
+                      + "not be rebased due to a conflict during merge.");
           break;
         case MERGE_ALWAYS:
         case MERGE_IF_NECESSARY:
-          assertThat(e.getMessage()).isEqualTo(
-              "Failed to submit 3 changes due to the following problems:\n"
-                  + "Change "
-                  + change2.getChange().getId()
-                  + ": Change could not be "
-                  + "merged due to a path conflict. Please rebase the change "
-                  + "locally and upload the rebased commit for review.\n"
-                  + "Change "
-                  + change3.getChange().getId()
-                  + ": Change could not be "
-                  + "merged due to a path conflict. Please rebase the change "
-                  + "locally and upload the rebased commit for review.\n"
-                  + "Change "
-                  + change4.getChange().getId()
-                  + ": Change could not be "
-                  + "merged due to a path conflict. Please rebase the change "
-                  + "locally and upload the rebased commit for review.");
+          assertThat(e.getMessage())
+              .isEqualTo(
+                  "Failed to submit 3 changes due to the following problems:\n"
+                      + "Change "
+                      + change2.getChange().getId()
+                      + ": Change could not be "
+                      + "merged due to a path conflict. Please rebase the change "
+                      + "locally and upload the rebased commit for review.\n"
+                      + "Change "
+                      + change3.getChange().getId()
+                      + ": Change could not be "
+                      + "merged due to a path conflict. Please rebase the change "
+                      + "locally and upload the rebased commit for review.\n"
+                      + "Change "
+                      + change4.getChange().getId()
+                      + ": Change could not be "
+                      + "merged due to a path conflict. Please rebase the change "
+                      + "locally and upload the rebased commit for review.");
           break;
+        case CHERRY_PICK:
         default:
           fail("Should not reach here.");
           break;
@@ -276,10 +271,7 @@
     PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
     // change 2 is not approved, but we ignore labels
     approve(change3.getChangeId());
-    Map<Branch.NameKey, RevTree> actual;
-    try (BinaryResult request = submitPreview(change4.getChangeId())) {
-      actual = fetchFromBundles(request);
-    }
+    Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change4.getChangeId());
     Map<String, Map<String, Integer>> expected = new HashMap<>();
     expected.put(project.get(), new HashMap<>());
     expected.get(project.get()).put("refs/heads/master", 3);
@@ -306,7 +298,7 @@
     // now check we actually have the same content:
     approve(change2.getChangeId());
     submit(change4.getChangeId());
-    assertRevTrees(project, actual);
+    assertTrees(project, actual);
   }
 
   @Test
@@ -842,14 +834,6 @@
     assertMerged(change.changeId);
   }
 
-  protected BinaryResult submitPreview(String changeId) throws Exception {
-    return gApi.changes().id(changeId).current().submitPreview();
-  }
-
-  protected BinaryResult submitPreview(String changeId, String format) throws Exception {
-    return gApi.changes().id(changeId).current().submitPreview(format);
-  }
-
   protected void assertSubmittable(String changeId) throws Exception {
     assertThat(get(changeId, SUBMITTABLE).submittable)
         .named("submit bit on ChangeInfo")
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 96b982d..04151e9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -41,8 +41,8 @@
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
@@ -171,10 +171,7 @@
     approve(change3.getChangeId());
 
     // get a preview before submitting:
-    Map<Branch.NameKey, RevTree> preview;
-    try (BinaryResult request = submitPreview(change1b.getChangeId())) {
-      preview = fetchFromBundles(request);
-    }
+    Map<Branch.NameKey, ObjectId> preview = fetchFromSubmitPreview(change1b.getChangeId());
     submit(change1b.getChangeId());
 
     RevCommit tip1 = getRemoteLog(p1, "master").get(0);
@@ -191,13 +188,13 @@
       assertThat(preview).hasSize(3);
 
       assertThat(preview).containsKey(new Branch.NameKey(p1, "refs/heads/master"));
-      assertRevTrees(p1, preview);
+      assertTrees(p1, preview);
 
       assertThat(preview).containsKey(new Branch.NameKey(p2, "refs/heads/master"));
-      assertRevTrees(p2, preview);
+      assertTrees(p2, preview);
 
       assertThat(preview).containsKey(new Branch.NameKey(p3, "refs/heads/master"));
-      assertRevTrees(p3, preview);
+      assertTrees(p3, preview);
     } else {
       assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
       assertThat(tip3.getShortMessage()).isEqualTo(initialHead3.getShortMessage());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 03459da..f191681 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -170,11 +170,17 @@
     TagInfo result = tag(input.ref).create(input).get();
     assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
     assertThat(result.revision).isEqualTo(input.revision);
+    assertThat(result.canDelete).isTrue();
 
     input.ref = "refs/tags/v2.0";
     result = tag(input.ref).create(input).get();
     assertThat(result.ref).isEqualTo(input.ref);
     assertThat(result.revision).isEqualTo(input.revision);
+    assertThat(result.canDelete).isTrue();
+
+    setApiUser(user);
+    result = tag(input.ref).get();
+    assertThat(result.canDelete).isFalse();
 
     eventRecorder.assertRefUpdatedEvents(project.get(), result.ref, null, result.revision);
   }
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 8d14326..27fdc18 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
@@ -114,6 +114,12 @@
   /** Publishes a draft change. */
   void publish() throws RestApiException;
 
+  /** Rebase the current revision of a change using default options. */
+  void rebase() throws RestApiException;
+
+  /** Rebase the current revision of a change. */
+  void rebase(RebaseInput in) throws RestApiException;
+
   /** Deletes a change. */
   void delete() throws RestApiException;
 
@@ -316,6 +322,16 @@
     }
 
     @Override
+    public void rebase() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void rebase(RebaseInput in) {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void delete() {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index 2b57781..ccfea46 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -21,13 +21,20 @@
   public String message;
   public GitPerson tagger;
 
-  public TagInfo(String ref, String revision) {
+  public TagInfo(String ref, String revision, boolean canDelete) {
     this.ref = ref;
     this.revision = revision;
+    this.canDelete = canDelete;
   }
 
-  public TagInfo(String ref, String revision, String object, String message, GitPerson tagger) {
-    this(ref, revision);
+  public TagInfo(
+      String ref,
+      String revision,
+      String object,
+      String message,
+      GitPerson tagger,
+      boolean canDelete) {
+    this(ref, revision, canDelete);
     this.object = object;
     this.message = message;
     this.tagger = tagger;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 7d22e9b..b44cd1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -49,10 +49,18 @@
 
   String branchCreationConfirmationMessage();
 
+  String tagCreationDialogTitle();
+
+  String tagCreationConfirmationMessage();
+
   String branchDeletionDialogTitle();
 
   String branchDeletionConfirmationMessage();
 
+  String tagDeletionDialogTitle();
+
+  String tagDeletionConfirmationMessage();
+
   String newUi();
 
   String notSignedInTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index b4665b2..9aa4388 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -20,9 +20,15 @@
 branchCreationDialogTitle = Branch Creation
 branchCreationConfirmationMessage = The following branch was successfully created:
 
+tagCreationDialogTitle = Tag Creation
+tagCreationConfirmationMessage = The following tag was successfully created:
+
 branchDeletionDialogTitle = Branch Deletion
 branchDeletionConfirmationMessage = Do you really want to delete the following branches?
 
+tagDeletionDialogTitle = Tag Deletion
+tagDeletionConfirmationMessage = Do you really want to delete the following tags?
+
 newUi = New UI
 
 notSignedInTitle = Code Review - Session Expired
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 165d0ca..5c0508e 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
@@ -40,7 +40,8 @@
                   "author:",
                   "committer:",
                   "from:",
-                  "assignee:"),
+                  "assignee:",
+                  "cc:"),
               new AccountSuggestOracle() {
                 @Override
                 public void onRequestSuggestions(final Request request, final Callback done) {
@@ -152,6 +153,7 @@
     suggestions.add("unresolved:");
 
     if (Gerrit.isNoteDbEnabled()) {
+      suggestions.add("cc:");
       suggestions.add("hashtag:");
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index b8da3a7..d7fb072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -27,6 +27,8 @@
 
   String defaultBranchName();
 
+  String defaultTagName();
+
   String defaultRevisionSpec();
 
   String buttonDeleteIncludedGroup();
@@ -171,18 +173,24 @@
 
   String columnBranchRevision();
 
+  String columnTagName();
+
+  String columnTagRevision();
+
   String initialRevision();
 
   String buttonAddBranch();
 
   String buttonDeleteBranch();
 
+  String buttonAddTag();
+
+  String buttonDeleteTag();
+
   String saveHeadButton();
 
   String cancelHeadButton();
 
-  String columnTagName();
-
   String groupItemHelp();
 
   String groupListTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 2f13a50..465bcfc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -1,6 +1,7 @@
 defaultAccountName = Name or Email
 defaultAccountGroupName = Group Name
 defaultBranchName = Branch Name
+defaultTagName = Tag Name
 defaultRevisionSpec = Revision (Branch or SHA-1)
 
 buttonDeleteIncludedGroup = Delete
@@ -81,12 +82,15 @@
 
 columnBranchName = Branch Name
 columnBranchRevision = Revision
+columnTagName = Tag Name
+columnTagRevision = Revision
 initialRevision = Initial Revision
 buttonAddBranch = Create Branch
+buttonAddTag = Create Tag
 buttonDeleteBranch = Delete
+buttonDeleteTag = Delete
 saveHeadButton = Save
 cancelHeadButton = Cancel
-columnTagName = Tag Name
 
 groupItemHelp = group
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
index e64f569..b89139c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -16,32 +16,63 @@
 
 import static com.google.gerrit.client.ui.Util.highlight;
 
+import com.google.gerrit.client.ConfirmationCallback;
+import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.access.AccessMap;
+import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.projects.TagInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.InlineHTML;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 public class ProjectTagsScreen extends PaginatedProjectScreen {
-  private NpTextBox filterTxt;
-  private Query query;
   private Hyperlink prev;
   private Hyperlink next;
-  private TagsTable tagsTable;
+  private TagsTable tagTable;
+  private Button delTag;
+  private Button addTag;
+  private HintTextBox nameTxtBox;
+  private HintTextBox irevTxtBox;
+  private FlowPanel addPanel;
+  private NpTextBox filterTxt;
+  private Query query;
 
   public ProjectTagsScreen(Project.NameKey toShow) {
     super(toShow);
@@ -53,30 +84,106 @@
   }
 
   @Override
+  protected void onLoad() {
+    super.onLoad();
+    addPanel.setVisible(false);
+    AccessMap.get(
+        getProjectKey(),
+        new GerritCallback<ProjectAccessInfo>() {
+          @Override
+          public void onSuccess(ProjectAccessInfo result) {
+            addPanel.setVisible(result.canAddRefs());
+          }
+        });
+    query = new Query(match).start(start).run();
+    savedPanel = TAGS;
+  }
+
+  private void updateForm() {
+    tagTable.updateDeleteButton();
+    addTag.setEnabled(true);
+    nameTxtBox.setEnabled(true);
+    irevTxtBox.setEnabled(true);
+  }
+
+  @Override
   protected void onInitUI() {
     super.onInitUI();
     initPageHeader();
+
     prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
     next = PagingHyperlink.createNext();
     next.setVisible(false);
 
-    tagsTable = new TagsTable();
+    addPanel = new FlowPanel();
+
+    Grid addGrid = new Grid(2, 2);
+    addGrid.setStyleName(Gerrit.RESOURCES.css().addBranch());
+    int texBoxLength = 50;
+
+    nameTxtBox = new HintTextBox();
+    nameTxtBox.setVisibleLength(texBoxLength);
+    nameTxtBox.setHintText(AdminConstants.I.defaultTagName());
+    nameTxtBox.addKeyPressHandler(
+        new KeyPressHandler() {
+          @Override
+          public void onKeyPress(KeyPressEvent event) {
+            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+              doAddNewTag();
+            }
+          }
+        });
+    addGrid.setText(0, 0, AdminConstants.I.columnTagName() + ":");
+    addGrid.setWidget(0, 1, nameTxtBox);
+
+    irevTxtBox = new HintTextBox();
+    irevTxtBox.setVisibleLength(texBoxLength);
+    irevTxtBox.setHintText(AdminConstants.I.defaultRevisionSpec());
+    irevTxtBox.addKeyPressHandler(
+        new KeyPressHandler() {
+          @Override
+          public void onKeyPress(KeyPressEvent event) {
+            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+              doAddNewTag();
+            }
+          }
+        });
+    addGrid.setText(1, 0, AdminConstants.I.initialRevision() + ":");
+    addGrid.setWidget(1, 1, irevTxtBox);
+
+    addTag = new Button(AdminConstants.I.buttonAddTag());
+    addTag.addClickHandler(
+        new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            doAddNewTag();
+          }
+        });
+    addPanel.add(addGrid);
+    addPanel.add(addTag);
+
+    tagTable = new TagsTable();
+
+    delTag = new Button(AdminConstants.I.buttonDeleteTag());
+    delTag.setStyleName(Gerrit.RESOURCES.css().branchTableDeleteButton());
+    delTag.addClickHandler(
+        new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            tagTable.deleteChecked();
+          }
+        });
 
     HorizontalPanel buttons = new HorizontalPanel();
     buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(delTag);
     buttons.add(prev);
     buttons.add(next);
-    add(tagsTable);
+    add(tagTable);
     add(buttons);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    query = new Query(match).start(start).run();
-    savedPanel = TAGS;
+    add(addPanel);
   }
 
   private void initPageHeader() {
@@ -95,8 +202,10 @@
             Query q = new Query(filterTxt.getValue());
             if (match.equals(q.qMatch)) {
               q.start(start);
-            } else if (query == null) {
-              q.run();
+            } else {
+              if (query == null) {
+                q.run();
+              }
               query = q;
             }
           }
@@ -105,16 +214,184 @@
     add(hp);
   }
 
+  private void doAddNewTag() {
+    String tagName = nameTxtBox.getText().trim();
+    if (tagName.isEmpty()) {
+      nameTxtBox.setFocus(true);
+      return;
+    }
+
+    String rev = irevTxtBox.getText().trim();
+    if (rev.isEmpty()) {
+      irevTxtBox.setText("HEAD");
+      Scheduler.get()
+          .scheduleDeferred(
+              new ScheduledCommand() {
+                @Override
+                public void execute() {
+                  irevTxtBox.selectAll();
+                  irevTxtBox.setFocus(true);
+                }
+              });
+      return;
+    }
+
+    addTag.setEnabled(false);
+    ProjectApi.createTag(
+        getProjectKey(),
+        tagName,
+        rev,
+        new GerritCallback<TagInfo>() {
+          @Override
+          public void onSuccess(TagInfo tag) {
+            showAddedTag(tag);
+            nameTxtBox.setText("");
+            irevTxtBox.setText("");
+            query = new Query(match).start(start).run();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            addTag.setEnabled(true);
+            selectAllAndFocus(nameTxtBox);
+            new ErrorDialog(caught.getMessage()).center();
+          }
+        });
+  }
+
+  void showAddedTag(TagInfo tag) {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.openElement("b");
+    b.append(Gerrit.C.tagCreationConfirmationMessage());
+    b.closeElement("b");
+
+    b.openElement("p");
+    b.append(tag.ref());
+    b.closeElement("p");
+
+    ConfirmationDialog confirmationDialog =
+        new ConfirmationDialog(
+            Gerrit.C.tagCreationDialogTitle(),
+            b.toSafeHtml(),
+            new ConfirmationCallback() {
+              @Override
+              public void onOk() {
+                //do nothing
+              }
+            });
+    confirmationDialog.center();
+    confirmationDialog.setCancelVisible(false);
+  }
+
+  private static void selectAllAndFocus(TextBox textBox) {
+    textBox.selectAll();
+    textBox.setFocus(true);
+  }
+
   private class TagsTable extends NavigationTable<TagInfo> {
+    private ValueChangeHandler<Boolean> updateDeleteHandler;
+    boolean canDelete;
 
     TagsTable() {
       table.setWidth("");
-      table.setText(0, 1, AdminConstants.I.columnTagName());
-      table.setText(0, 2, AdminConstants.I.columnBranchRevision());
+      table.setText(0, 2, AdminConstants.I.columnTagName());
+      table.setText(0, 3, AdminConstants.I.columnTagRevision());
 
       FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
       fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+
+      updateDeleteHandler =
+          new ValueChangeHandler<Boolean>() {
+            @Override
+            public void onValueChange(ValueChangeEvent<Boolean> event) {
+              updateDeleteButton();
+            }
+          };
+    }
+
+    Set<String> getCheckedRefs() {
+      Set<String> refs = new HashSet<>();
+      for (int row = 1; row < table.getRowCount(); row++) {
+        TagInfo k = getRowItem(row);
+        if (k != null
+            && table.getWidget(row, 1) instanceof CheckBox
+            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          refs.add(k.ref());
+        }
+      }
+      return refs;
+    }
+
+    void setChecked(Set<String> refs) {
+      for (int row = 1; row < table.getRowCount(); row++) {
+        TagInfo k = getRowItem(row);
+        if (k != null && refs.contains(k.ref()) && table.getWidget(row, 1) instanceof CheckBox) {
+          ((CheckBox) table.getWidget(row, 1)).setValue(true);
+        }
+      }
+    }
+
+    void deleteChecked() {
+      final Set<String> refs = getCheckedRefs();
+
+      SafeHtmlBuilder b = new SafeHtmlBuilder();
+      b.openElement("b");
+      b.append(Gerrit.C.tagDeletionConfirmationMessage());
+      b.closeElement("b");
+
+      b.openElement("p");
+      boolean first = true;
+      for (String ref : refs) {
+        if (!first) {
+          b.append(",").br();
+        }
+        b.append(ref);
+        first = false;
+      }
+      b.closeElement("p");
+
+      if (refs.isEmpty()) {
+        updateDeleteButton();
+        return;
+      }
+
+      delTag.setEnabled(false);
+      ConfirmationDialog confirmationDialog =
+          new ConfirmationDialog(
+              Gerrit.C.tagDeletionDialogTitle(),
+              b.toSafeHtml(),
+              new ConfirmationCallback() {
+                @Override
+                public void onOk() {
+                  deleteTags(refs);
+                }
+
+                @Override
+                public void onCancel() {
+                  tagTable.updateDeleteButton();
+                }
+              });
+      confirmationDialog.center();
+    }
+
+    private void deleteTags(final Set<String> tags) {
+      ProjectApi.deleteTags(
+          getProjectKey(),
+          tags,
+          new GerritCallback<VoidResult>() {
+            @Override
+            public void onSuccess(VoidResult result) {
+              query = new Query(match).start(start).run();
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              query = new Query(match).start(start).run();
+              super.onFailure(caught);
+            }
+          });
     }
 
     void display(List<TagInfo> tags) {
@@ -122,6 +399,8 @@
     }
 
     void displaySubset(List<TagInfo> tags, int fromIndex, int toIndex) {
+      canDelete = false;
+
       while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
       }
@@ -135,22 +414,52 @@
     }
 
     void populate(int row, TagInfo k) {
-      table.setWidget(row, 1, new InlineHTML(highlight(k.getShortName(), match)));
+      if (k.canDelete()) {
+        CheckBox sel = new CheckBox();
+        sel.addValueChangeHandler(updateDeleteHandler);
+        table.setWidget(row, 1, sel);
+        canDelete = true;
+      } else {
+        table.setText(row, 1, "");
+      }
+
+      table.setWidget(row, 2, new InlineHTML(highlight(k.getShortName(), match)));
 
       if (k.revision() != null) {
-        table.setText(row, 2, k.revision());
+        table.setText(row, 3, k.revision());
       } else {
-        table.setText(row, 2, "");
+        table.setText(row, 3, "");
       }
 
       FlexCellFormatter fmt = table.getFlexCellFormatter();
+      String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
       String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
-      fmt.addStyleName(row, 1, dataCellStyle);
+      fmt.addStyleName(row, 1, iconCellStyle);
       fmt.addStyleName(row, 2, dataCellStyle);
+      fmt.addStyleName(row, 3, dataCellStyle);
 
       setRowItem(row, k);
     }
 
+    boolean hasTagCanDelete() {
+      return canDelete;
+    }
+
+    void updateDeleteButton() {
+      boolean on = false;
+      for (int row = 1; row < table.getRowCount(); row++) {
+        Widget w = table.getWidget(row, 1);
+        if (w != null && w instanceof CheckBox) {
+          CheckBox sel = (CheckBox) w;
+          if (sel.getValue()) {
+            on = true;
+            break;
+          }
+        }
+      }
+      delTag.setEnabled(on);
+    }
+
     @Override
     protected void onOpenRow(int row) {
       if (row > 0) {
@@ -216,10 +525,10 @@
       ProjectTagsScreen.this.start = qStart;
 
       if (result.length() <= pageSize) {
-        tagsTable.display(Natives.asList(result));
+        tagTable.display(Natives.asList(result));
         next.setVisible(false);
       } else {
-        tagsTable.displaySubset(Natives.asList(result), 0, result.length() - 1);
+        tagTable.displaySubset(Natives.asList(result), 0, result.length() - 1);
         setupNavigationLink(next, qMatch, qStart + pageSize);
       }
       if (qStart > 0) {
@@ -228,6 +537,11 @@
         prev.setVisible(false);
       }
 
+      delTag.setVisible(tagTable.hasTagCanDelete());
+      Set<String> checkedRefs = tagTable.getCheckedRefs();
+      tagTable.setChecked(checkedRefs);
+      updateForm();
+
       if (!isCurrentView()) {
         display();
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index d1343df..4be877e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -60,6 +60,14 @@
     return call;
   }
 
+  /** Create a new tag */
+  public static void createTag(
+      Project.NameKey name, String ref, String revision, AsyncCallback<TagInfo> cb) {
+    TagInput input = TagInput.create();
+    input.setRevision(revision);
+    project(name).view("tags").id(ref).ifNoneMatch().put(input, cb);
+  }
+
   /** Retrieve all visible tags of the project */
   public static void getTags(Project.NameKey name, AsyncCallback<JsArray<TagInfo>> cb) {
     project(name).view("tags").get(cb);
@@ -74,6 +82,20 @@
     getRestApi(name, "tags", limit, start, match).get(cb);
   }
 
+  /** Delete tags. One call is fired to the server to delete all the tags. */
+  public static void deleteTags(
+      Project.NameKey name, Set<String> refs, AsyncCallback<VoidResult> cb) {
+    if (refs.size() == 1) {
+      project(name).view("tags").id(refs.iterator().next()).delete(cb);
+    } else {
+      DeleteTagsInput d = DeleteTagsInput.create();
+      for (String ref : refs) {
+        d.addTag(ref);
+      }
+      project(name).view("tags:delete").post(d, cb);
+    }
+  }
+
   /** Create a new branch */
   public static void createBranch(
       Project.NameKey name, String ref, String revision, AsyncCallback<BranchInfo> cb) {
@@ -325,6 +347,16 @@
     public final native void put(String n, ConfigParameterValue v) /*-{ this[n] = v; }-*/;
   }
 
+  private static class TagInput extends JavaScriptObject {
+    static TagInput create() {
+      return (TagInput) createObject();
+    }
+
+    protected TagInput() {}
+
+    final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
+  }
+
   private static class BranchInput extends JavaScriptObject {
     static BranchInput create() {
       return (BranchInput) createObject();
@@ -355,6 +387,20 @@
     final native void setRef(String r) /*-{ if(r)this.ref=r; }-*/;
   }
 
+  private static class DeleteTagsInput extends JavaScriptObject {
+    static DeleteTagsInput create() {
+      DeleteTagsInput d = createObject().cast();
+      d.init();
+      return d;
+    }
+
+    protected DeleteTagsInput() {}
+
+    final native void init() /*-{ this.tags = []; }-*/;
+
+    final native void addTag(String b) /*-{ this.tags.push(b); }-*/;
+  }
+
   private static class DeleteBranchesInput extends JavaScriptObject {
     static DeleteBranchesInput create() {
       DeleteBranchesInput d = createObject().cast();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
index 1657b25..24487ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.projects;
 
 public class TagInfo extends RefInfo {
+  public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
 
   // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
   protected TagInfo() {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 9a294c9..ebed0f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -117,7 +117,11 @@
         getClass().getSimpleName() + " is not an IdentifiedUser");
   }
 
-  /** Return account ID if {@link #isIdentifiedUser} is true. */
+  /**
+   * Return account ID if {@link #isIdentifiedUser} is true.
+   *
+   * @throws UnsupportedOperationException if the user is not logged in.
+   */
   public Account.Id getAccountId() {
     throw new UnsupportedOperationException(
         getClass().getSimpleName() + " is not an IdentifiedUser");
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 52353ca..5c398ea 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.api.changes;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -24,6 +25,7 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
@@ -63,6 +65,7 @@
 import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.PutAssignee;
 import com.google.gerrit.server.change.PutTopic;
+import com.google.gerrit.server.change.Rebase;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Reviewers;
@@ -99,6 +102,7 @@
   private final CreateMergePatchSet updateByMerge;
   private final Provider<SubmittedTogether> submittedTogether;
   private final PublishDraftPatchSet.CurrentRevision publishDraftChange;
+  private final Rebase.CurrentRevision rebase;
   private final DeleteChange deleteChange;
   private final GetTopic getTopic;
   private final PutTopic putTopic;
@@ -133,6 +137,7 @@
       CreateMergePatchSet updateByMerge,
       Provider<SubmittedTogether> submittedTogether,
       PublishDraftPatchSet.CurrentRevision publishDraftChange,
+      Rebase.CurrentRevision rebase,
       DeleteChange deleteChange,
       GetTopic getTopic,
       PutTopic putTopic,
@@ -165,6 +170,7 @@
     this.updateByMerge = updateByMerge;
     this.submittedTogether = submittedTogether;
     this.publishDraftChange = publishDraftChange;
+    this.rebase = rebase;
     this.deleteChange = deleteChange;
     this.getTopic = getTopic;
     this.putTopic = putTopic;
@@ -326,6 +332,20 @@
   }
 
   @Override
+  public void rebase() throws RestApiException {
+    rebase(new RebaseInput());
+  }
+
+  @Override
+  public void rebase(RebaseInput in) throws RestApiException {
+    try {
+      rebase.apply(change, in);
+    } catch (EmailException | OrmException | UpdateException | RestApiException | IOException e) {
+      throw new RestApiException("Cannot rebase change", e);
+    }
+  }
+
+  @Override
   public void delete() throws RestApiException {
     try {
       deleteChange.apply(change, null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 3fc9789..90c2daf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -111,11 +111,13 @@
     IdentifiedUser caller = control.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
 
-    final MergeOp op = mergeOpProvider.get();
+    @SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
+    MergeOp op = mergeOpProvider.get();
     try {
       op.merge(db, change, caller, false, new SubmitInput(), true);
       BinaryResult bin = new SubmitPreviewResult(op, f, maxBundleSize);
-      bin.disableGzip().setContentType(f.getMimeType())
+      bin.disableGzip()
+          .setContentType(f.getMimeType())
           .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
       return bin;
     } catch (OrmException | RestApiException | RuntimeException e) {
@@ -138,8 +140,7 @@
 
     @Override
     public void writeTo(OutputStream out) throws IOException {
-      try (ArchiveOutputStream aos = archiveFormat
-          .createArchiveOutputStream(out)) {
+      try (ArchiveOutputStream aos = archiveFormat.createArchiveOutputStream(out)) {
         MergeOpRepoManager orm = mergeOp.getMergeOpRepoManager();
         for (Project.NameKey p : mergeOp.getAllProjects()) {
           OpenRepo or = orm.getRepo(p);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 046712d..608a56f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RebaseUtil.Base;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -204,18 +205,19 @@
   }
 
   public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
+    private final PatchSetUtil psUtil;
     private final Rebase rebase;
 
     @Inject
-    CurrentRevision(Rebase rebase) {
+    CurrentRevision(PatchSetUtil psUtil, Rebase rebase) {
+      this.psUtil = psUtil;
       this.rebase = rebase;
     }
 
     @Override
     public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
-        throws EmailException, OrmException, UpdateException, RestApiException, IOException,
-            NoSuchChangeException {
-      PatchSet ps = rebase.dbProvider.get().patchSets().get(rsrc.getChange().currentPatchSetId());
+        throws EmailException, OrmException, UpdateException, RestApiException, IOException {
+      PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
       } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
index ed38c47..f674d17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -134,7 +134,7 @@
             result.getObjectId(),
             identifiedUser.get().getAccount());
         try (RevWalk w = new RevWalk(repo)) {
-          return ListTags.createTagInfo(result, w);
+          return ListTags.createTagInfo(result, w, refControl);
         }
       }
     } catch (InvalidRevisionException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 9312e49..7f1ee60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -121,10 +121,11 @@
 
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
+      ProjectControl control = resource.getControl();
       Map<String, Ref> all =
-          visibleTags(resource.getControl(), repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
+          visibleTags(control, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
       for (Ref ref : all.values()) {
-        tags.add(createTagInfo(ref, rw));
+        tags.add(createTagInfo(ref, rw, control.controlForRef(ref.getName())));
       }
     }
 
@@ -154,16 +155,16 @@
         tagName = Constants.R_TAGS + tagName;
       }
       Ref ref = repo.getRefDatabase().exactRef(tagName);
+      ProjectControl control = resource.getControl();
       if (ref != null
-          && !visibleTags(resource.getControl(), repo, ImmutableMap.of(ref.getName(), ref))
-              .isEmpty()) {
-        return createTagInfo(ref, rw);
+          && !visibleTags(control, repo, ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
+        return createTagInfo(ref, rw, control.controlForRef(ref.getName()));
       }
     }
     throw new ResourceNotFoundException(id);
   }
 
-  public static TagInfo createTagInfo(Ref ref, RevWalk rw)
+  public static TagInfo createTagInfo(Ref ref, RevWalk rw, RefControl control)
       throws MissingObjectException, IOException {
     RevObject object = rw.parseAny(ref.getObjectId());
     if (object instanceof RevTag) {
@@ -175,10 +176,11 @@
           tag.getName(),
           tag.getObject().getName(),
           tag.getFullMessage().trim(),
-          tagger != null ? CommonConverters.toGitPerson(tag.getTaggerIdent()) : null);
+          tagger != null ? CommonConverters.toGitPerson(tag.getTaggerIdent()) : null,
+          control.canDelete());
     }
     // Lightweight tag
-    return new TagInfo(ref.getName(), ref.getObjectId().getName());
+    return new TagInfo(ref.getName(), ref.getObjectId().getName(), control.canDelete());
   }
 
   private Repository getRepository(Project.NameKey project)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 1009ce6..8a57c73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.server.query.QueryParser.OR;
 import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
 
+import com.google.common.base.Strings;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -181,6 +182,9 @@
    *     an invalid value being passed to a recognized operator.
    */
   public Predicate<T> parse(final String query) throws QueryParseException {
+    if (Strings.isNullOrEmpty(query)) {
+      throw new QueryParseException("query is empty");
+    }
     return toPredicate(QueryParser.parse(query));
   }
 
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 aa220e1..39968f1 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
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -61,6 +62,7 @@
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ListChildProjects;
@@ -173,34 +175,35 @@
 
   @VisibleForTesting
   public static class Arguments {
-    final Provider<ReviewDb> db;
-    final Provider<InternalChangeQuery> queryProvider;
-    final ChangeIndexRewriter rewriter;
-    final DynamicMap<ChangeOperatorFactory> opFactories;
-    final DynamicMap<ChangeHasOperandFactory> hasOperands;
-    final IdentifiedUser.GenericFactory userFactory;
-    final CapabilityControl.Factory capabilityControlFactory;
-    final ChangeControl.GenericFactory changeControlGenericFactory;
-    final ChangeNotes.Factory notesFactory;
-    final ChangeData.Factory changeDataFactory;
-    final FieldDef.FillArgs fillArgs;
-    final CommentsUtil commentsUtil;
+    final AccountCache accountCache;
     final AccountResolver accountResolver;
-    final GroupBackend groupBackend;
     final AllProjectsName allProjectsName;
     final AllUsersName allUsersName;
-    final PatchListCache patchListCache;
-    final GitRepositoryManager repoManager;
-    final ProjectCache projectCache;
-    final Provider<ListChildProjects> listChildProjects;
-    final SubmitDryRun submitDryRun;
-    final ConflictsCache conflictsCache;
-    final TrackingFooters trackingFooters;
+    final CapabilityControl.Factory capabilityControlFactory;
+    final ChangeControl.GenericFactory changeControlGenericFactory;
+    final ChangeData.Factory changeDataFactory;
     final ChangeIndex index;
+    final ChangeIndexRewriter rewriter;
+    final ChangeNotes.Factory notesFactory;
+    final CommentsUtil commentsUtil;
+    final ConflictsCache conflictsCache;
+    final DynamicMap<ChangeHasOperandFactory> hasOperands;
+    final DynamicMap<ChangeOperatorFactory> opFactories;
+    final FieldDef.FillArgs fillArgs;
+    final GitRepositoryManager repoManager;
+    final GroupBackend groupBackend;
+    final IdentifiedUser.GenericFactory userFactory;
     final IndexConfig indexConfig;
+    final NotesMigration notesMigration;
+    final PatchListCache patchListCache;
+    final ProjectCache projectCache;
+    final Provider<InternalChangeQuery> queryProvider;
+    final Provider<ListChildProjects> listChildProjects;
     final Provider<ListMembers> listMembers;
+    final Provider<ReviewDb> db;
     final StarredChangesUtil starredChangesUtil;
-    final AccountCache accountCache;
+    final SubmitDryRun submitDryRun;
+    final TrackingFooters trackingFooters;
     final boolean allowsDrafts;
 
     private final Provider<CurrentUser> self;
@@ -237,7 +240,8 @@
         Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
-        @GerritServerConfig Config cfg) {
+        @GerritServerConfig Config cfg,
+        NotesMigration notesMigration) {
       this(
           db,
           queryProvider,
@@ -268,7 +272,8 @@
           listMembers,
           starredChangesUtil,
           accountCache,
-          cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
+          cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true),
+          notesMigration);
     }
 
     private Arguments(
@@ -301,7 +306,8 @@
         Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
-        boolean allowsDrafts) {
+        boolean allowsDrafts,
+        NotesMigration notesMigration) {
       this.db = db;
       this.queryProvider = queryProvider;
       this.rewriter = rewriter;
@@ -332,6 +338,7 @@
       this.accountCache = accountCache;
       this.allowsDrafts = allowsDrafts;
       this.hasOperands = hasOperands;
+      this.notesMigration = notesMigration;
     }
 
     Arguments asUser(CurrentUser otherUser) {
@@ -365,7 +372,8 @@
           listMembers,
           starredChangesUtil,
           accountCache,
-          allowsDrafts);
+          allowsDrafts,
+          notesMigration);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -553,7 +561,11 @@
     }
 
     if ("reviewer".equalsIgnoreCase(value)) {
-      return ReviewerPredicate.create(args, self());
+      return ReviewerPredicate.reviewer(args, self());
+    }
+
+    if ("cc".equalsIgnoreCase(value)) {
+      return ReviewerPredicate.cc(args, self());
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
@@ -925,12 +937,17 @@
 
   @Operator
   public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
-    Set<Account.Id> m = parseAccount(who);
-    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
-    for (Account.Id id : m) {
-      p.add(ReviewerPredicate.create(args, id));
-    }
-    return Predicate.or(p);
+    return Predicate.or(
+        parseAccount(who)
+            .stream()
+            .map(id -> ReviewerPredicate.reviewer(args, id))
+            .collect(toList()));
+  }
+
+  @Operator
+  public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException {
+    return Predicate.or(
+        parseAccount(who).stream().map(id -> ReviewerPredicate.cc(args, id)).collect(toList()));
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 4a11d28..6ce02fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -21,32 +23,50 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.stream.Stream;
 
 class ReviewerPredicate extends ChangeIndexPredicate {
-  static Predicate<ChangeData> create(Arguments args, Account.Id id) {
-    List<Predicate<ChangeData>> and = new ArrayList<>(2);
-    ReviewerStateInternal[] states = ReviewerStateInternal.values();
-    List<Predicate<ChangeData>> or = new ArrayList<>(states.length - 1);
-    for (ReviewerStateInternal state : states) {
-      if (state != ReviewerStateInternal.REMOVED) {
-        or.add(new ReviewerPredicate(state, id));
-      }
+  static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) {
+    Predicate<ChangeData> p;
+    if (args.notesMigration.readChanges()) {
+      // With NoteDb, Reviewer/CC are clearly distinct states, so only choose reviewer.
+      p = new ReviewerPredicate(ReviewerStateInternal.REVIEWER, id);
+    } else {
+      // Without NoteDb, Reviewer/CC are a bit unpredictable; maintain the old behavior of matching
+      // any reviewer state.
+      p = anyReviewerState(id);
     }
-    and.add(Predicate.or(or));
+    return create(args, p);
+  }
 
-    // TODO(dborowitz): This really belongs much higher up e.g. QueryProcessor.
+  static Predicate<ChangeData> cc(Arguments args, Account.Id id) {
+    // As noted above, CC is nebulous without NoteDb, but it certainly doesn't make sense to return
+    // Reviewers for cc:foo. Most likely this will just not match anything, but let the index sort
+    // it out.
+    return create(args, new ReviewerPredicate(ReviewerStateInternal.CC, id));
+  }
+
+  private static Predicate<ChangeData> anyReviewerState(Account.Id id) {
+    return Predicate.or(
+        Stream.of(ReviewerStateInternal.values())
+            .filter(s -> s != ReviewerStateInternal.REMOVED)
+            .map(s -> new ReviewerPredicate(s, id))
+            .collect(toList()));
+  }
+
+  private static Predicate<ChangeData> create(Arguments args, Predicate<ChangeData> p) {
     if (!args.allowsDrafts) {
-      and.add(Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT)));
+      // TODO(dborowitz): This really belongs much higher up e.g. QueryProcessor. Also, why are we
+      // even doing this?
+      return Predicate.and(p, Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT)));
     }
-    return Predicate.and(and);
+    return p;
   }
 
   private final ReviewerStateInternal state;
   private final Account.Id id;
 
-  ReviewerPredicate(ReviewerStateInternal state, Account.Id id) {
+  private ReviewerPredicate(ReviewerStateInternal state, Account.Id id) {
     super(ChangeField.REVIEWER, ChangeField.getReviewerFieldValue(state, id));
     this.state = state;
     this.id = id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 3652ec7..6fda100 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -28,7 +28,7 @@
         new ChangeQueryBuilder.Arguments(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
             null, null, null, null, null, null, null, indexes, null, null, null, null, null, null,
-            null, null));
+            null, null, null));
   }
 
   @Operator
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 1b9fc61..6bde106 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
@@ -34,6 +34,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
@@ -43,6 +44,7 @@
 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.client.ReviewerState;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -1480,17 +1482,30 @@
   }
 
   @Test
-  public void reviewer() throws Exception {
+  public void reviewerAndCc() throws Exception {
+    Account.Id user1 = createAccount("user1");
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
     insert(repo, newChange(repo));
 
-    gApi.changes().id(change1.getId().get()).current().review(ReviewInput.approve());
-    gApi.changes().id(change2.getId().get()).current().review(ReviewInput.approve());
+    AddReviewerInput rin = new AddReviewerInput();
+    rin.reviewer = user1.toString();
+    rin.state = ReviewerState.REVIEWER;
+    gApi.changes().id(change1.getId().get()).addReviewer(rin);
 
-    Account.Id id = user.getAccountId();
-    assertQuery("reviewer:" + id, change2, change1);
+    rin = new AddReviewerInput();
+    rin.reviewer = user1.toString();
+    rin.state = ReviewerState.CC;
+    gApi.changes().id(change2.getId().get()).addReviewer(rin);
+
+    if (notesMigration.readChanges()) {
+      assertQuery("reviewer:" + user1, change1);
+      assertQuery("cc:" + user1, change2);
+    } else {
+      assertQuery("reviewer:" + user1, change2, change1);
+      assertQuery("cc:" + user1);
+    }
   }
 
   @Test
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 2dc439b..7789c65 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -49,11 +49,7 @@
   private static final Logger log = LoggerFactory.getLogger(Receive.class);
 
   @Inject private AsyncReceiveCommits.Factory factory;
-
   @Inject private IdentifiedUser currentUser;
-
-  @Inject private IdentifiedUser.GenericFactory identifiedUserFactory;
-
   @Inject private SshSession session;
 
   private final Set<Account.Id> reviewerId = new HashSet<>();
@@ -92,9 +88,6 @@
       throw die(r.getMessage());
     }
 
-    verifyProjectVisible("reviewer", reviewerId);
-    verifyProjectVisible("CC", ccId);
-
     receive.init();
     receive.addReviewers(reviewerId);
     receive.addExtraCC(ccId);
@@ -172,14 +165,4 @@
       throw new Failure(128, "fatal: Unpack error, check server log", detail);
     }
   }
-
-  private void verifyProjectVisible(final String type, final Set<Account.Id> who)
-      throws UnloggedFailure {
-    for (final Account.Id id : who) {
-      final IdentifiedUser user = identifiedUserFactory.create(id);
-      if (!projectControl.forUser(user).isVisible()) {
-        throw die(type + " " + user.getAccount().getFullName() + " cannot access the project");
-      }
-    }
-  }
 }
diff --git a/plugins/hooks b/plugins/hooks
index 7156fc2..a329746 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 7156fc2b350307fd8212591e29eb6b4662f9d17d
+Subproject commit a32974634954aa904acc00644bb0beb58b1ea934
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 5314b09..7875450 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -391,7 +391,8 @@
                 on-change="_handlePatchChange">
               <template is="dom-repeat" items="[[_allPatchSets]]"
                   as="patchNum">
-                <option value$="[[patchNum.num]]">
+                <option value$="[[patchNum.num]]"
+                    disabled$="[[_computePatchSetDisabled(patchNum.num, _patchRange.basePatchNum)]]">
                   [[patchNum.num]]
                   /
                   [[_computeLatestPatchNum(_allPatchSets)]]
@@ -429,7 +430,7 @@
         <gr-file-list id="fileList"
             change="[[_change]]"
             change-num="[[_changeNum]]"
-            patch-range="[[_patchRange]]"
+            patch-range="{{_patchRange}}"
             comments="[[_comments]]"
             drafts="[[_diffDrafts]]"
             revisions="[[_change.revisions]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 7cfaf76..8eeac09 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -639,6 +639,18 @@
       return 'patchInfo--oldPatchSet';
     },
 
+    /**
+     * Determines if a patch number should be disabled based on value of the
+     * basePatchNum from gr-file-list.
+     * @param {Number} patchNum Patch number available in dropdown
+     * @param {Number|String} basePatchNum Base patch number from file list
+     * @return {Boolean}
+     */
+    _computePatchSetDisabled: function(patchNum, basePatchNum) {
+      basePatchNum = basePatchNum === 'PARENT' ? 0 : basePatchNum;
+      return parseInt(patchNum, 10) <= parseInt(basePatchNum, 10);
+    },
+
     _computeAllPatchSets: function(change) {
       var patchNums = [];
       for (var commit in change.revisions) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index a416aa2..8180dfc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -151,6 +151,19 @@
           'Add a patch set description');
     });
 
+    test('_computePatchSetDisabled', function() {
+      var basePatchNum = 'PARENT';
+      var patchNum = 1;
+      assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
+          false);
+      basePatchNum = 1;
+      assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
+          true);
+      patchNum = 2;
+      assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
+          false);
+    });
+
     test('_prepareCommitMsgForLinkify', function() {
       var commitMessage = 'R=test@google.com';
       var result = element._prepareCommitMsgForLinkify(commitMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 9cee168..760d6c7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -114,6 +114,9 @@
         margin-right: .5em;
         white-space: nowrap;
       }
+      .labelMessage {
+        color: #666;
+      }
       iron-selector {
         display: inline-flex;
       }
@@ -236,12 +239,12 @@
             config="[[projectConfig.commentlinks]]"></gr-formatted-text>
       </section>
       <section class="labelsContainer">
-        <template is="dom-repeat"
-            items="[[_labels]]" as="label">
+        <template is="dom-repeat" items="[[_labels]]" as="label">
           <div class="labelContainer">
             <span class="labelName">[[label.name]]</span>
             <iron-selector data-label$="[[label.name]]"
-                selected="[[_computeIndexOfLabelValue(change.labels, permittedLabels, label)]]">
+                selected="[[_computeIndexOfLabelValue(change.labels, permittedLabels, label)]]"
+                hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
               <template is="dom-repeat"
                   items="[[_computePermittedLabelValues(permittedLabels, label.name)]]"
                   as="value">
@@ -249,6 +252,10 @@
                     title$="[[_computeLabelValueTitle(change.labels, label.name, value)]]">[[value]]</gr-button>
               </template>
             </iron-selector>
+            <span class="labelMessage"
+                hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
+              You don't have permission to edit this label.
+            </span>
           </div>
         </template>
       </section>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index f929888..f56d85d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -425,6 +425,10 @@
       return permittedLabels[label];
     },
 
+    _computeAnyPermittedLabelValues: function(permittedLabels, label) {
+      return permittedLabels.hasOwnProperty(label);
+    },
+
     _changeUpdated: function(changeRecord, owner, serverConfig) {
       this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig);
     },
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 e9fdbd1..a225b12 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
@@ -22,6 +22,8 @@
     'author',
     'branch',
     'bug',
+    'cc',
+    'cc:self',
     'change',
     'comment',
     'commentby',
@@ -237,6 +239,7 @@
           return this._fetchProjects(predicate, expression);
 
         case 'author':
+        case 'cc':
         case 'commentby':
         case 'committer':
         case 'from':
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 8863aa6..8ef9ad6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -83,19 +83,11 @@
       .action {
         margin-right: .5em;
       }
-      .danger {
-        display: flex;
-        flex: 1;
-        justify-content: flex-end;
-      }
       .editMessage {
         display: none;
         margin: .5em 0;
         width: 100%;
       }
-      .danger .action {
-        margin-right: 0;
-      }
       .container:not(.draft) .actions .hideOnPublished {
         display: none;
       }
@@ -118,7 +110,8 @@
       .editing .quote,
       .editing .ack,
       .editing .done,
-      .editing .edit {
+      .editing .edit,
+      .editing .unresolved {
         display: none;
       }
       .editing .editMessage {
@@ -171,8 +164,12 @@
       #container.collapsed iron-autogrow-textarea {
         display: none;
       }
-      .resolve {
-        margin: auto;
+      .resolve,
+      .unresolved {
+        align-items: center;
+        display: flex;
+        flex: 1;
+        justify-content: flex-end;
       }
       .resolve label {
         color: #333;
@@ -236,6 +233,8 @@
             disabled$="[[_computeSaveDisabled(_messageText)]]">Save</gr-button>
         <gr-button class="action cancel hideOnPublished"
             on-tap="_handleCancel" hidden>Cancel</gr-button>
+        <gr-button class="action discard hideOnPublished"
+            on-tap="_handleDiscard">Discard</gr-button>
         <div class="action resolve hideOnPublished">
           <label>
             <input type="checkbox"
@@ -244,9 +243,8 @@
             Resolved
           </label>
         </div>
-        <div class="danger">
-          <gr-button class="action discard hideOnPublished"
-              on-tap="_handleDiscard">Discard</gr-button>
+        <div class="action unresolved hideOnPublished" hidden$="[[resolved]]">
+          Unresolved
         </div>
       </div>
       <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
old mode 100644
new mode 100755