Merge "Make sure plugins are not double counted" into stable-2.15
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index aabe87f..78e8512 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2175,6 +2175,19 @@
 Defaults to GWT (if GWT is enabled) or POLYGERRIT (if POLYGERRIT is
 enabled and GWT is disabled)
 
+[[gerrit.serverId]]gerrit.serverId::
++
+Used by NoteDb to, amongst other things, identify author identities from
+per-server specific account IDs.
++
+If this value is not set on startup it is automatically set to a random UUID.
++
+[NOTE]
+If this value doesn't match the serverId used when creating an already existing
+NoteDb, Gerrit will not be able to use that instance of NoteDb. The serverId
+used to create the NoteDb will show in the resulting exception message in case
+the value differs.
+
 [[gitweb]]
 === Section gitweb
 
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index 4c0ca9b..595cc3c 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>2.15.1-SNAPSHOT</version>
+  <version>2.15.1</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index e39f9f5..7e3e053 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -802,6 +802,49 @@
   }
 
   @Test
+  public void rebaseHunksDirectlyTouchingHunksOfPatchSetsNotModifiedBetweenThemAreIdentified()
+      throws Exception {
+    // Add to hunks in a patch set and remove them in a further patch set to allow rebasing.
+    Function<String, String> contentModification =
+        fileContent ->
+            fileContent.replace("Line 1\n", "Line one\n").replace("Line 3\n", "Line three\n");
+    addModifiedPatchSet(changeId, FILE_NAME, contentModification);
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    Function<String, String> reverseContentModification =
+        fileContent ->
+            fileContent.replace("Line one\n", "Line 1\n").replace("Line three\n", "Line 3\n");
+    addModifiedPatchSet(changeId, FILE_NAME, reverseContentModification);
+
+    String newFileContent = FILE_CONTENT.replace("Line 2\n", "Line two\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
+    rebaseChangeOn(changeId, commit2);
+
+    // Add the hunks again and modify another line so that we get a diff for the file.
+    // (Files with only edits due to rebase are filtered out.)
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        contentModification.andThen(fileContent -> fileContent.replace("Line 10\n", "Line ten\n")));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(1);
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 2");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line two");
+    assertThat(diffInfo).content().element(1).isDueToRebase();
+    assertThat(diffInfo).content().element(2).commonLines().hasSize(7);
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 10");
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line ten");
+    assertThat(diffInfo).content().element(3).isNotDueToRebase();
+    assertThat(diffInfo).content().element(4).commonLines().hasSize(90);
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
   public void multipleRebaseEditsMixedWithRegularEditsCanBeIdentified() throws Exception {
     addModifiedPatchSet(
         changeId,
@@ -1210,6 +1253,118 @@
     assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
   }
 
+  @Test
+  public void closeNonRebaseHunksAreCombinedForIntralineOptimizations() throws Exception {
+    assume().that(intraline).isTrue();
+
+    String fileContent = FILE_CONTENT.replace("Line 5\n", "{\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, fileContent);
+    rebaseChangeOn(changeId, commit2);
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        content -> content.replace("Line 4\n", "Line four\n").replace("Line 6\n", "Line six\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(3);
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 4", "{", "Line 6");
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .linesOfB()
+        .containsExactly("Line four", "{", "Line six");
+    assertThat(diffInfo).content().element(1).isNotDueToRebase();
+    assertThat(diffInfo).content().element(2).commonLines().hasSize(94);
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    // Lines which weren't modified but are included in a hunk due to optimization don't count for
+    // the number of inserted/deleted lines.
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+  }
+
+  @Test
+  public void closeRebaseHunksAreNotCombinedForIntralineOptimizations() throws Exception {
+    assume().that(intraline).isTrue();
+
+    String fileContent = FILE_CONTENT.replace("Line 5\n", "{\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, fileContent);
+    rebaseChangeOn(changeId, commit2);
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+
+    String newFileContent =
+        fileContent.replace("Line 4\n", "Line four\n").replace("Line 6\n", "Line six\n");
+    ObjectId commit3 = addCommit(commit1, FILE_NAME, newFileContent);
+    rebaseChangeOn(changeId, commit3);
+
+    addModifiedPatchSet(
+        changeId, FILE_NAME, content -> content.replace("Line 20\n", "Line twenty\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(3);
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 4");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line four");
+    assertThat(diffInfo).content().element(1).isDueToRebase();
+    assertThat(diffInfo).content().element(2).commonLines().hasSize(1);
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 6");
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line six");
+    assertThat(diffInfo).content().element(3).isDueToRebase();
+    assertThat(diffInfo).content().element(4).commonLines().hasSize(13);
+    assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 20");
+    assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line twenty");
+    assertThat(diffInfo).content().element(5).isNotDueToRebase();
+    assertThat(diffInfo).content().element(6).commonLines().hasSize(80);
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void closeRebaseAndNonRebaseHunksAreNotCombinedForIntralineOptimizations()
+      throws Exception {
+    assume().that(intraline).isTrue();
+
+    String fileContent = FILE_CONTENT.replace("Line 5\n", "{\n").replace("Line 7\n", "{\n");
+    ObjectId commit2 = addCommit(commit1, FILE_NAME, fileContent);
+    rebaseChangeOn(changeId, commit2);
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+
+    String newFileContent =
+        fileContent.replace("Line 4\n", "Line four\n").replace("Line 8\n", "Line eight\n");
+    ObjectId commit3 = addCommit(commit1, FILE_NAME, newFileContent);
+    rebaseChangeOn(changeId, commit3);
+
+    addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 6\n", "Line six\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().hasSize(3);
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 4");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line four");
+    assertThat(diffInfo).content().element(1).isDueToRebase();
+    assertThat(diffInfo).content().element(2).commonLines().hasSize(1);
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 6");
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line six");
+    assertThat(diffInfo).content().element(3).isNotDueToRebase();
+    assertThat(diffInfo).content().element(4).commonLines().hasSize(1);
+    assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 8");
+    assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line eight");
+    assertThat(diffInfo).content().element(5).isDueToRebase();
+    assertThat(diffInfo).content().element(6).commonLines().hasSize(92);
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
   private void assertDiffForNewFile(
       PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
     DiffInfo diff =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 4f61a56..cefde21 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -21,6 +21,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -39,7 +40,9 @@
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -186,19 +189,26 @@
   }
 
   @Test
-  public void harmfulMutationsOfEditsAreNotPossibleForIntraLineDiffArgsAndCachedValue()
-      throws Exception {
+  public void harmfulMutationsOfEditsAreNotPossibleForIntraLineDiffArgsAndCachedValue() {
     String a = "First line\nSecond line\n";
     String b = "1st line\n2nd line\n";
     Text aText = new Text(a.getBytes(UTF_8));
     Text bText = new Text(b.getBytes(UTF_8));
     Edit inputEdit = new Edit(0, 2, 0, 2);
     List<Edit> inputEdits = new ArrayList<>(ImmutableList.of(inputEdit));
+    Set<Edit> inputEditsDueToRebase = new HashSet<>(ImmutableSet.of(inputEdit));
 
     IntraLineDiffKey diffKey =
         IntraLineDiffKey.create(ObjectId.zeroId(), ObjectId.zeroId(), Whitespace.IGNORE_NONE);
     IntraLineDiffArgs diffArgs =
-        IntraLineDiffArgs.create(aText, bText, inputEdits, project, ObjectId.zeroId(), "file.txt");
+        IntraLineDiffArgs.create(
+            aText,
+            bText,
+            inputEdits,
+            inputEditsDueToRebase,
+            project,
+            ObjectId.zeroId(),
+            "file.txt");
     IntraLineDiff intraLineDiff = patchListCache.getIntraLineDiff(diffKey, diffArgs);
 
     Edit outputEdit = Iterables.getOnlyElement(intraLineDiff.getEdits());
@@ -206,9 +216,11 @@
     outputEdit.shift(5);
     inputEdit.shift(7);
     inputEdits.add(new Edit(43, 47, 50, 51));
+    inputEditsDueToRebase.add(new Edit(53, 57, 60, 61));
 
     Edit originalEdit = new Edit(0, 2, 0, 2);
     assertThat(diffArgs.edits()).containsExactly(originalEdit);
+    assertThat(diffArgs.editsDueToRebase()).containsExactly(originalEdit);
     assertThat(intraLineDiff.getEdits()).containsExactly(originalEdit);
   }
 
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 8f0ea8f..0588c48 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -60,6 +60,7 @@
 
   private final Schema<V> schema;
   private final SitePaths sitePaths;
+  private final String indexNameRaw;
 
   protected final String indexName;
   protected final JestHttpClient client;
@@ -78,10 +79,11 @@
     this.queryBuilder = new ElasticQueryBuilder();
     this.indexName =
         String.format(
-            "%s%s%04d",
+            "%s%s_%04d",
             Strings.nullToEmpty(cfg.getString("elasticsearch", null, "prefix")),
             indexName,
             schema.getVersion());
+    this.indexNameRaw = indexName;
     this.client = clientBuilder.build();
   }
 
@@ -97,7 +99,7 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
+    IndexUtils.setReady(sitePaths, indexNameRaw, schema.getVersion(), ready);
   }
 
   @Override
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index 18eb660..9c14c66 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -69,7 +69,6 @@
   }
 
   static final String ACCOUNTS = "accounts";
-  static final String ACCOUNTS_PREFIX = ACCOUNTS + "_";
 
   private static final Logger log = LoggerFactory.getLogger(ElasticAccountIndex.class);
 
@@ -83,7 +82,7 @@
       Provider<AccountCache> accountCache,
       JestClientBuilder clientBuilder,
       @Assisted Schema<AccountState> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, ACCOUNTS_PREFIX);
+    super(cfg, sitePaths, schema, clientBuilder, ACCOUNTS);
     this.accountCache = accountCache;
     this.mapping = new AccountMapping(schema);
   }
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index b99f296..0974710 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -92,9 +92,9 @@
     }
   }
 
-  static final String CHANGES_PREFIX = "changes_";
-  static final String OPEN_CHANGES = "open_changes";
-  static final String CLOSED_CHANGES = "closed_changes";
+  static final String CHANGES = "changes";
+  static final String OPEN_CHANGES = "open_" + CHANGES;
+  static final String CLOSED_CHANGES = "closed_" + CHANGES;
 
   private final ChangeMapping mapping;
   private final Provider<ReviewDb> db;
@@ -108,7 +108,7 @@
       SitePaths sitePaths,
       JestClientBuilder clientBuilder,
       @Assisted Schema<ChangeData> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, CHANGES_PREFIX);
