Merge "Add Group names and links to gr-rule-editor"
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
index e0fe1b3..03aaabf 100644
--- a/Documentation/pgm-LocalUsernamesToLowerCase.txt
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -7,7 +7,7 @@
 == SYNOPSIS
 [verse]
 --
-_java_ -jar gerrit.war _LocalUsernamesToLowerCase
+_java_ -jar gerrit.war _LocalUsernamesToLowerCase_
   -d <SITE_PATH>
 --
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fad4b9c..79c7a1a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1077,6 +1077,36 @@
 
 If the change had no assignee the response is "`204 No Content`".
 
+[[get-pure-revert]]
+=== Get Pure Revert
+--
+'GET /changes/link:#change-id[\{change-id\}]/pure_revert'
+--
+
+Check if the given change is a pure revert of the change it references in `revertOf`.
+Optionally, the query parameter `o` can be passed in to specify a commit (SHA1 in
+40 digit hex representation) to check against. It takes precedence over `revertOf`.
+If the change has no reference in `revertOf`, the parameter is mandatory.
+
+As response a link:#pure-revert-info[PureRevertInfo] entity is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/pure_revert?o=247bccf56ae47634650bcc08b8aa784c3580ccas HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "is_pure_revert" : false
+  }
+----
+
 [[abandon-change]]
 === Abandon Change
 --
@@ -6557,6 +6587,16 @@
 of recipient type to link:#notify-info[NotifyInfo] entity.
 |=============================
 
+[[pure-revert-info]]
+=== PureRevertInfo
+The `PureRevertInfo` entity describes the result of a pure revert check.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name      |Description
+|`is_pure_revert`  |Outcome of the check as boolean.
+|======================
+
 [[push-certificate-info]]
 === PushCertificateInfo
 The `PushCertificateInfo` entity contains information about a push
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
index 6edb3f0..43477ae 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
@@ -25,7 +25,7 @@
 @Retention(RUNTIME)
 @Repeatable(GlobalPluginConfigs.class)
 public @interface GlobalPluginConfig {
-  /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.comfig}. */
+  /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.config}. */
   String pluginName();
 
   /** @see GerritConfig#name() */
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 d409a74..4ce412c 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
@@ -89,6 +89,7 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.TrackingIdInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -3173,6 +3174,107 @@
     assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message);
   }
 