+    super(cfg, sitePaths, schema, clientBuilder, CHANGES);
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     mapping = new ChangeMapping(schema);
@@ -134,7 +134,7 @@
     Bulk bulk =
         new Bulk.Builder()
             .defaultIndex(indexName)
-            .defaultType("changes")
+            .defaultType(CHANGES)
             .addAction(insert(insertIndex, cd))
             .addAction(delete(deleteIndex, cd.getId()))
             .refresh(true)
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index 6ca4ad5..85aa76d 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -68,7 +68,6 @@
   }
 
   static final String GROUPS = "groups";
-  static final String GROUPS_PREFIX = GROUPS + "_";
 
   private static final Logger log = LoggerFactory.getLogger(ElasticGroupIndex.class);
 
@@ -82,7 +81,7 @@
       Provider<GroupCache> groupCache,
       JestClientBuilder clientBuilder,
       @Assisted Schema<InternalGroup> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, GROUPS_PREFIX);
+    super(cfg, sitePaths, schema, clientBuilder, GROUPS);
     this.groupCache = groupCache;
     this.mapping = new GroupMapping(schema);
   }
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 7868443..54e5c38 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -82,6 +82,7 @@
     } else {
       install(new SingleVersionModule(singleVersions));
     }
+    bind(VersionManager.class).to(ElasticVersionManager.class);
   }
 
   @Provides
@@ -93,7 +94,6 @@
   private class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
-      bind(VersionManager.class).to(ElasticVersionManager.class);
       listener().to(ElasticVersionManager.class);
       if (onlineUpgrade) {
         listener().to(OnlineUpgrader.class);
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
index b2b241f..dce8fac 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
@@ -54,19 +54,9 @@
   }
 
   @Override
-  protected <V> boolean isDirty(Collection<Version<V>> inUse, Version<V> v) {
-    return !inUse.contains(v);
-  }
-
-  @Override
   protected <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>> scanVersions(
       IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
     TreeMap<Integer, Version<V>> versions = new TreeMap<>();
-    for (Schema<V> schema : def.getSchemas().values()) {
-      int v = schema.getVersion();
-      versions.put(v, new Version<>(schema, v, cfg.getReady(def.getName(), v)));
-    }
-
     try {
       for (String version : versionDiscovery.discover(prefix, def.getName())) {
         Integer v = Ints.tryParse(version);
@@ -74,13 +64,17 @@
           log.warn("Unrecognized version in index {}: {}", def.getName(), version);
           continue;
         }
-        if (!versions.containsKey(v)) {
-          versions.put(v, new Version<V>(null, v, cfg.getReady(def.getName(), v)));
-        }
+        versions.put(v, new Version<V>(null, v, true, cfg.getReady(def.getName(), v)));
       }
     } catch (IOException e) {
       log.error("Error scanning index: " + def.getName(), e);
     }
+
+    for (Schema<V> schema : def.getSchemas().values()) {
+      int v = schema.getVersion();
+      boolean exists = versions.containsKey(v);
+      versions.put(v, new Version<>(schema, v, exists, cfg.getReady(def.getName(), v)));
+    }
     return versions;
   }
 }
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 fac10eb..f39f0e57 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
@@ -15,11 +15,11 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.elasticsearch.ElasticAccountIndex.ACCOUNTS_PREFIX;
-import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CHANGES_PREFIX;
+import static com.google.gerrit.elasticsearch.ElasticAccountIndex.ACCOUNTS;
+import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CHANGES;
 import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CLOSED_CHANGES;
 import static com.google.gerrit.elasticsearch.ElasticChangeIndex.OPEN_CHANGES;
-import static com.google.gerrit.elasticsearch.ElasticGroupIndex.GROUPS_PREFIX;
+import static com.google.gerrit.elasticsearch.ElasticGroupIndex.GROUPS;
 
 import com.google.common.base.Strings;
 import com.google.common.io.Files;
@@ -128,7 +128,7 @@
         .client()
         .admin()
         .indices()
-        .prepareCreate(String.format("%s%04d", CHANGES_PREFIX, changeSchema.getVersion()))
+        .prepareCreate(String.format("%s_%04d", CHANGES, changeSchema.getVersion()))
         .addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping))
         .addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping))
         .execute()
@@ -141,7 +141,7 @@
         .client()
         .admin()
         .indices()
-        .prepareCreate(String.format("%s%04d", ACCOUNTS_PREFIX, accountSchema.getVersion()))
+        .prepareCreate(String.format("%s_%04d", ACCOUNTS, accountSchema.getVersion()))
         .addMapping(ElasticAccountIndex.ACCOUNTS, gson.toJson(accountMapping))
         .execute()
         .actionGet();
@@ -153,7 +153,7 @@
         .client()
         .admin()
         .indices()
-        .prepareCreate(String.format("%s%04d", GROUPS_PREFIX, groupSchema.getVersion()))
+        .prepareCreate(String.format("%s_%04d", GROUPS, groupSchema.getVersion()))
         .addMapping(ElasticGroupIndex.GROUPS, gson.toJson(groupMapping))
         .execute()
         .actionGet();
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index b730bd0..795ef9b 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.15.1-SNAPSHOT</version>
+  <version>2.15.1</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 2c945be..69acf75 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -36,15 +36,6 @@
   public Map<String, List<RobotCommentInput>> robotComments;
 
   /**
-   * If true require all labels to be within the user's permitted ranges based on access controls,
-   * attempting to use a label not granted to the user will fail the entire modify operation early.
-   * If false the operation will execute anyway, but the proposed labels given by the user will be
-   * modified to be the "best" value allowed by the access controls, or ignored if the label does
-   * not exist.
-   */
-  public boolean strictLabels = true;
-
-  /**
    * How to process draft comments already in the database that were not also described in this
    * input request.
    *
@@ -66,8 +57,6 @@
    * on behalf of this named user instead of the caller. Caller must have the labelAs-$NAME
    * permission granted for each label that appears in {@link #labels}. This is in addition to the
    * named user also needing to have permission to use the labels.
-   *
-   * <p>{@link #strictLabels} impacts how labels is processed for the named user, not the caller.
    */
   public String onBehalfOf;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index d1e4e889..4d4ef8e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -403,7 +403,11 @@
         Type type = inputType(m);
         inputRequestBody = parseRequest(req, type);
         result = m.apply(rsrc, inputRequestBody);
-        consumeRawInputRequestBody(req, type);
+        if (inputRequestBody instanceof RawInput) {
+          try (InputStream is = req.getInputStream()) {
+            ServletUtils.consumeRequestBody(is);
+          }
+        }
       } else {
         throw new ResourceNotFoundException();
       }
@@ -750,7 +754,9 @@
           br.skip(Long.MAX_VALUE);
         }
       }
-    } else if (rawInputRequest(req, type)) {
+    }
+    String method = req.getMethod();
+    if (("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type)) {
       return parseRawInput(req, type);
     } else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
       return null;
@@ -773,19 +779,6 @@
     }
   }
 
-  private void consumeRawInputRequestBody(HttpServletRequest req, Type type) throws IOException {
-    if (rawInputRequest(req, type)) {
-      try (InputStream is = req.getInputStream()) {
-        ServletUtils.consumeRequestBody(is);
-      }
-    }
-  }
-
-  private static boolean rawInputRequest(HttpServletRequest req, Type type) {
-    String method = req.getMethod();
-    return ("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type);
-  }
-
   private static boolean hasNoBody(HttpServletRequest req) {
     int len = req.getContentLength();
     String type = req.getContentType();
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index d738540..0a8f22b 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -92,6 +92,7 @@
     } else {
       install(new SingleVersionModule(singleVersions));
     }
+    bind(VersionManager.class).to(LuceneVersionManager.class);
   }
 
   @Provides
@@ -105,7 +106,6 @@
   private class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