+  @Test
+  public void pureRevertReturnsTrueForPureRevert() throws Exception {
+    PushOneCommit.Result r = createChange();
+    merge(r);
+    String revertId = gApi.changes().id(r.getChangeId()).revert().get().id;
+    // Without query parameter
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+    // With query parameter
+    assertThat(
+            gApi.changes()
+                .id(revertId)
+                .pureRevert(getRemoteHead().toObjectId().name())
+                .isPureRevert)
+        .isTrue();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseOnContentChange() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    merge(r1);
+    // Create a revert and expect pureRevert to be true
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+
+    // Create a new PS and expect pureRevert to be false
+    PushOneCommit.Result result = amendChange(revertId);
+    result.assertOkStatus();
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertParameterTakesPrecedence() throws Exception {
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+    String oldHead = getRemoteHead().toObjectId().name();
+
+    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
+    merge(r2);
+
+    String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId;
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+    assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseOnInvalidInput() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    merge(r1);
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid object ID");
+    gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id");
+  }
+
+  @Test
+  public void pureRevertReturnsTrueWithCleanRebase() throws Exception {
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+
+    PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2");
+    merge(r2);
+
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    // Rebase revert onto HEAD
+    gApi.changes().id(revertId).rebase();
+    // Check that pureRevert is true which implies that the commit can be rebased onto the original
+    // commit.
+    assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+  }
+
+  @Test
+  public void pureRevertReturnsFalseWithRebaseConflict() throws Exception {
+    // Create an initial commit to serve as claimed original
+    PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+    merge(r1);
+    String claimedOriginal = getRemoteHead().toObjectId().name();
+
+    // Change contents of the file to provoke a conflict
+    merge(createChange("commit message", "a.txt", "content2"));
+
+    // Create a commit that we can revert
+    PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3");
+    merge(r2);
+
+    // Create a revert of r2
+    String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id();
+    // Assert that the change is a pure revert of it's 'revertOf'
+    assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue();
+    // Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying
+    // to rebase this on claimed original, which fails.
+    PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal);
+    assertThat(pureRevert.isPureRevert).isFalse();
+  }
+
+  @Test
+  public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("no ID was provided and change isn't a revert");
+    gApi.changes().id(createChange().getChangeId()).pureRevert();
+  }
+
   private String getCommitMessage(String changeId) throws RestApiException, IOException {
     return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index b99e99d..b3a53b0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -834,7 +834,7 @@
     try (BatchUpdate bu = newUpdate(ctl.getChange().getOwner())) {
       ins =
           patchSetInserterFactory
-              .create(ctl, nextPatchSetId(ctl), commit)
+              .create(ctl.getNotes(), nextPatchSetId(ctl), commit)
               .setValidate(false)
               .setFireRevisionCreated(false)
               .setNotify(NotifyHandling.NONE);
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 f713ad2..53bf6a0 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.common.CommitMessageInput;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -257,6 +258,12 @@
 
   void index() throws RestApiException;
 
+  /** Check if this change is a pure revert of the change stored in revertOf. */
+  PureRevertInfo pureRevert() throws RestApiException;
+
+  /** Check if this change is a pure revert of claimedOriginal (SHA1 in 40 digit hex). */
+  PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException;
+
   abstract class SuggestedReviewersRequest {
     private String query;
     private int limit;
@@ -548,5 +555,15 @@
     public void mute(boolean mute) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public PureRevertInfo pureRevert() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java
new file mode 100644
index 0000000..7f0d7a8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class PureRevertInfo {
+  public boolean isPureRevert;
+
+  public PureRevertInfo() {}
+
+  public PureRevertInfo(boolean isPureRevert) {
+    this.isPureRevert = isPureRevert;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 2068540..444f64f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -199,7 +199,7 @@
         T def, A options, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
-        String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
+        String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString().toLowerCase());
         if (r == null) {
           throw abort();
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index 5e2f045..538c7c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -184,7 +184,7 @@
 
         ChangeKind kind =
             changeKindCache.getChangeKind(
-                project.getProject().getNameKey(),
+                project.getNameKey(),
                 rw,
                 repoConfig,
                 ObjectId.fromString(priorPs.getRevision().get()),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index e9152d0..9bd7783 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -135,7 +135,7 @@
                   .getProvider()
                   .get()
                   .suggestReviewers(
-                      projectState.getProject().getNameKey(),
+                      projectState.getNameKey(),
                       changeNotes.getChangeId(),
                       query,
                       reviewerScores.keySet()));
@@ -239,8 +239,7 @@
     List<Predicate<ChangeData>> predicates = new ArrayList<>();
     for (Account.Id id : candidates) {
       try {
-        Predicate<ChangeData> projectQuery =
-            changeQueryBuilder.project(projectState.getProject().getName());
+        Predicate<ChangeData> projectQuery = changeQueryBuilder.project(projectState.getName());
 
         // Get all labels for this project and create a compound OR query to
         // fetch all changes where users have applied one of these labels
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 dfa0e7c..2dc9f6c 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
@@ -42,6 +42,7 @@
 import com.google.gerrit.extensions.common.CommitMessageInput;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -59,6 +60,7 @@
 import com.google.gerrit.server.change.GetAssignee;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetPastAssignees;
+import com.google.gerrit.server.change.GetPureRevert;
 import com.google.gerrit.server.change.GetTopic;
 import com.google.gerrit.server.change.Ignore;
 import com.google.gerrit.server.change.Index;
@@ -142,6 +144,7 @@
   private final SetWorkInProgress setWip;
   private final SetReadyForReview setReady;
   private final PutMessage putMessage;
+  private final GetPureRevert getPureRevert;
 
   @Inject
   ChangeApiImpl(
@@ -186,6 +189,7 @@
       SetWorkInProgress setWip,
       SetReadyForReview setReady,
       PutMessage putMessage,
+      GetPureRevert getPureRevert,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -228,6 +232,7 @@
     this.setWip = setWip;
     this.setReady = setReady;
     this.putMessage = putMessage;
+    this.getPureRevert = getPureRevert;
     this.change = change;
   }
 
@@ -669,4 +674,18 @@
       unmute.apply(change, new Unmute.Input());
     }
   }
+
+  @Override
+  public PureRevertInfo pureRevert() throws RestApiException {
+    return pureRevert(null);
+  }
+
+  @Override
+  public PureRevertInfo pureRevert(@Nullable String claimedOriginal) throws RestApiException {
+    try {
+      return getPureRevert.setClaimedOriginal(claimedOriginal).apply(change);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot compute pure revert", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 6e555e5..12c0483 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -48,7 +48,6 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -246,8 +245,7 @@
           if (destChanges.size() == 1) {
             // The change key exists on the destination branch. The cherry pick
             // will be added as a new patch set.
-            ChangeControl destCtl = projectControl.controlFor(destChanges.get(0).notes());
-            result = insertPatchSet(bu, git, destCtl, cherryPickCommit, input);
+            result = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
           } else {
             // Change key not found on destination branch. We can create a new
             // change.
@@ -321,15 +319,15 @@
   private Change.Id insertPatchSet(
       BatchUpdate bu,
       Repository git,
-      ChangeControl destCtl,
+      ChangeNotes destNotes,
       CodeReviewCommit cherryPickCommit,
       CherryPickInput input)
       throws IOException, OrmException, BadRequestException, ConfigInvalidException {
-    Change destChange = destCtl.getChange();
+    Change destChange = destNotes.getChange();
     PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
-    PatchSet current = psUtil.current(dbProvider.get(), destCtl.getNotes());
+    PatchSet current = psUtil.current(dbProvider.get(), destNotes);
 
-    PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit);
+    PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
     inserter
         .setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
         .setDraft(current.isDraft())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index a3927b5..0444e0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -76,7 +76,7 @@
     input.message = message.isEmpty() ? commit.getFullMessage() : message;
     String destination = Strings.nullToEmpty(input.destination).trim();
     input.parent = input.parent == null ? 1 : input.parent;
-    Project.NameKey projectName = rsrc.getProjectState().getProject().getNameKey();
+    Project.NameKey projectName = rsrc.getProjectState().getNameKey();
 
     if (destination.isEmpty()) {
       throw new BadRequestException("destination must be non-empty");
@@ -99,7 +99,7 @@
               projectName,
               commit,
               input,
-              new Branch.NameKey(rsrc.getProjectState().getProject().getNameKey(), refName));
+              new Branch.NameKey(rsrc.getProjectState().getNameKey(), refName));
       return json.noOptions().format(projectName, cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 46d8063..76d5550 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -515,7 +515,7 @@
           (psIdToDelete != null && reuseOldPsId)
               ? psIdToDelete
               : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
-      PatchSetInserter inserter = patchSetInserterFactory.create(ctl, psId, commit);
+      PatchSetInserter inserter = patchSetInserterFactory.create(ctl.getNotes(), psId, commit);
       try (BatchUpdate bu = newBatchUpdate()) {
         bu.setRepository(repo, rw, oi);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
index b02f31b..072052b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -154,7 +154,7 @@
 
       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
       PatchSetInserter psInserter =
-          patchSetInserterFactory.create(rsrc.getControl(), nextPsId, newCommit);
+          patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
       try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
         bu.setRepository(git, rw, oi);
         bu.addOp(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index ee93da0..c725089 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -127,7 +127,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(), change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) {
+            db.get(), change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
       bu.addOp(
           change.getId(),
           new Op(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 181505d..ff5fb0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -315,6 +315,6 @@
 
   private Repository openRepository(ProjectState project)
       throws RepositoryNotFoundException, IOException {
-    return repoManager.openRepository(project.getProject().getNameKey());
+    return repoManager.openRepository(project.getNameKey());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 1ac5a88f..eec318b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -218,7 +218,7 @@
 
       List<DiffWebLinkInfo> links =
           webLinks.getDiffLinks(
-              state.getProject().getName(),
+              state.getName(),
               resource.getPatchKey().getParentKey().getParentKey().get(),
               basePatchSet != null ? basePatchSet.getId().get() : null,
               revA,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
new file mode 100644
index 0000000..c849134
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+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.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+public class GetPureRevert implements RestReadView<ChangeResource> {
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final ChangeNotes.Factory notesFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final PatchSetUtil psUtil;
+
+  @Option(
+    name = "--claimed-original",
+    aliases = {"-o"},
+    usage = "SHA1 (40 digit hex) of the original commit"
+  )
+  @Nullable
+  private String claimedOriginal;
+
+  @Inject
+  GetPureRevert(
+      MergeUtil.Factory mergeUtilFactory,
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      ChangeNotes.Factory notesFactory,
+      Provider<ReviewDb> dbProvider,
+      PatchSetUtil psUtil) {
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.repoManager = repoManager;
+    this.projectCache = projectCache;
+    this.notesFactory = notesFactory;
+    this.dbProvider = dbProvider;
+    this.psUtil = psUtil;
+  }
+
+  @Override
+  public PureRevertInfo apply(ChangeResource rsrc)
+      throws ResourceConflictException, IOException, BadRequestException, OrmException,
+          AuthException {
+    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+    if (currentPatchSet == null) {
+      throw new ResourceConflictException("current revision is missing");
+    } else if (!rsrc.getControl().isPatchVisible(currentPatchSet, dbProvider.get())) {
+      throw new AuthException("current revision not accessible");
+    }
+
+    if (claimedOriginal == null) {
+      if (rsrc.getChange().getRevertOf() == null) {
+        throw new BadRequestException("no ID was provided and change isn't a revert");
+      }
+      PatchSet ps =
+          psUtil.current(
+              dbProvider.get(),
+              notesFactory.createChecked(
+                  dbProvider.get(), rsrc.getProject(), rsrc.getChange().getRevertOf()));
+      claimedOriginal = ps.getRevision().get();
+    }
+
+    try (Repository repo = repoManager.openRepository(rsrc.getProject());
+        ObjectInserter oi = repo.newObjectInserter();
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit claimedOriginalCommit;
+      try {
+        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
+      } catch (InvalidObjectIdException | MissingObjectException e) {
+        throw new BadRequestException("invalid object ID");
+      }
+      if (claimedOriginalCommit.getParentCount() == 0) {
+        throw new BadRequestException("can't check against initial commit");
+      }
+      RevCommit claimedRevertCommit =
+          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
+      if (claimedRevertCommit.getParentCount() == 0) {
+        throw new BadRequestException("claimed revert has no parents");
+      }
+      // Rebase claimed revert onto claimed original
+      ThreeWayMerger merger =
+          mergeUtilFactory
+              .create(projectCache.checkedGet(rsrc.getProject()))
+              .newThreeWayMerger(oi, repo.getConfig());
+      merger.setBase(claimedRevertCommit.getParent(0));
+      merger.merge(claimedRevertCommit, claimedOriginalCommit);
+      if (merger.getResultTreeId() == null) {
+        // Merge conflict during rebase
+        return new PureRevertInfo(false);
+      }
+
+      // Any differences between claimed original's parent and the rebase result indicate that the
+      // claimedRevert is not a pure revert but made content changes
+      try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+        df.setRepository(repo);
+        List<DiffEntry> entries =
+            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+        return new PureRevertInfo(entries.isEmpty());
+      }
+    }
+  }
+
+  public GetPureRevert setClaimedOriginal(String claimedOriginal) {
+    this.claimedOriginal = claimedOriginal;
+    return this;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index bf76af9..5089574 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -70,6 +70,7 @@
     get(CHANGE_KIND, "robotcomments").to(ListChangeRobotComments.class);
     get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
     get(CHANGE_KIND, "check").to(Check.class);
+    get(CHANGE_KIND, "pure_revert").to(GetPureRevert.class);
     post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND, "topic").to(PutTopic.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 5e26305..77f4e5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -44,12 +43,14 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -70,7 +71,7 @@
   private static final Logger log = LoggerFactory.getLogger(PatchSetInserter.class);
 
   public interface Factory {
-    PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, ObjectId commitId);
+    PatchSetInserter create(ChangeNotes notes, PatchSet.Id psId, ObjectId commitId);
   }
 
   // Injected fields.
@@ -78,6 +79,7 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+  private final ProjectCache projectCache;
   private final RevisionCreated revisionCreated;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
@@ -88,9 +90,9 @@
   private final PatchSet.Id psId;
   private final ObjectId commitId;
   // Read prior to running the batch update, so must only be used during
-  // updateRepo; updateChange and later must use the control from the
+  // updateRepo; updateChange and later must use the notes from the
   // ChangeContext.
-  private final ChangeControl origCtl;
+  private final ChangeNotes origNotes;
 
   // Fields exposed as setters.
   private String message;
@@ -123,7 +125,8 @@
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       PatchSetUtil psUtil,
       RevisionCreated revisionCreated,
-      @Assisted ChangeControl ctl,
+      ProjectCache projectCache,
+      @Assisted ChangeNotes notes,
       @Assisted PatchSet.Id psId,
       @Assisted ObjectId commitId) {
     this.permissionBackend = permissionBackend;
@@ -135,8 +138,9 @@
     this.replacePatchSetFactory = replacePatchSetFactory;
     this.psUtil = psUtil;
     this.revisionCreated = revisionCreated;
+    this.projectCache = projectCache;
 
-    this.origCtl = ctl;
+    this.origNotes = notes;
     this.psId = psId;
     this.commitId = commitId.copy();
   }
@@ -316,7 +320,7 @@
       permissionBackend
           .user(ctx.getUser())
           .database(ctx.getDb())
-          .change(origCtl.getNotes())
+          .change(origNotes)
           .check(ChangePermission.ADD_PATCH_SET);
     }
     if (!validate) {
@@ -324,7 +328,7 @@
     }
 
     PermissionBackend.ForRef perm =
-        permissionBackend.user(ctx.getUser()).ref(origCtl.getChange().getDest());
+        permissionBackend.user(ctx.getUser()).ref(origNotes.getChange().getDest());
 
     String refName = getPatchSetId().toRefName();
     try (CommitReceivedEvent event =
@@ -333,16 +337,15 @@
                 ObjectId.zeroId(),
                 commitId,
                 refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
-            origCtl.getProjectControl().getProject(),
-            origCtl.getRefControl().getRefName(),
+            projectCache.checkedGet(origNotes.getProjectName()).getProject(),
+            origNotes.getChange().getDest().get(),
             ctx.getRevWalk().getObjectReader(),
             commitId,
             ctx.getIdentifiedUser())) {
       commitValidatorsFactory
           .forGerritCommits(
               perm,
-              new Branch.NameKey(
-                  origCtl.getProject().getNameKey(), origCtl.getRefControl().getRefName()),
+              origNotes.getChange().getDest(),
               ctx.getIdentifiedUser(),
               new NoSshInfo(),
               ctx.getRevWalk())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
index 7d17dfb..a1a5ab7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -141,8 +141,7 @@
         PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId());
         ObjectId newCommit =
             createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
-        PatchSetInserter inserter =
-            psInserterFactory.create(resource.getControl(), psId, newCommit);
+        PatchSetInserter inserter = psInserterFactory.create(resource.getNotes(), psId, newCommit);
         inserter.setMessage(
             String.format("Patch Set %s: Commit message was updated.", psId.getId()));
         inserter.setDescription("Edit commit message");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 34d239c..465a1b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -172,7 +172,7 @@
             ctl.getChange().currentPatchSetId());
     patchSetInserter =
         patchSetInserterFactory
-            .create(ctl, rebasedPatchSetId, rebasedCommit)
+            .create(ctl.getNotes(), rebasedPatchSetId, rebasedCommit)
             .setDescription("Rebase")
             .setDraft(originalPatchSet.isDraft())
             .setNotify(NotifyHandling.NONE)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 3928b4f..c71c050 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -83,7 +83,7 @@
     ChangeData cd = null;
     for (ReviewerResource rsrc : rsrcs) {
       if (cd == null || !cd.getId().equals(rsrc.getChangeId())) {
-        cd = changeDataFactory.create(db.get(), rsrc.getControl().getNotes());
+        cd = changeDataFactory.create(db.get(), rsrc.getChangeResource().getNotes());
       }
       ReviewerInfo info =
           format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
index f6f7919..47e25b04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -114,25 +113,4 @@
   public boolean isByEmail() {
     return user == null;
   }
-
-  /**
-   * Get the control for the caller's user.
-   *
-   * @return the control for the caller's user (as opposed to the reviewer's user as returned by
-   *     {@link #getReviewerControl()}).
-   */
-  public ChangeControl getControl() {
-    return change.getControl();
-  }
-
-  /**
-   * Get the control for the reviewer's user.
-   *
-   * @return the control for the reviewer's user (as opposed to the caller's user as returned by
-   *     {@link #getControl()}).
-   */
-  public ChangeControl getReviewerControl() {
-    checkArgument(user != null, "no user provided");
-    return change.getControl().forUser(user);
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
index ddf48fd..980e6e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -84,7 +84,7 @@
       Iterable<PatchSetApproval> byPatchSetUser =
           approvalsUtil.byPatchSetUser(
               db.get(),
-              rsrc.getControl(),
+              rsrc.getChangeResource().getControl(),
               rsrc.getChange().currentPatchSetId(),
               rsrc.getReviewerUser().getAccountId(),
               null,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 743df20..917c005 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -180,7 +180,7 @@
       PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
       PatchSetInserter inserter =
           patchSetInserterFactory
-              .create(ctl, psId, squashed)
+              .create(ctl.getNotes(), psId, squashed)
               .setNotify(notify)
               .setAccountsToNotify(accountsToNotify);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index e7303e8..29b2548 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -95,7 +95,7 @@
     }
 
     Project.NameKey getProjectName() {
-      return project.getProject().getNameKey();
+      return project.getNameKey();
     }
 
     public CodeReviewRevWalk getCodeReviewRevWalk() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 7241511..9d7dd19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -188,7 +188,7 @@
     if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeparately)) {
       TagMatcher tags =
           tagCache
-              .get(projectState.getProject().getNameKey())
+              .get(projectState.getNameKey())
               .matcher(
                   tagCache,
                   git,
@@ -268,7 +268,7 @@
   }
 
   private Map<Change.Id, Branch.NameKey> visibleChangesBySearch() {
-    Project.NameKey project = projectState.getProject().getNameKey();
+    Project.NameKey project = projectState.getNameKey();
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(db.get(), project)) {
@@ -286,7 +286,7 @@
   }
 
   private Map<Change.Id, Branch.NameKey> visibleChangesByScan() {
-    Project.NameKey p = projectState.getProject().getNameKey();
+    Project.NameKey p = projectState.getNameKey();
     Stream<ChangeNotesResult> s;
     try {
       s = changeNotesFactory.scan(git, db.get(), p);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 561a0d3..affa918 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -526,7 +526,7 @@
         try (Repository git = args.repoManager.openRepository(getProject())) {
           git.setGitwebDescription(p.getProject().getDescription());
         } catch (IOException e) {
-          log.error("cannot update description of " + p.getProject().getName(), e);
+          log.error("cannot update description of " + p.getName(), e);
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index b384405..f4deee8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -391,7 +391,7 @@
                   + " tried to push an invalid project configuration "
                   + receiveEvent.command.getNewId().name()
                   + " for project "
-                  + receiveEvent.project.getName(),
+                  + receiveEvent.project,
               e);
           throw new CommitValidationException("invalid project configuration", messages);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index fd524b4..51e47a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -145,7 +145,7 @@
       if (RefNames.REFS_CONFIG.equals(destBranch.get())) {
         final Project.NameKey newParent;
         try {
-          ProjectConfig cfg = new ProjectConfig(destProject.getProject().getNameKey());
+          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
           cfg.load(repo, commit);
           newParent = cfg.getProject().getParent(allProjectsName);
           final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName);
@@ -256,7 +256,7 @@
         IdentifiedUser caller)
         throws MergeValidationException {
       Account.Id accountId = Account.Id.fromRef(destBranch.get());
-      if (!allUsersName.equals(destProject.getProject().getNameKey()) || accountId == null) {
+      if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
         return;
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
index 7d83c5c..2d1fc56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -258,9 +258,7 @@
             log.warn(
                 String.format(
                     "Cannot load %s from %s in %s",
-                    c.key.filename,
-                    patchList.getNewId().name(),
-                    projectState.getProject().getName()),
+                    c.key.filename, patchList.getNewId().name(), projectState.getName()),
                 e);
             currentGroup.fileData = null;
           }
@@ -586,7 +584,7 @@
 
   private Repository getRepository() {
     try {
-      return args.server.openRepository(projectState.getProject().getNameKey());
+      return args.server.openRepository(projectState.getNameKey());
     } catch (IOException e) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index d1434bc..e1b6e36 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -103,7 +103,7 @@
           } catch (QueryParseException e) {
             log.warn(
                 "Project {} has invalid notify {} filter \"{}\": {}",
-                state.getProject().getName(),
+                state.getName(),
                 nc.getName(),
                 nc.getFilter(),
                 e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
index edd3fb6..b372b38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -41,6 +41,6 @@
 
   public boolean isDirectChild() {
     ProjectState firstParent = Iterables.getFirst(child.parents(), null);
-    return firstParent != null && parent.getNameKey().equals(firstParent.getProject().getNameKey());
+    return firstParent != null && parent.getNameKey().equals(firstParent.getNameKey());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
index 3152e97..5b36916 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
@@ -38,7 +38,7 @@
   public IncludedInInfo apply(CommitResource rsrc)
       throws RestApiException, OrmException, IOException {
     RevCommit commit = rsrc.getCommit();
-    Project.NameKey project = rsrc.getProjectState().getProject().getNameKey();
+    Project.NameKey project = rsrc.getProjectState().getNameKey();
     return includedIn.apply(project, commit.getId().getName());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
index e38f442..a504a1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -101,7 +101,7 @@
 
   /** @return true if {@code commit} is visible to the caller. */
   public boolean canRead(ProjectState state, Repository repo, RevCommit commit) {
-    Project.NameKey project = state.getProject().getNameKey();
+    Project.NameKey project = state.getNameKey();
 
     // Look for changes associated with the commit.
     try {
@@ -126,7 +126,7 @@
       log.error(
           String.format(
               "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), state.getProject().getNameKey()),
+              commit.name(), state.getNameKey()),
           e);
       return false;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index 34c3287..eb0dde4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -159,7 +159,7 @@
       p.type = configEntry.getType();
       p.permittedValues = configEntry.getPermittedValues();
       p.editable = configEntry.isEditable(project) ? true : null;
-      if (configEntry.isInheritable() && !allProjects.equals(project.getProject().getNameKey())) {
+      if (configEntry.isInheritable() && !allProjects.equals(project.getNameKey())) {
         PluginConfig cfgWithInheritance =
             cfgFactory.getFromProjectConfigWithInheritance(project, e.getPluginName());
         p.inheritable = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 8c1c1e2..82462b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -34,7 +34,7 @@
   public static FileResource create(
       GitRepositoryManager repoManager, ProjectState projectState, ObjectId rev, String path)
       throws ResourceNotFoundException, IOException {
-    try (Repository repo = repoManager.openRepository(projectState.getProject().getNameKey());
+    try (Repository repo = repoManager.openRepository(projectState.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
       RevTree tree = rw.parseTree(rev);
       if (TreeWalk.forPath(repo, path, tree) != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
index 53e1baa..afffdfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -39,6 +39,6 @@
     if (recursive || rsrc.isDirectChild()) {
       return json.format(rsrc.getChild().getProject());
     }
-    throw new ResourceNotFoundException(rsrc.getChild().getProject().getName());
+    throw new ResourceNotFoundException(rsrc.getChild().getName());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
index 23a4417..e5fe37d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -77,7 +77,7 @@
     for (Project.NameKey name : projectCache.all()) {
       ProjectState c = projectCache.get(name);
       if (c != null && parent.equals(c.getProject().getParent(allProjects))) {
-        children.put(c.getProject().getNameKey(), c.getProject());
+        children.put(c.getNameKey(), c.getProject());
       }
     }
     return permissionBackend
@@ -105,7 +105,7 @@
     for (Project.NameKey name : projectCache.all()) {
       ProjectState c = projectCache.get(name);
       if (c != null) {
-        projects.put(c.getProject().getNameKey(), c.getProject());
+        projects.put(c.getNameKey(), c.getProject());
       }
     }
     return projects;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index e1d6c14..f81e84b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -93,7 +93,7 @@
   private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException {
     Map<Project.NameKey, ProjectState> tree = new LinkedHashMap<>();
     for (ProjectState ps : rsrc.getProjectState().tree()) {
-      tree.put(ps.getProject().getNameKey(), ps);
+      tree.put(ps.getNameKey(), ps);
     }
     tree.keySet()
         .retainAll(permissionBackend.user(user).filter(ProjectPermission.ACCESS, tree.keySet()));
@@ -102,10 +102,8 @@
 
   private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
-    Project.NameKey projectName = state.getProject().getNameKey();
-    PermissionBackend.ForProject perm =
-        permissionBackend.user(user).project(state.getProject().getNameKey());
-    try (Repository git = gitManager.openRepository(projectName);
+    PermissionBackend.ForProject perm = permissionBackend.user(user).project(state.getNameKey());
+    try (Repository git = gitManager.openRepository(state.getNameKey());
         RevWalk rw = new RevWalk(git)) {
       List<DashboardInfo> all = new ArrayList<>();
       for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index a284d7d..522aa89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -373,12 +373,12 @@
           ProjectState parent = Iterables.getFirst(e.parents(), null);
           if (parent != null) {
             if (isParentAccessible(accessibleParents, perm, parent)) {
-              info.parent = parent.getProject().getName();
+              info.parent = parent.getName();
             } else {
-              info.parent = hiddenNames.get(parent.getProject().getName());
+              info.parent = hiddenNames.get(parent.getName());
               if (info.parent == null) {
                 info.parent = "?-" + (hiddenNames.size() + 1);
-                hiddenNames.put(parent.getProject().getName(), info.parent);
+                hiddenNames.put(parent.getName(), info.parent);
               }
             }
           }
@@ -506,8 +506,7 @@
           } else {
             log.warn(
                 String.format(
-                    "parent project %s of project %s not found",
-                    parent.get(), ps.getProject().getName()));
+                    "parent project %s of project %s not found", parent.get(), ps.getName()));
           }
         }
       }
@@ -518,7 +517,7 @@
   private boolean isParentAccessible(
       Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState p)
       throws PermissionBackendException {
-    Project.NameKey name = p.getProject().getNameKey();
+    Project.NameKey name = p.getNameKey();
     Boolean b = checked.get(name);
     if (b == null) {
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 2011cd5..a3bee39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -274,8 +274,7 @@
     if (!canPerformOnAnyRef(Permission.PUSH)
         && !canPerformOnAnyRef(Permission.CREATE_TAG)
         && !isOwner()) {
-      String pName = state.getProject().getName();
-      return new Capable("Upload denied for project '" + pName + "'");
+      return new Capable("Upload denied for project '" + state.getName() + "'");
     }
     if (state.isUseContributorAgreements()) {
       return verifyActiveContributorAgreement();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index 9d9e5bb..ac8d536 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -44,7 +44,7 @@
     allProjectsName = all;
 
     seen = Sets.newLinkedHashSet();
-    seen.add(firstResult.getProject().getNameKey());
+    seen.add(firstResult.getNameKey());
     next = firstResult;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index d4c344b..72e5ee6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -180,7 +180,7 @@
   }
 
   private boolean isRevisionOutOfDate() {
-    try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+    try (Repository git = gitMgr.openRepository(getNameKey())) {
       Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
       if (ref == null || ref.getObjectId() == null) {
         return true;
@@ -203,7 +203,7 @@
   public PrologEnvironment newPrologEnvironment() throws CompileException {
     PrologMachineCopy pmc = rulesMachine;
     if (pmc == null) {
-      pmc = rulesCache.loadMachine(getProject().getNameKey(), config.getRulesId());
+      pmc = rulesCache.loadMachine(getNameKey(), config.getRulesId());
       rulesMachine = pmc;
     }
     return envFactory.create(pmc);
@@ -226,6 +226,14 @@
     return config.getProject();
   }
 
+  public Project.NameKey getNameKey() {
+    return getProject().getNameKey();
+  }
+
+  public String getName() {
+    return getNameKey().get();
+  }
+
   public ProjectConfig getConfig() {
     return config;
   }
@@ -236,10 +244,10 @@
     }
 
     ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
-    try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+    try (Repository git = gitMgr.openRepository(getNameKey())) {
       cfg.load(git);
     } catch (IOException | ConfigInvalidException e) {
-      log.warn("Failed to load " + fileName + " for " + getProject().getName(), e);
+      log.warn("Failed to load " + fileName + " for " + getName(), e);
     }
 
     configs.put(fileName, cfg);
@@ -268,7 +276,7 @@
           section.setPermissions(copy);
         }
 
-        SectionMatcher matcher = SectionMatcher.wrap(getProject().getNameKey(), section);
+        SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section);
         if (matcher != null) {
           sm.add(matcher);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 0775bcf..c4a7eb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -105,7 +105,7 @@
 
   public ConfigInfo apply(ProjectState projectState, ConfigInput input)
       throws ResourceNotFoundException, BadRequestException, ResourceConflictException {
-    Project.NameKey projectName = projectState.getProject().getNameKey();
+    Project.NameKey projectName = projectState.getNameKey();
     if (input == null) {
       throw new BadRequestException("config is required");
     }
@@ -309,7 +309,7 @@
       throw new BadRequestException(
           String.format(
               "Not allowed to set parameter '%s' of plugin '%s' on project '%s'.",
-              parameterName, pluginName, projectState.getProject().getName()));
+              parameterName, pluginName, projectState.getName()));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 07f7ead..37cfcdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -77,8 +77,7 @@
     IdentifiedUser user = rsrc.getUser().asIdentifiedUser();
     String parentName =
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
-    validateParentUpdate(
-        rsrc.getProjectState().getProject().getNameKey(), user, parentName, checkIfAdmin);
+    validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin);
     try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
       ProjectConfig config = ProjectConfig.read(md);
       Project project = config.getProject();
@@ -128,11 +127,11 @@
       if (Iterables.tryFind(
               parent.tree(),
               p -> {
-                return p.getProject().getNameKey().equals(project);
+                return p.getNameKey().equals(project);
               })
           .isPresent()) {
         throw new ResourceConflictException(
-            "cycle exists between " + project.get() + " and " + parent.getProject().getName());
+            "cycle exists between " + project.get() + " and " + parent.getName());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 7274100..4595933 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -600,8 +600,7 @@
       try {
         parentEnv = parentState.newPrologEnvironment();
       } catch (CompileException err) {
-        throw new RuleEvalException(
-            "Cannot consult rules.pl for " + parentState.getProject().getName(), err);
+        throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
       }
 
       parentEnv.copyStoredValues(childEnv);
@@ -619,12 +618,12 @@
         throw new RuleEvalException(
             String.format(
                 "%s on change %d of %s",
-                err.getMessage(), cd.getId().get(), parentState.getProject().getName()));
+                err.getMessage(), cd.getId().get(), parentState.getName()));
       } catch (RuntimeException err) {
         throw new RuleEvalException(
             String.format(
                 "Exception calling %s on change %d of %s",
-                filterRule, cd.getId().get(), parentState.getProject().getName()),
+                filterRule, cd.getId().get(), parentState.getName()),
             err);
       } finally {
         reductionsConsumed += env.getReductions();
@@ -690,6 +689,6 @@
   }
 
   private String getProjectName() {
-    return control.getProjectControl().getProjectState().getProject().getName();
+    return control.getProjectControl().getProjectState().getName();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index f5e8d69..19c0515 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -56,7 +56,7 @@
     }
 
     List<Predicate<ChangeData>> r = new ArrayList<>();
-    r.add(new ProjectPredicate(projectState.getProject().getName()));
+    r.add(new ProjectPredicate(projectState.getName()));
     try {
       ProjectResource proj = new ProjectResource(projectState.controlFor(self.get()));
       ListChildProjects children = listChildProjects.get();
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 ea9af4c..0153004 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
@@ -2330,7 +2330,7 @@
 
     PatchSetInserter inserter =
         patchSetFactory
-            .create(ctl, new PatchSet.Id(c.getId(), n), commit)
+            .create(ctl.getNotes(), new PatchSet.Id(c.getId(), n), commit)
             .setNotify(NotifyHandling.NONE)
             .setFireRevisionCreated(false)
             .setValidate(false);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 58492b2..b9a98b9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -69,8 +69,7 @@
 
             @Override
             public Project.NameKey getProjectName() {
-              Project project = projectControl.getProjectState().getProject();
-              return project.getNameKey();
+              return projectControl.getProjectState().getNameKey();
             }
           });
     } finally {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 35788fd..0d7fa24 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -226,6 +226,6 @@
     if (ps == null) {
       return Collections.emptySet();
     }
-    return ps.parents().transform(s -> s.getProject().getNameKey()).toSet();
+    return ps.parents().transform(s -> s.getNameKey()).toSet();
   }
 }
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 83c4d80..09e5d34 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
@@ -468,10 +468,6 @@
         return;
       }
 
-      // If the patch changed, and was not set to undefined/undefined, we need
-      // not reload all resources -- only the commit info and the file list.
-      // If the patch range was set to undefined/undefined, the user is looking
-      // to refresh the whole view.
       const patchChanged = this._patchRange &&
           (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
           (this._patchRange.patchNum !== value.patchNum ||
@@ -481,24 +477,28 @@
         this._initialLoadComplete = false;
       }
 
-      const patchNum = value.patchNum ||
-          this.computeLatestPatchNum(this._allPatchSets);
-
-      const basePatchNum = value.basePatchNum || 'PARENT';
-
-      this._patchRange = {patchNum, basePatchNum};
+      const patchRange = {
+        patchNum: value.patchNum,
+        basePatchNum: value.basePatchNum || 'PARENT',
+      };
+      this.$.fileList.collapseAllDiffs();
 
       if (this._initialLoadComplete && patchChanged) {
+        if (patchRange.patchNum == null) {
+          patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+        }
+        this._patchRange = patchRange;
         this._reloadPatchNumDependentResources().then(() => {
           this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
             change: this._change,
-            patchNum,
+            patchNum: patchRange.patchNum,
           });
         });
         return;
       }
 
       this._changeNum = value.changeNum;
+      this._patchRange = patchRange;
       this.$.relatedChanges.clear();
 
       this._reload().then(() => {
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 57b3e76..59f4f10 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
@@ -672,6 +672,7 @@
           '_reloadPatchNumDependentResources',
           () => { return Promise.resolve(); });
       const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
+      const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
 
       const value = {
         view: Gerrit.Nav.View.CHANGE,
@@ -689,26 +690,22 @@
       assert.isFalse(reloadStub.calledTwice);
       assert.isTrue(reloadPatchDependentStub.calledOnce);
       assert.isTrue(relatedClearSpy.calledOnce);
+      assert.isTrue(collapseStub.calledTwice);
     });
 
     test('reload entire page when patchRange doesnt change', () => {
-      const mockPatchRange = {patchNum: '1337', basePatchNum: 'PARENT'};
       const reloadStub = sandbox.stub(element, '_reload',
           () => { return Promise.resolve(); });
-      element._patchRange = {};
-      sandbox.stub(element, 'computeLatestPatchNum').returns('1337');
+      const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
       const value = {
         view: Gerrit.Nav.View.CHANGE,
       };
       element._paramsChanged(value);
       assert.isTrue(reloadStub.calledOnce);
-      assert.deepEqual(element._patchRange, mockPatchRange);
-
       element._initialLoadComplete = true;
-      element._patchRange = {};
       element._paramsChanged(value);
       assert.isTrue(reloadStub.calledTwice);
-      assert.deepEqual(element._patchRange, mockPatchRange);
+      assert.isTrue(collapseStub.calledTwice);
     });
 
     test('include base patch when not parent', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index b218f02..50bf0e4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -208,7 +208,7 @@
             if="[[_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
           <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
           <span class="separator">/</span>
-          <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+          <gr-button link on-tap="collapseAllDiffs">Hide diffs</gr-button>
         </template>
         <template is="dom-if"
             if="[[!_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
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 7164a16..088adc2 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
@@ -162,7 +162,7 @@
 
       this._loading = true;
 
-      this._collapseAllDiffs();
+      this.collapseAllDiffs();
       const promises = [];
 
       promises.push(this._getFiles().then(files => {
@@ -304,7 +304,7 @@
       this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
     },
 
-    _collapseAllDiffs() {
+    collapseAllDiffs() {
       this._showInlineDiffs = false;
       this._expandedFilePaths = [];
       this.$.diffCursor.handleDiffUpdate();
@@ -640,7 +640,7 @@
 
     _toggleInlineDiffs() {
       if (this._showInlineDiffs) {
-        this._collapseAllDiffs();
+        this.collapseAllDiffs();
       } else {
         this._expandAllDiffs();
       }
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 ff62845..28bb781 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
@@ -859,6 +859,24 @@
       assert.notInclude(element._expandedFilePaths, path);
     });
 
+    test('collapseAllDiffs', () => {
+      sandbox.stub(element, '_renderInOrder')
+          .returns(Promise.resolve());
+      const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+          'handleDiffUpdate');
+
+      const path = 'path/to/my/file.txt';
+      element.files = [{__path: path}];
+      element._expandedFilePaths = [path];
+      element._showInlineDiffs = true;
+
+      element.collapseAllDiffs();
+      flushAsynchronousOperations();
+      assert.equal(element._expandedFilePaths.length, 0);
+      assert.isFalse(element._showInlineDiffs);
+      assert.isTrue(cursorUpdateStub.calledOnce);
+    });
+
     test('_expandedPathsChanged', done => {
       sandbox.stub(element, '_reviewFile');
       const path = 'path/to/my/file.txt';
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index c7a3815..fe4f7cf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -219,6 +219,12 @@
       // makes assumptions that work for the GWT UI, but not PolyGerrit,
       // so we'll just disable it altogether for now.
       delete linkObj.target;
+
+      // Becasue the "my menu" links may be arbitrary URLs, we don't know
+      // whether they correspond to any client routes. Mark all such links as
+      // external.
+      linkObj.external = true;
+
       return linkObj;
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index e4cc7bd..3f45bbf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -58,9 +58,9 @@
         {url: 'https://awesometown.com/#hashyhash'},
         {url: 'url', target: '_blank'},
       ].map(element._fixMyMenuItem), [
-        {url: '/q/owner:self+is:draft'},
-        {url: 'https://awesometown.com/#hashyhash'},
-        {url: 'url'},
+        {url: '/q/owner:self+is:draft', external: true},
+        {url: 'https://awesometown.com/#hashyhash', external: true},
+        {url: 'url', external: true},
       ]);
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 440f44b..b5da3a0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -280,7 +280,9 @@
           </gr-patch-range-select>
           <span class="download desktop">
             <span class="separator">/</span>
-            <a class="downloadLink"
+            <a
+              class="downloadLink"
+              download
               href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
               Download
             </a>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index e239bf0..73dd94a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -455,8 +455,10 @@
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
       element._path = 'glados.txt';
       flushAsynchronousOperations();
-      assert.equal(element.$$('.downloadLink').getAttribute('href'),
+      const link = element.$$('.downloadLink');
+      assert.equal(link.getAttribute('href'),
           '/changes/42/revisions/10/patch?zip&path=glados.txt');
+      assert.isTrue(link.hasAttribute('download'));
     });
 
     test('file review status', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 2d48d36..21aa6cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -55,7 +55,16 @@
   };
 
   GrEtagDecorator.prototype.getCachedPayload = function(url) {
-    return this._payloadCache.get(url);
+    let payload = this._payloadCache.get(url);
+
+    if (typeof payload === 'object') {
+      // Note: For the sake of cache transparency, deep clone the response
+      // object so that cache hits are not equal object references. Some code
+      // expects every network response to deserialize to a fresh object.
+      payload = JSON.parse(JSON.stringify(payload));
+    }
+
+    return payload;
   };
 
   GrEtagDecorator.prototype._truncateCache = function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 8be2352..40e639e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -84,7 +84,7 @@
     });
 
     test('getCachedPayload', () => {
-      const payload = {};
+      const payload = 'payload';
       etag.collect('/foo', fakeRequest('bar'), payload);
       assert.strictEqual(etag.getCachedPayload('/foo'), payload);
       etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
@@ -92,5 +92,25 @@
       etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
       assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
     });
+
+    test('getCachedPayload does not preserve object equality', () => {
+      const payload = {foo: 'bar'};
+      etag.collect('/foo', fakeRequest('bar'), payload);
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+      etag.collect('/foo', fakeRequest('bar', 304), {foo: 'baz'});
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+      etag.collect('/foo', fakeRequest('bar', 200), {foo: 'bar baz'});
+      assert.deepEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+      assert.notStrictEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+    });
+
+    test('getCachedPayload clones the response deeply', () => {
+      const payload = {foo: {bar: 'baz'}};
+      etag.collect('/foo', fakeRequest('bar'), payload);
+      assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+      assert.notStrictEqual(etag.getCachedPayload('/foo').foo, payload.foo);
+    });
   });
 </script>