-      bind(VersionManager.class).to(LuceneVersionManager.class);
       listener().to(LuceneVersionManager.class);
       if (onlineUpgrade) {
         listener().to(OnlineUpgrader.class);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index ce92432..aabce35 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -40,15 +40,6 @@
 public class LuceneVersionManager extends VersionManager {
   private static final Logger log = LoggerFactory.getLogger(LuceneVersionManager.class);
 
-  private static class Version<V> extends VersionManager.Version<V> {
-    private final boolean exists;
-
-    private Version(Schema<V> schema, int version, boolean exists, boolean ready) {
-      super(schema, version, ready);
-      this.exists = exists;
-    }
-  }
-
   static Path getDir(SitePaths sitePaths, String name, Schema<?> schema) {
     return sitePaths.index_dir.resolve(String.format("%s_%04d", name, schema.getVersion()));
   }
@@ -63,13 +54,6 @@
   }
 
   @Override
-  protected <V> boolean isDirty(
-      Collection<com.google.gerrit.server.index.VersionManager.Version<V>> inUse,
-      com.google.gerrit.server.index.VersionManager.Version<V> v) {
-    return !inUse.contains(v) && ((Version<V>) v).exists;
-  }
-
-  @Override
   protected <K, V, I extends Index<K, V>> TreeMap<Integer, VersionManager.Version<V>> scanVersions(
       IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
     TreeMap<Integer, VersionManager.Version<V>> versions = new TreeMap<>();
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 6b5c157..e05db354 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
@@ -532,7 +532,7 @@
             slave,
             sysInjector.getInstance(DownloadConfig.class),
             sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
-    if (!slave && indexType == IndexType.LUCENE) {
+    if (!slave) {
       modules.add(new IndexCommandsModule());
     }
     return sysInjector.createChildInjector(modules);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 740f4ce..93e0f5d7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -62,7 +62,7 @@
 
     if (type == IndexType.ELASTICSEARCH) {
       Section elasticsearch = sections.get("elasticsearch", null);
-      elasticsearch.string("Index Prefix", "prefix", "gerrit");
+      elasticsearch.string("Index Prefix", "prefix", "gerrit_");
       String name = ui.readString("default", "Server Name");
 
       Section defaultServer = sections.get("elasticsearch", name);
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 5b5a640..a87a456 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.15.1-SNAPSHOT</version>
+  <version>2.15.1</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 040569d..f54511b 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.15.1-SNAPSHOT</version>
+  <version>2.15.1</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerId.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerId.java
index 87dc44a..237f18c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerId.java
@@ -22,7 +22,8 @@
 /**
  * Marker on a string holding a unique identifier for the server.
  *
- * <p>This value is generated on first use and stored in {@code $site_path/etc/uuid}.
+ * <p>This value is generated on first use and stored in {@code gerrit.serverId} in {@code
+ * gerrit.config}.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
index 8389729..8aabb60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
@@ -45,12 +45,14 @@
   public static class Version<V> {
     public final Schema<V> schema;
     public final int version;
+    public final boolean exists;
     public final boolean ready;
 
-    public Version(Schema<V> schema, int version, boolean ready) {
+    public Version(Schema<V> schema, int version, boolean exists, boolean ready) {
       checkArgument(schema == null || schema.getVersion() == version);
       this.schema = schema;
       this.version = version;
+      this.exists = exists;
       this.ready = ready;
     }
   }
@@ -135,6 +137,16 @@
     return false;
   }
 
+  /**
+   * Tells if an index with this name is currently known or not.
+   *
+   * @param name index name
+   * @return true if index is known and can be used, otherwise false.
+   */
+  public boolean isKnownIndex(String name) {
+    return defs.get(name) != null;
+  }
+
   protected <K, V, I extends Index<K, V>> void initIndex(
       IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
     TreeMap<Integer, Version<V>> versions = scanVersions(def, cfg);
@@ -221,11 +233,13 @@
     }
   }
 
-  protected abstract <V> boolean isDirty(Collection<Version<V>> inUse, Version<V> v);
-
   protected abstract <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>> scanVersions(
       IndexDefinition<K, V, I> def, GerritIndexStatus cfg);
 
+  private <V> boolean isDirty(Collection<Version<V>> inUse, Version<V> v) {
+    return !inUse.contains(v) && v.exists;
+  }
+
   private boolean isLatestIndexVersion(String name, OnlineReindexer<?, ?, ?> reindexer) {
     int readVersion = defs.get(name).getIndexCollection().getSearchIndex().getSchema().getVersion();
     return reindexer == null || reindexer.getVersion() == readVersion;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
index 0a02e36..8ce4dd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
@@ -19,7 +19,6 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import java.io.IOException;
 import java.io.ObjectInputStream;
@@ -41,7 +40,6 @@
   private transient Whitespace whitespace;
 
   public static DiffSummaryKey fromPatchListKey(PatchListKey plk) {
-    Preconditions.checkArgument(plk.getAlgorithm() == PatchListKey.Algorithm.OPTIMIZED_DIFF);
     return new DiffSummaryKey(
         plk.getOldId(), plk.getParentNum(), plk.getNewId(), plk.getWhitespace());
   }
@@ -54,8 +52,7 @@
   }
 
   PatchListKey toPatchListKey() {
-    return new PatchListKey(
-        oldId, parentNum, newId, whitespace, PatchListKey.Algorithm.OPTIMIZED_DIFF);
+    return new PatchListKey(oldId, parentNum, newId, whitespace);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java
index 271c7c3..9083ede 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java
@@ -163,10 +163,10 @@
     while (origIndex < originalEdits.size() && transIndex < transformingEdits.size()) {
       ContextAwareEdit originalEdit = originalEdits.get(origIndex);
       Edit transformingEdit = transformingEdits.get(transIndex);
-      if (transformingEdit.getEndA() < sideStrategy.getBegin(originalEdit)) {
+      if (transformingEdit.getEndA() <= sideStrategy.getBegin(originalEdit)) {
         shiftedAmount = transformingEdit.getEndB() - transformingEdit.getEndA();
         transIndex++;
-      } else if (sideStrategy.getEnd(originalEdit) < transformingEdit.getBeginA()) {
+      } else if (sideStrategy.getEnd(originalEdit) <= transformingEdit.getBeginA()) {
         resultingEdits.add(sideStrategy.create(originalEdit, shiftedAmount, adjustedFilePath));
         origIndex++;
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
index 882360c..4661485 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
@@ -16,8 +16,10 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.List;
+import java.util.Set;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -27,17 +29,22 @@
       Text aText,
       Text bText,
       List<Edit> edits,
+      Set<Edit> editsDueToRebase,
       Project.NameKey project,
       ObjectId commit,
       String path) {
     return new AutoValue_IntraLineDiffArgs(
-        aText, bText, deepCopyEdits(edits), project, commit, path);
+        aText, bText, deepCopyEdits(edits), deepCopyEdits(editsDueToRebase), project, commit, path);
   }
 
   private static ImmutableList<Edit> deepCopyEdits(List<Edit> edits) {
     return edits.stream().map(IntraLineDiffArgs::copy).collect(ImmutableList.toImmutableList());
   }
 
+  private static ImmutableSet<Edit> deepCopyEdits(Set<Edit> edits) {
+    return edits.stream().map(IntraLineDiffArgs::copy).collect(ImmutableSet.toImmutableSet());
+  }
+
   private static Edit copy(Edit edit) {
     return new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB());
   }
@@ -48,6 +55,8 @@
 
   public abstract ImmutableList<Edit> edits();
 
+  public abstract ImmutableSet<Edit> editsDueToRebase();
+
   public abstract Project.NameKey project();
 
   public abstract ObjectId commit();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index fbda00a..b47bbfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -21,7 +21,7 @@
 
 @AutoValue
 public abstract class IntraLineDiffKey implements Serializable {
-  public static final long serialVersionUID = 8L;
+  public static final long serialVersionUID = 11L;
 
   public static IntraLineDiffKey create(ObjectId aId, ObjectId bId, Whitespace whitespace) {
     return new AutoValue_IntraLineDiffKey(aId, bId, whitespace);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index e5e1bad..d7ecc8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -17,6 +17,7 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -77,7 +78,9 @@
   public IntraLineDiff call() throws Exception {
     Future<IntraLineDiff> result =
         diffExecutor.submit(
-            () -> IntraLineLoader.compute(args.aText(), args.bText(), args.edits()));
+            () ->
+                IntraLineLoader.compute(
+                    args.aText(), args.bText(), args.edits(), args.editsDueToRebase()));
     try {
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
@@ -104,10 +107,13 @@
     }
   }
 
-  static IntraLineDiff compute(Text aText, Text bText, ImmutableList<Edit> immutableEdits)
-      throws Exception {
+  static IntraLineDiff compute(
+      Text aText,
+      Text bText,
+      ImmutableList<Edit> immutableEdits,
+      ImmutableSet<Edit> immutableEditsDueToRebase) {
     List<Edit> edits = new ArrayList<>(immutableEdits);
-    combineLineEdits(edits, aText, bText);
+    combineLineEdits(edits, immutableEditsDueToRebase, aText, bText);
 
     for (int i = 0; i < edits.size(); i++) {
       Edit e = edits.get(i);
@@ -256,11 +262,18 @@
     return new IntraLineDiff(edits);
   }
 
-  private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
+  private static void combineLineEdits(
+      List<Edit> edits, ImmutableSet<Edit> editsDueToRebase, Text a, Text b) {
     for (int j = 0; j < edits.size() - 1; ) {
       Edit c = edits.get(j);
       Edit n = edits.get(j + 1);
 
+      if (editsDueToRebase.contains(c) || editsDueToRebase.contains(n)) {
+        // Don't combine any edits which were identified as being introduced by a rebase as we would
+        // lose that information because of the combination.
+        continue;
+      }
+
       // Combine edits that are really close together. Right now our rule
       // is, coalesce two line edits which are only one line apart if that
       // common context line is either a "pointless line", or is identical
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 73e82a1..df5ad41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -32,19 +32,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 public class PatchListKey implements Serializable {
-  public static final long serialVersionUID = 28L;
-
-  // TODO(aliceks): Get rid of this enum and the parameter in the PatchListKey as we only use one of
-  // its values.
-  public enum Algorithm {
-    PURE_TREE_DIFF,
-    OPTIMIZED_DIFF
-  }
-
-  private static final ImmutableBiMap<Algorithm, Character> ALGORITHM_TYPES =
-      ImmutableBiMap.of(
-          Algorithm.PURE_TREE_DIFF, 'T',
-          Algorithm.OPTIMIZED_DIFF, 'O');
+  public static final long serialVersionUID = 30L;
 
   public static final ImmutableBiMap<Whitespace, Character> WHITESPACE_TYPES =
       ImmutableBiMap.of(
@@ -55,20 +43,19 @@
 
   static {
     checkState(WHITESPACE_TYPES.size() == Whitespace.values().length);
-    checkState(ALGORITHM_TYPES.size() == Algorithm.values().length);
   }
 
   public static PatchListKey againstDefaultBase(AnyObjectId newId, Whitespace ws) {
-    return new PatchListKey(null, newId, ws, Algorithm.OPTIMIZED_DIFF);
+    return new PatchListKey(null, newId, ws);
   }
 
   public static PatchListKey againstParentNum(int parentNum, AnyObjectId newId, Whitespace ws) {
-    return new PatchListKey(parentNum, newId, ws, Algorithm.OPTIMIZED_DIFF);
+    return new PatchListKey(parentNum, newId, ws);
   }
 
   public static PatchListKey againstCommit(
       AnyObjectId otherCommitId, AnyObjectId newId, Whitespace whitespace) {
-    return new PatchListKey(otherCommitId, newId, whitespace, Algorithm.OPTIMIZED_DIFF);
+    return new PatchListKey(otherCommitId, newId, whitespace);
   }
 
   /**
@@ -93,34 +80,25 @@
 
   private transient ObjectId newId;
   private transient Whitespace whitespace;
-  private transient Algorithm algorithm;
 
-  private PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws, Algorithm algorithm) {
+  private PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws) {
     oldId = a != null ? a.copy() : null;
     newId = b.copy();
     whitespace = ws;
-    this.algorithm = algorithm;
   }
 
-  private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws, Algorithm algorithm) {
+  private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws) {
     this.parentNum = Integer.valueOf(parentNum);
     newId = b.copy();
     whitespace = ws;
-    this.algorithm = algorithm;
   }
 
   /** For use only by DiffSummaryKey. */
-  PatchListKey(
-      ObjectId oldId,
-      Integer parentNum,
-      ObjectId newId,
-      Whitespace whitespace,
-      Algorithm algorithm) {
+  PatchListKey(ObjectId oldId, Integer parentNum, ObjectId newId, Whitespace whitespace) {
     this.oldId = oldId;
     this.parentNum = parentNum;
     this.newId = newId;
     this.whitespace = whitespace;
-    this.algorithm = algorithm;
   }
 
   /** Old side commit, or null to assume ancestor or combined merge. */
@@ -144,13 +122,9 @@
     return whitespace;
   }
 
-  public Algorithm getAlgorithm() {
-    return algorithm;
-  }
-
   @Override
   public int hashCode() {
-    return Objects.hash(oldId, parentNum, newId, whitespace, algorithm);
+    return Objects.hash(oldId, parentNum, newId, whitespace);
   }
 
   @Override
@@ -160,8 +134,7 @@
       return Objects.equals(oldId, k.oldId)
           && Objects.equals(parentNum, k.parentNum)
           && Objects.equals(newId, k.newId)
-          && whitespace == k.whitespace
-          && algorithm == k.algorithm;
+          && whitespace == k.whitespace;
     }
     return false;
   }
@@ -179,8 +152,6 @@
       n.append(" ");
     }
     n.append(whitespace.name());
-    n.append(" ");
-    n.append(algorithm.name());
     n.append("]");
     return n.toString();
   }
@@ -194,7 +165,6 @@
       throw new IOException("Invalid whitespace type: " + whitespace);
     }
     out.writeChar(c);
-    out.writeChar(ALGORITHM_TYPES.get(algorithm));
   }
 
   private void readObject(ObjectInputStream in) throws IOException {
@@ -207,7 +177,5 @@
     if (whitespace == null) {
       throw new IOException("Invalid whitespace type code: " + t);
     }
-    char algorithmCharacter = in.readChar();
-    algorithm = ALGORITHM_TYPES.inverse().get(algorithmCharacter);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 8fce6d3..9ffd985 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -187,12 +187,10 @@
       List<DiffEntry> diffEntries = df.scan(aTree, bTree);
 
       Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath = ImmutableMultimap.of();
-      if (key.getAlgorithm() == PatchListKey.Algorithm.OPTIMIZED_DIFF) {
-        EditsDueToRebaseResult editsDueToRebaseResult =
-            determineEditsDueToRebase(aCommit, b, diffEntries, df, rw);
-        diffEntries = editsDueToRebaseResult.getRelevantOriginalDiffEntries();
-        editsDueToRebasePerFilePath = editsDueToRebaseResult.getEditsDueToRebasePerFilePath();
-      }
+      EditsDueToRebaseResult editsDueToRebaseResult =
+          determineEditsDueToRebase(aCommit, b, diffEntries, df, rw);
+      diffEntries = editsDueToRebaseResult.getRelevantOriginalDiffEntries();
+      editsDueToRebasePerFilePath = editsDueToRebaseResult.getEditsDueToRebasePerFilePath();
 
       List<PatchListEntry> entries = new ArrayList<>();
       entries.add(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 8b86be2..6f3e055 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
@@ -135,6 +136,7 @@
     b.resolve(a, bId);
 
     edits = new ArrayList<>(content.getEdits());
+    ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
 
     if (!isModify(content)) {
       intralineDifferenceIsPossible = false;
@@ -142,7 +144,8 @@
       IntraLineDiff d =
           patchListCache.getIntraLineDiff(
               IntraLineDiffKey.create(a.id, b.id, diffPrefs.ignoreWhitespace),
-              IntraLineDiffArgs.create(a.src, b.src, edits, projectKey, bId, b.path));
+              IntraLineDiffArgs.create(
+                  a.src, b.src, edits, editsDueToRebase, projectKey, bId, b.path));
       if (d != null) {
         switch (d.getStatus()) {
           case EDIT_LIST:
@@ -214,7 +217,7 @@
         a.dst,
         b.dst,
         edits,
-        content.getEditsDueToRebase(),
+        editsDueToRebase,
         a.displayMethod,
         b.displayMethod,
         a.mimeType.toString(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 1cf6a44..a637414 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -151,7 +151,7 @@
         Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
         throw new IOException(e);
       }
-      log.warn("Cannot find project {}", projectName.get(), e);
+      log.debug("Cannot find project {}", projectName.get(), e);
       return null;
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index eb31abd..2b660ed 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.util.List;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.EditList;
@@ -149,7 +150,8 @@
     Text aText = new Text(a.getBytes(UTF_8));
     Text bText = new Text(b.getBytes(UTF_8));
 
-    IntraLineDiff diff = IntraLineLoader.compute(aText, bText, ImmutableList.of(lines));
+    IntraLineDiff diff =
+        IntraLineLoader.compute(aText, bText, ImmutableList.of(lines), ImmutableSet.of());
 
     assertThat(diff.getStatus()).isEqualTo(IntraLineDiff.Status.EDIT_LIST);
     List<Edit> actualEdits = diff.getEdits();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index 6b3e6d7..0804d08 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.lucene.LuceneVersionManager;
 import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -30,15 +30,19 @@
   @Argument(index = 0, required = true, metaVar = "INDEX", usage = "index name to activate")
   private String name;
 
-  @Inject private LuceneVersionManager luceneVersionManager;
+  @Inject private VersionManager versionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.activateLatestIndex(name)) {
-        stdout.println("Activated latest index version");
+      if (versionManager.isKnownIndex(name)) {
+        if (versionManager.activateLatestIndex(name)) {
+          stdout.println("Activated latest index version");
+        } else {
+          stdout.println("Not activating index, already using latest version");
+        }
       } else {
-        stdout.println("Not activating index, already using latest version");
+        stderr.println(String.format("Cannot activate index %s: unknown", name));
       }
     } catch (ReindexerAlreadyRunningException e) {
       throw die("Failed to activate latest index: " + e.getMessage());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index fb9b482..f3d349c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.lucene.LuceneVersionManager;
 import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -34,15 +34,19 @@
   @Argument(index = 0, required = true, metaVar = "INDEX", usage = "index name to start")
   private String name;
 
-  @Inject private LuceneVersionManager luceneVersionManager;
+  @Inject private VersionManager versionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.startReindexer(name, force)) {
-        stdout.println("Reindexer started");
+      if (versionManager.isKnownIndex(name)) {
+        if (versionManager.startReindexer(name, force)) {
+          stdout.println("Reindexer started");
+        } else {
+          stdout.println("Nothing to reindex, index is already the latest version");
+        }
       } else {
-        stdout.println("Nothing to reindex, index is already the latest version");
+        stderr.println(String.format("Cannot reindex %s: unknown", name));
       }
     } catch (ReindexerAlreadyRunningException e) {
       throw die("Failed to start reindexer: " + e.getMessage());
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index c24be1e..5fcbd65b 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.15.1-SNAPSHOT</version>
+  <version>2.15.1</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
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 9a02fcd..b11cd77 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
@@ -395,9 +395,7 @@
             false,
             sysInjector.getInstance(DownloadConfig.class),
             sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
-    if (indexType == IndexType.LUCENE) {
-      modules.add(new IndexCommandsModule());
-    }
+    modules.add(new IndexCommandsModule());
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 1ce05b1..5257e69 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -15,6 +15,8 @@
   'use strict';
 
   const SUGGESTIONS_LIMIT = 15;
+  const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+      'permission to add it';
 
   const URL_REGEX = '^(?:[a-z]+:)?//';
 
@@ -186,7 +188,16 @@
 
     _handleSavingIncludedGroups() {
       return this.$.restAPI.saveIncludedGroup(this._groupName,
-          this._includedGroupSearch)
+          this._includedGroupSearch, err => {
+            if (err.status === 404) {
+              this.dispatchEvent(new CustomEvent('show-alert', {
+                detail: {message: SAVING_ERROR_TEXT},
+                bubbles: true,
+              }));
+              return err;
+            }
+            throw Error(err.statusText);
+          })
           .then(config => {
             if (!config) {
               return;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 194750e..d670d4d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -183,6 +183,24 @@
       });
     });
 
+    test('add included group 404 shows helpful error text', () => {
+      element._groupOwner = true;
+
+      const memberName = 'bad-name';
+      const alertStub = sandbox.stub();
+      element.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+          () => Promise.reject({status: 404}));
+
+      element.$.groupMemberSearchInput.text = memberName;
+      element.$.groupMemberSearchInput.value = 1234;
+
+      return element._handleSavingIncludedGroups().then(() => {
+        assert.isTrue(alertStub.called);
+      });
+    });
+
     test('_getAccountSuggestions empty', () => {
       return element._getAccountSuggestions('nonexistent').then(accounts => {
         assert.equal(accounts.length, 0);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index d79dc69..276febb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -61,6 +61,8 @@
     <div hidden$="[[_loading]]" hidden>
       <gr-user-header
           user-id="[[_userId]]"
+          show-dashboard-link
+          logged-in="[[loggedIn]]"
           class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header>
       <gr-change-list
           changes="{{_changes}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 572695b..769ef99 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -278,10 +278,13 @@
     },
 
     _handleRKey(e) {
-      if (this.shouldSuppressKeyboardShortcut(e) ||
-          this.modifierPressed(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
       e.preventDefault();
+      this._reloadWindow();
+    },
+
+    _reloadWindow() {
       window.location.reload();
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index bded5f6..25153cd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -190,6 +190,10 @@
         MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 0);
 
+        const reloadStub = sandbox.stub(element, '_reloadWindow');
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+        assert.isTrue(reloadStub.called);
+
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
index 9a7ca33..d3d0736 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -26,35 +27,36 @@
       :host {
         display: block;
         height: 9em;
-        position: relative;
         width: 100%;
       }
       gr-avatar {
+        display: inline-block;
         height: 7em;
         left: 1em;
-        position: absolute;
+        margin: 1em;
         top: 1em;
         width: 7em;
       }
       .info {
-        left: 9em;
-        position: absolute;
-        top: 1em;
+        display: inline-block;
+        padding: 1em;
+        vertical-align: top;
       }
       .info > div > span {
         display: inline-block;
         font-weight: bold;
         text-align: right;
-        width: 6em;
+        width: 4em;
       }
       .name {
-        margin-bottom: .25em;
+        display: inline-block;
       }
       .name hr {
         width: 100%;
       }
       .status.hide,
-      .name.hide {
+      .name.hide,
+      .dashboardLink.hide {
         display: none;
       }
     </style>
@@ -63,10 +65,10 @@
         image-size="100"
         aria-label="Account avatar"></gr-avatar>
     <div class="info">
-      <h1 class$="name">
+      <h1 class="name">
         [[_computeDetail(_accountDetails, 'name')]]
-        <hr/>
       </h1>
+      <hr/>
       <div class$="status [[_computeStatusClass(_accountDetails)]]">
         <span>Status:</span> [[_status]]
       </div>
@@ -82,6 +84,11 @@
         </gr-date-formatter>
       </div>
     </div>
+    <div class="info">
+      <div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
+        <a href$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
+      </div>
+    </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-user-header.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index dd3512a..d09e865 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -23,6 +23,16 @@
         observer: '_accountChanged',
       },
 
+      showDashboardLink: {
+        type: Boolean,
+        value: false,
+      },
+
+      loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+
       /**
        * @type {?{name: ?, email: ?, registered_on: ?}}
        */
@@ -64,5 +74,15 @@
     _computeStatusClass(accountDetails) {
       return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
     },
+
+    _computeDashboardUrl(accountDetails) {
+      if (!accountDetails || !accountDetails.email) { return null; }
+      return Gerrit.Nav.getUrlForUserDashboard(accountDetails.email);
+    },
+
+    _computeDashboardLinkClass(showDashboardLink, loggedIn) {
+      return showDashboardLink && loggedIn ?
+          'dashboardLink' : 'dashboardLink hide';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index ab3b249..4ae8db4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -68,5 +68,12 @@
         });
       });
     });
+
+    test('_computeDashboardLinkClass', () => {
+      assert.include(element._computeDashboardLinkClass(false, false), 'hide');
+      assert.include(element._computeDashboardLinkClass(true, false), 'hide');
+      assert.include(element._computeDashboardLinkClass(false, true), 'hide');
+      assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index d6d8993..bdb437e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -510,11 +510,14 @@
         return;
       }
 
-      e.preventDefault();
       if (this._showInlineDiffs) {
+        e.preventDefault();
         this.$.diffCursor.moveDown();
         this._displayLine = true;
       } else {
+        // Down key
+        if (this.getKeyboardEvent(e).keyCode === 40) { return; }
+        e.preventDefault();
         this.$.fileCursor.next();
         this.selectedIndex = this.$.fileCursor.index;
       }
@@ -525,11 +528,14 @@
         return;
       }
 
-      e.preventDefault();
       if (this._showInlineDiffs) {
+        e.preventDefault();
         this.$.diffCursor.moveUp();
         this._displayLine = true;
       } else {
+        // Up key
+        if (this.getKeyboardEvent(e).keyCode === 38) { return; }
+        e.preventDefault();
         this.$.fileCursor.previous();
         this.selectedIndex = this.$.fileCursor.index;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 6b102ba..a5fd521 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -368,6 +368,10 @@
         // j with a modifier should not move the cursor.
         MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
         assert.equal(element.$.fileCursor.index, 0);
+        // down should not move the cursor.
+        MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down');
+        assert.equal(element.$.fileCursor.index, 0);
+
         MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
         assert.equal(element.$.fileCursor.index, 1);
         assert.equal(element.selectedIndex, 1);
@@ -381,6 +385,10 @@
         MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
         assert.equal(element.$.fileCursor.index, 2);
 
+        // up should not move the cursor.
+        MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
+        assert.equal(element.$.fileCursor.index, 2);
+
         MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.$.fileCursor.index, 1);
         assert.equal(element.selectedIndex, 1);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index bd69007..2ed7952 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -298,8 +298,19 @@
        */
       getUrlForOwner(owner) {
         return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          owner,
+        });
+      },
+
+      /**
+       * @param {string} user The name of the user.
+       * @return {string}
+       */
+      getUrlForUserDashboard(user) {
+        return this._getUrlFor({
           view: Gerrit.Nav.View.DASHBOARD,
-          user: owner,
+          user,
         });
       },
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
index bcae0bf..c26ac87 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -15,9 +15,11 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-group-list">
   <template>
@@ -47,7 +49,11 @@
         <tbody>
           <template is="dom-repeat" items="[[_groups]]">
             <tr>
-              <td class="nameColumn">[[item.name]]</td>
+              <td class="nameColumn">
+                <a href$="[[_computeGroupPath(item)]]">
+                  [[item.name]]
+                </a>
+              </td>
               <td>[[item.description]]</td>
               <td class="visibleCell">[[_computeVisibleToAll(item)]]</td>
             </tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index d26482d..d294a9b 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -21,6 +21,11 @@
       _groups: Array,
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
     loadData() {
       return this.$.restAPI.getAccountGroups().then(groups => {
         this._groups = groups.sort((a, b) => {
@@ -32,5 +37,13 @@
     _computeVisibleToAll(group) {
       return group.options.visible_to_all ? 'Yes' : 'No';
     },
+
+    _computeGroupPath(group) {
+      if (!group || !group.id) { return; }
+
+      const encodeGroup = this.encodeURL(group.id, true);
+
+      return `${this.getBaseUrl()}/admin/groups/${encodeGroup}`;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 3c4eff2..582ed6c 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -33,10 +33,12 @@
 
 <script>
   suite('gr-group-list tests', () => {
+    let sandbox;
     let element;
     let groups;
 
     setup(done => {
+      sandbox = sinon.sandbox.create();
       groups = [{
         url: 'some url',
         options: {},
@@ -65,13 +67,15 @@
       element.loadData().then(() => { flush(done); });
     });
 
+    teardown(() => { sandbox.restore(); });
+
     test('renders', () => {
       const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
 
       assert.equal(rows.length, 3);
 
       const nameCells = rows.map(row =>
-        row.querySelectorAll('td')[0].textContent
+        row.querySelectorAll('td a')[0].textContent.trim()
       );
 
       assert.equal(nameCells[0], 'Group 1');
@@ -83,5 +87,18 @@
       assert.equal(element._computeVisibleToAll(groups[0]), 'No');
       assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
     });
+
+    test('_computeGroupPath', () => {
+      let group = {
+        id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
+      };
+      assert.equal(element._computeGroupPath(group),
+          '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+      group = {
+        name: 'admin',
+      };
+      assert.isUndefined(element._computeGroupPath(group));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 7764d3b..addb9da 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -309,11 +309,16 @@
         </fieldset>
         <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
         <fieldset id="menu">
-          <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor>
+          <gr-menu-editor
+              menu-items="{{_localMenu}}"></gr-menu-editor>
           <gr-button
               id="saveMenu"
               on-tap="_handleSaveMenu"
               disabled="[[!_menuChanged]]">Save changes</gr-button>
+          <gr-button
+              id="resetMenu"
+              link
+              on-tap="_handleResetMenuButton">Reset</gr-button>
         </fieldset>
         <h2 id="ChangeTableColumns"
             class$="[[_computeHeaderClass(_changeTableChanged)]]">
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 25dd259..115a612 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -151,7 +151,7 @@
         this.prefs = prefs;
         this._showNumber = !!prefs.legacycid_in_change_table;
         this._copyPrefs('_localPrefs', 'prefs');
-        this._cloneMenu();
+        this._cloneMenu(prefs.my);
         this._cloneChangeTableColumns();
       }));
 
@@ -209,9 +209,9 @@
       }
     },
 
-    _cloneMenu() {
+    _cloneMenu(prefs) {
       const menu = [];
-      for (const item of this.prefs.my) {
+      for (const item of prefs) {
         menu.push({
           name: item.name,
           url: item.url,
@@ -322,12 +322,20 @@
 
     _handleSaveMenu() {
       this.set('prefs.my', this._localMenu);
-      this._cloneMenu();
+      this._cloneMenu(this.prefs.my);
       return this.$.restAPI.savePreferences(this.prefs).then(() => {
         this._menuChanged = false;
       });
     },
 
+    _handleResetMenuButton() {
+      return this.$.restAPI.getDefaultPreferences().then(data => {
+        if (data && data.my) {
+          this._cloneMenu(data.my);
+        }
+      });
+    },
+
     _handleSaveWatchedProjects() {
       this.$.watchedProjectsEditor.save();
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 868a9e3..6b998bf 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -405,6 +405,42 @@
       assert.isTrue(element.prefs.legacycid_in_change_table);
     });
 
+    test('reset menu item back to default', done => {
+      const originalMenu = {
+        my: [
+          {url: '/first/url', name: 'first name', target: '_blank'},
+          {url: '/second/url', name: 'second name', target: '_blank'},
+          {url: '/third/url', name: 'third name', target: '_blank'},
+        ],
+      };
+
+      stub('gr-rest-api-interface', {
+        getDefaultPreferences() { return Promise.resolve(originalMenu); },
+      });
+
+      const updatedMenu = [
+        {url: '/first/url', name: 'first name', target: '_blank'},
+        {url: '/second/url', name: 'second name', target: '_blank'},
+        {url: '/third/url', name: 'third name', target: '_blank'},
+        {url: '/fourth/url', name: 'fourth name', target: '_blank'},
+      ];
+
+      element.set('_localMenu', updatedMenu);
+
+      element._handleResetMenuButton().then(() => {
+        assertMenusEqual(element._localMenu, originalMenu.my);
+        done();
+      });
+    });
+
+    test('test that reset button is called', () => {
+      const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+
+      MockInteractions.tap(element.$.resetMenu);
+
+      assert.isTrue(overlayOpen.called);
+    });
+
     suite('_getFilterDocsLink', () => {
       test('with http: docs base URL', () => {
         const base = 'http://example.com/';
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 5e9d913..9d5a4e3 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
@@ -401,12 +401,16 @@
           .then(response => this.getResponseObject(response));
     },
 
-    saveIncludedGroup(groupName, includedGroup) {
+    saveIncludedGroup(groupName, includedGroup, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
       return this.send('PUT',
-          `/groups/${encodeName}/groups/${encodeIncludedGroup}`)
-          .then(response => this.getResponseObject(response));
+          `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
+          opt_errFn).then(response => {
+            if (response.ok) {
+              return this.getResponseObject(response);
+            }
+          });
     },
 
     deleteGroupMembers(groupName, groupMembers) {
@@ -658,6 +662,10 @@
       });
     },
 
+    getDefaultPreferences() {
+      return this._fetchSharedCacheURL('/config/server/preferences');
+    },
+
     getPreferences() {
       return this.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index b55a643..21dea94 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -114,10 +114,15 @@
     json.dump(bower_json, f, indent=2)
   return ret
 
+def decode(input):
+  try:
+    return input.decode("utf-8")
+  except TypeError:
+    return input
 
 def bower_command(args):
   base = subprocess.check_output(["bazel", "info", "output_base"]).strip()
-  exp = os.path.join(base, "external", "bower", "*npm_binary.tgz")
+  exp = os.path.join(decode(base), "external", "bower", "*npm_binary.tgz")
   fs = sorted(glob.glob(exp))
   assert len(fs) == 1, "bower tarball not found or have multiple versions %s" % fs
   return ["python", os.getcwd() + "/tools/js/run_npm_binary.py", sorted(fs)[0]] + args
@@ -133,8 +138,8 @@
     "bazel", "query", "kind(bower_component_bundle, //polygerrit-ui/...)"])
   seed_str = subprocess.check_output([
     "bazel", "query", "attr(seed, 1, kind(bower_component, deps(//polygerrit-ui/...)))"])
-  targets = [s for s in target_str.split('\n') if s]
-  seeds = [s for s in seed_str.split('\n') if s]
+  targets = [s for s in decode(target_str).split('\n') if s]
+  seeds = [s for s in decode(seed_str).split('\n') if s]
   prefix = "//lib/js:"
   non_seeds = [s for s in seeds if not s.startswith(prefix)]
   assert not non_seeds, non_seeds
@@ -219,7 +224,7 @@
   out = subprocess.check_output(["find", "bower_components/", "-name", ".bower.json"])
 
   data = []
-  for f in sorted(out.split('\n')):
+  for f in sorted(decode(out).split('\n')):
     if not f:
       continue
     pkg = json.load(open(f))
diff --git a/version.bzl b/version.bzl
index e122db3..29fff64 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "2.15.1-SNAPSHOT"
+GERRIT_VERSION = "2.15.1"