Merge "PolyGerrit: Add gr-account-link to gr-group-members"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index fccc32e..932f4da 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -180,7 +180,9 @@
 a user's full name and email address based on information obtained
 from the user's account object in LDAP.  The user's group membership
 is also pulled from LDAP, making any LDAP groups that a user is a
-member of available as groups in Gerrit.
+member of available as groups in Gerrit. Hence the `_LDAP` suffix in
+the name of this authentication type. Gerrit does NOT authenticate
+the user via LDAP.
 +
 * `CLIENT_SSL_CERT_LDAP`
 +
@@ -191,7 +193,8 @@
 into the <review-site>/etc/keystore.
 After the authentication is done Gerrit will obtain basic user
 registration (name and email) from LDAP, and some group memberships.
-Therefore, the "_LDAP" suffix in the name of this authentication type.
+Hence the `_LDAP` suffix in the name of this authentication type.
+Gerrit does NOT authenticate the user via LDAP.
 This authentication type can only be used under hosted daemon mode, and
 the httpd.listenUrl must use https:// as the protocol.
 Optionally, certificate revocation list file can be used
@@ -540,15 +543,14 @@
 
 [[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
 +
-When `auth.type` is `LDAP` or `OAUTH`, it allows using either the generated
+When `auth.type` is `LDAP`, `LDAP_BIND` or `OAUTH`, it allows using either the generated
 HTTP password, the LDAP or OAUTH password, or a combination of HTTP and LDAP
 authentication, to authenticate Git over HTTP and REST API requests.
 The supported values are:
 +
 *`HTTP`
 +
-Only the randomly generated HTTP password is accepted when doing Git over HTTP
-and REST API requests.
+Only the HTTP password is accepted when doing Git over HTTP and REST API requests.
 +
 *`LDAP`
 +
@@ -557,7 +559,7 @@
 +
 *`OAUTH`
 +
-Only the `OAUTH` password is allowed when doing Git over HTTP and REST API
+Only the `OAUTH` authentication is allowed when doing Git over HTTP and REST API
 requests.
 +
 *`HTTP_LDAP`
diff --git a/WORKSPACE b/WORKSPACE
index 69a23a0..57066a1 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -581,10 +581,10 @@
 
 maven_jar(
     name = "blame_cache",
-    artifact = "com/google/gitiles:blame-cache:0.2-2",
+    artifact = "com/google/gitiles:blame-cache:0.2-3",
     attach_source = False,
     repository = GERRIT,
-    sha1 = "ac8693b319b3e70506fb27df1b77b598cfdcd00f",
+    sha1 = "fc31fb07fab42b2b4f645e80449fae403e83bcb6",
 )
 
 # Keep this version of Soy synchronized with the version used in Gitiles.
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index 44e7e0e..9ac6b4b 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -25,20 +25,23 @@
 
 """ Script to abandon stale changes from the review server.
 
-Fetches a list of open changes that have not been updated since a
-given age in months or years (default 6 months), and then abandons them.
+Fetches a list of open changes that have not been updated since a given age in
+days, months or years (default 6 months), and then abandons them.
 
-Assumes that the user's credentials are in the .netrc file.  Supports
-either basic or digest authentication.
+Requires the user's credentials for the Gerrit server to be declared in the
+.netrc file. Supports either basic or digest authentication.
 
 Example to abandon changes that have not been updated for 3 months:
 
   ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
 
-Supports dry-run mode to only list the stale changes but not actually
+Supports dry-run mode to only list the stale changes, but not actually
 abandon them.
 
-Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2).
+See the --help output for more information about options.
+
+Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2) to be installed
+and available for import.
 
 """
 
@@ -71,32 +74,32 @@
     parser.add_option('-a', '--age', dest='age',
                       metavar='AGE',
                       default="6months",
-                      help='age of change since last update '
-                           '(default: %default)')
+                      help='age of change since last update in days, months'
+                           ' or years (default: %default)')
     parser.add_option('-m', '--message', dest='message',
                       metavar='STRING', default=None,
-                      help='Custom message to append to abandon message')
+                      help='custom message to append to abandon message')
     parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
                       default=[], action='append',
-                      help='Abandon changes only on the given branch')
+                      help='abandon changes only on the given branch')
     parser.add_option('--exclude-branch', dest='exclude_branches',
                       metavar='BRANCH_NAME',
                       default=[],
                       action='append',
-                      help='Do not abandon changes on given branch')
+                      help='do not abandon changes on given branch')
     parser.add_option('--project', dest='projects', metavar='PROJECT_NAME',
                       default=[], action='append',
-                      help='Abandon changes only on the given project')
+                      help='abandon changes only on the given project')
     parser.add_option('--exclude-project', dest='exclude_projects',
                       metavar='PROJECT_NAME',
                       default=[],
                       action='append',
-                      help='Do not abandon changes on given project')
+                      help='do not abandon changes on given project')
     parser.add_option('--owner', dest='owner',
                       metavar='USERNAME',
                       default=None,
                       action='store',
-                      help='Only abandon changes owned by the given user')
+                      help='only abandon changes owned by the given user')
     parser.add_option('-v', '--verbose', dest='verbose',
                       action='store_true',
                       help='enable verbose (debug) logging')
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
index 7a8dda3..2478e10 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -32,14 +32,17 @@
   HTTP,
 
   /**
-   * Login relies upon the container/web server security, but also uses LDAP.
+   * Login relies upon the container/web server security.
    *
    * <p>Like {@link #HTTP}, the container or web server must populate an HTTP header with a unique
    * name for the current user. Gerrit will implicitly trust the value of this header to supply the
    * unique identity.
    *
-   * <p>In addition to trusting the HTTP headers, Gerrit will obtain basic user registration (name
-   * and email) from LDAP, and some group memberships.
+   * <p>After the authentication is done Gerrit will obtain basic user registration (name and
+   * email), and some group memberships, from LDP. Hence the "_LDAP" suffix in the name of this
+   * authentication type.
+   *
+   * <p>Gerrit will NOT authenticate the user via LDAP.
    */
   HTTP_LDAP,
 
@@ -51,9 +54,11 @@
    * to import the root certificate of the trust chain used to issue the client's certificate into
    * the &lt;review-site&gt;/etc/keystore.
    *
-   * <p>After the authentication is done Gerrit will obtain basic user registration (name and email)
-   * from LDAP, and some group memberships. Therefore, the "_LDAP" suffix in the name of this
+   * <p>After the authentication is done Gerrit will obtain basic user registration (name and
+   * email), and some group memberships, from LDP. Hence the "_LDAP" suffix in the name of this
    * authentication type.
+   *
+   * <p>Gerrit will NOT authenticate the user via LDAP.
    */
   CLIENT_SSL_CERT_LDAP,
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
index 028c911..27fc9e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
@@ -15,8 +15,18 @@
 package com.google.gerrit.extensions.client;
 
 public enum GitBasicAuthPolicy {
+  /** Only the HTTP password is accepted when doing Git over HTTP and REST API requests. */
   HTTP,
+
+  /** Only the LDAP password is allowed when doing Git over HTTP and REST API requests. */
   LDAP,
+
+  /**
+   * The password in the request is first checked against the HTTP password and, if it does not
+   * match, it is then validated against the LDAP password.
+   */
   HTTP_LDAP,
+
+  /** Only the `OAUTH` authentication is allowed when doing Git over HTTP and REST API requests. */
   OAUTH
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
index 5f6dec3..b9d15d2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.webui;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -28,6 +29,7 @@
    *     properties. If null the action will assumed unavailable and not presented. This is usually
    *     the same as {@code setVisible(false)}.
    */
+  @Nullable
   Description getDescription(R resource);
 
   /** Describes an action invokable through the web interface. */
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 1a263f3..08b42e5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -133,7 +133,7 @@
           .check(ChangePermission.READ);
       if (patchKey.getParentKey().get() == 0) {
         // change edit
-        Optional<ChangeEdit> edit = changeEditUtil.byChange(notes.getChange());
+        Optional<ChangeEdit> edit = changeEditUtil.byChange(notes);
         if (edit.isPresent()) {
           revision = ObjectId.toString(edit.get().getEditCommit());
         } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
index 2e0fe2b..b13c43b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -181,7 +181,7 @@
     List<ChangeControl> ctls = new ArrayList<>(cds.size());
     if (!indexConfig.separateChangeSubIndexes()) {
       for (ChangeData cd : cds) {
-        ctls.add(cd.changeControl(user));
+        checkedAdd(cd, ctls, user);
       }
       return ctls;
     }
@@ -195,9 +195,18 @@
     Set<Change.Id> seen = Sets.newHashSetWithExpectedSize(cds.size());
     for (ChangeData cd : cds) {
       if (seen.add(cd.getId())) {
-        ctls.add(cd.changeControl(user));
+        checkedAdd(cd, ctls, user);
       }
     }
     return ctls;
   }
+
+  private static void checkedAdd(ChangeData cd, List<ChangeControl> ctls, CurrentUser user)
+      throws OrmException {
+    try {
+      ctls.add(cd.changeControl(user));
+    } catch (NoSuchChangeException e) {
+      // Ignore
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
index 6267dca..4854112 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
@@ -138,7 +138,7 @@
 
   public void parseDynamicBeans(CmdLineParser clp) {
     for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
-      clp.parseWithPrefix(e.getKey(), e.getValue());
+      clp.parseWithPrefix("--" + e.getKey(), e.getValue());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 5184e89..d1b57e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -172,7 +172,7 @@
   @Override
   public void modifyFile(String filePath, RawInput newContent) throws RestApiException {
     try {
-      changeEditsPut.apply(changeResource.getControl(), filePath, newContent);
+      changeEditsPut.apply(changeResource, filePath, newContent);
     } catch (Exception e) {
       throw asRestApiException("Cannot modify file of change edit", e);
     }
@@ -181,7 +181,7 @@
   @Override
   public void deleteFile(String filePath) throws RestApiException {
     try {
-      changeEditDeleteContent.apply(changeResource.getControl(), filePath);
+      changeEditDeleteContent.apply(changeResource, filePath);
     } catch (Exception e) {
       throw asRestApiException("Cannot delete file of change edit", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index 14aa108..cd0f34e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.AbandonOp;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -76,49 +77,58 @@
           IOException, ConfigInvalidException {
     req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
 
-    NotifyHandling notify = input.notify == null ? defaultNotify(req.getControl()) : input.notify;
+    NotifyHandling notify = input.notify == null ? defaultNotify(req.getChange()) : input.notify;
     Change change =
         abandon(
             updateFactory,
-            req.getControl(),
+            req.getNotes(),
+            req.getUser(),
             input.message,
             notify,
             notifyUtil.resolveAccounts(input.notifyDetails));
     return json.noOptions().format(change);
   }
 
-  private NotifyHandling defaultNotify(ChangeControl control) {
-    return control.getChange().hasReviewStarted() ? NotifyHandling.ALL : NotifyHandling.OWNER;
+  private NotifyHandling defaultNotify(Change change) {
+    return change.hasReviewStarted() ? NotifyHandling.ALL : NotifyHandling.OWNER;
   }
 
-  public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control)
-      throws RestApiException, UpdateException {
-    return abandon(updateFactory, control, "", defaultNotify(control), ImmutableListMultimap.of());
-  }
-
-  public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control, String msgTxt)
+  public Change abandon(BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user)
       throws RestApiException, UpdateException {
     return abandon(
-        updateFactory, control, msgTxt, defaultNotify(control), ImmutableListMultimap.of());
+        updateFactory,
+        notes,
+        user,
+        "",
+        defaultNotify(notes.getChange()),
+        ImmutableListMultimap.of());
+  }
+
+  public Change abandon(
+      BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, String msgTxt)
+      throws RestApiException, UpdateException {
+    return abandon(
+        updateFactory,
+        notes,
+        user,
+        msgTxt,
+        defaultNotify(notes.getChange()),
+        ImmutableListMultimap.of());
   }
 
   public Change abandon(
       BatchUpdate.Factory updateFactory,
-      ChangeControl control,
+      ChangeNotes notes,
+      CurrentUser user,
       String msgTxt,
       NotifyHandling notifyHandling,
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
       throws RestApiException, UpdateException {
-    CurrentUser user = control.getUser();
     Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null;
     AbandonOp op = abandonOpFactory.create(account, msgTxt, notifyHandling, accountsToNotify);
     try (BatchUpdate u =
-        updateFactory.create(
-            dbProvider.get(),
-            control.getProject().getNameKey(),
-            control.getUser(),
-            TimeUtil.nowTs())) {
-      u.addOp(control.getId(), op).execute();
+        updateFactory.create(dbProvider.get(), notes.getProjectName(), user, TimeUtil.nowTs())) {
+      u.addOp(notes.getChangeId(), op).execute();
     }
     return op.getChange();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
index aee97fc..2ae9a86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
@@ -75,7 +75,7 @@
               repository, projectState, patchSetCommitId, fixResource.getFixReplacements());
       ChangeEdit changeEdit =
           changeEditModifier.combineWithModifiedPatchSetTree(
-              repository, revisionResource.getControl(), patchSet, treeModifications);
+              repository, revisionResource.getNotes(), patchSet, treeModifications);
       return Response.ok(changeEditJson.toEditInfo(changeEdit, false));
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
index 1695d0c..08bcabe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.edit.ChangeEdit;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
 /**
@@ -54,10 +53,6 @@
     return change;
   }
 
-  public ChangeControl getControl() {
-    return getChangeResource().getControl();
-  }
-
   public ChangeEdit getChangeEdit() {
     return edit;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 929268b..0064281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -105,7 +104,7 @@
   @Override
   public ChangeEditResource parse(ChangeResource rsrc, IdString id)
       throws ResourceNotFoundException, AuthException, IOException, OrmException {
-    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (!edit.isPresent()) {
       throw new ResourceNotFoundException(id);
     }
@@ -157,7 +156,7 @@
     public Response<?> apply(ChangeResource resource, Put.Input input)
         throws AuthException, ResourceConflictException, IOException, OrmException,
             PermissionBackendException {
-      putEdit.apply(resource.getControl(), path, input.content);
+      putEdit.apply(resource, path, input.content);
       return Response.none();
     }
   }
@@ -182,7 +181,7 @@
     public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in)
         throws IOException, AuthException, ResourceConflictException, OrmException,
             PermissionBackendException {
-      return deleteContent.apply(rsrc.getControl(), path);
+      return deleteContent.apply(rsrc, path);
     }
   }
 
@@ -218,7 +217,7 @@
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc)
         throws AuthException, IOException, ResourceNotFoundException, OrmException {
-      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         return Response.none();
       }
@@ -275,13 +274,12 @@
             PermissionBackendException {
       Project.NameKey project = resource.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
-        ChangeControl changeControl = resource.getControl();
         if (isRestoreFile(input)) {
-          editModifier.restoreFile(repository, changeControl, input.restorePath);
+          editModifier.restoreFile(repository, resource.getNotes(), input.restorePath);
         } else if (isRenameFile(input)) {
-          editModifier.renameFile(repository, changeControl, input.oldPath, input.newPath);
+          editModifier.renameFile(repository, resource.getNotes(), input.oldPath, input.newPath);
         } else {
-          editModifier.createEdit(repository, changeControl);
+          editModifier.createEdit(repository, resource.getNotes());
         }
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
@@ -320,19 +318,18 @@
     public Response<?> apply(ChangeEditResource rsrc, Input input)
         throws AuthException, ResourceConflictException, IOException, OrmException,
             PermissionBackendException {
-      return apply(rsrc.getControl(), rsrc.getPath(), input.content);
+      return apply(rsrc.getChangeResource(), rsrc.getPath(), input.content);
     }
 
-    public Response<?> apply(ChangeControl changeControl, String path, RawInput newContent)
+    public Response<?> apply(ChangeResource rsrc, String path, RawInput newContent)
         throws ResourceConflictException, AuthException, IOException, OrmException,
             PermissionBackendException {
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
       }
 
-      Project.NameKey project = changeControl.getChange().getProject();
-      try (Repository repository = repositoryManager.openRepository(project)) {
-        editModifier.modifyFile(repository, changeControl, path, newContent);
+      try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
+        editModifier.modifyFile(repository, rsrc.getNotes(), path, newContent);
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
       }
@@ -364,15 +361,14 @@
     public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input)
         throws AuthException, ResourceConflictException, OrmException, IOException,
             PermissionBackendException {
-      return apply(rsrc.getControl(), rsrc.getPath());
+      return apply(rsrc.getChangeResource(), rsrc.getPath());
     }
 
-    public Response<?> apply(ChangeControl changeControl, String filePath)
+    public Response<?> apply(ChangeResource rsrc, String filePath)
         throws AuthException, IOException, OrmException, ResourceConflictException,
             PermissionBackendException {
-      Project.NameKey project = changeControl.getChange().getProject();
-      try (Repository repository = repositoryManager.openRepository(project)) {
-        editModifier.deleteFile(repository, changeControl, filePath);
+      try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
+        editModifier.deleteFile(repository, rsrc.getNotes(), filePath);
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
       }
@@ -401,7 +397,7 @@
         ChangeEdit edit = rsrc.getChangeEdit();
         return Response.ok(
             fileContentUtil.getContent(
-                rsrc.getControl().getProjectControl().getProjectState(),
+                rsrc.getChangeResource().getControl().getProjectControl().getProjectState(),
                 base
                     ? ObjectId.fromString(edit.getBasePatchSet().getRevision().get())
                     : edit.getEditCommit(),
@@ -471,8 +467,7 @@
 
       Project.NameKey project = rsrc.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
-        ChangeControl changeControl = rsrc.getControl();
-        editModifier.modifyMessage(repository, changeControl, input.message);
+        editModifier.modifyMessage(repository, rsrc.getNotes(), input.message);
       } catch (UnchangedCommitMessageException e) {
         throw new ResourceConflictException(e.getMessage());
       }
@@ -501,7 +496,7 @@
     @Override
     public BinaryResult apply(ChangeResource rsrc)
         throws AuthException, IOException, ResourceNotFoundException, OrmException {
-      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       String msg;
       if (edit.isPresent()) {
         if (base) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java
index f852a97..47f5a16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java
@@ -18,10 +18,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,9 +42,7 @@
   @Override
   public IncludedInInfo apply(ChangeResource rsrc)
       throws RestApiException, OrmException, IOException {
-    ChangeControl ctl = rsrc.getControl();
     PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
-    Project.NameKey project = ctl.getProject().getNameKey();
-    return includedIn.apply(project, ps.getRevision().get());
+    return includedIn.apply(rsrc.getProject(), ps.getRevision().get());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 3ceeb24..7208de1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -290,7 +290,7 @@
   }
 
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
-    return format(changeDataFactory.create(db.get(), rsrc.getControl()));
+    return format(changeDataFactory.create(db.get(), rsrc.getNotes()));
   }
 
   public ChangeInfo format(Change change) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index f27c53b..0cbbe3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -77,8 +76,8 @@
     return control;
   }
 
-  public IdentifiedUser getUser() {
-    return getControl().getUser().asIdentifiedUser();
+  public CurrentUser getUser() {
+    return getControl().getUser();
   }
 
   public Change.Id getId() {
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 0b7d495..dfc0639 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
@@ -42,7 +42,6 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -117,11 +116,10 @@
       throw new BadRequestException("merge.source must be non-empty");
     }
 
-    ChangeControl ctl = rsrc.getControl();
-    PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
-    ProjectControl projectControl = ctl.getProjectControl();
+    PatchSet ps = psUtil.current(db.get(), rsrc.getNotes());
+    ProjectControl projectControl = rsrc.getControl().getProjectControl();
     ProjectState state = projectControl.getProjectState();
-    Change change = ctl.getChange();
+    Change change = rsrc.getChange();
     Project.NameKey project = change.getProject();
     Branch.NameKey dest = change.getDest();
     try (Repository git = gitManager.openRepository(project);
@@ -153,11 +151,12 @@
               ObjectId.fromString(change.getKey().get().substring(1)));
 
       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
-      PatchSetInserter psInserter = patchSetInserterFactory.create(ctl, nextPsId, newCommit);
+      PatchSetInserter psInserter =
+          patchSetInserterFactory.create(rsrc.getControl(), nextPsId, newCommit);
       try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
         bu.setRepository(git, rw, oi);
         bu.addOp(
-            ctl.getId(),
+            rsrc.getId(),
             psInserter
                 .setMessage("Uploaded patch set " + nextPsId.get() + ".")
                 .setDraft(ps.isDraft())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
index f196ec8..e2e3920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -41,7 +41,7 @@
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
       throws AuthException, ResourceNotFoundException, IOException, OrmException {
-    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
     if (edit.isPresent()) {
       editUtil.delete(edit.get());
     } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
index 001ef89..8409ab4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -67,15 +66,11 @@
       throw new ResourceConflictException("change is not private");
     }
 
-    ChangeControl control = rsrc.getControl();
     SetPrivateOp op = new SetPrivateOp(cmUtil, false, input);
     try (BatchUpdate u =
         updateFactory.create(
-            dbProvider.get(),
-            control.getProject().getNameKey(),
-            control.getUser(),
-            TimeUtil.nowTs())) {
-      u.addOp(control.getId(), op).execute();
+            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      u.addOp(rsrc.getId(), op).execute();
     }
 
     return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
index d6d0acc..4702b5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
@@ -30,8 +30,8 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gitiles.blame.BlameCache;
-import com.google.gitiles.blame.Region;
+import com.google.gitiles.blame.cache.BlameCache;
+import com.google.gitiles.blame.cache.Region;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
index 4ea1c02..c285734 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -31,8 +30,7 @@
   @Override
   public Response<Set<String>> apply(ChangeResource req)
       throws AuthException, OrmException, IOException, BadRequestException {
-    ChangeControl control = req.getControl();
-    ChangeNotes notes = control.getNotes().load();
+    ChangeNotes notes = req.getNotes().load();
     Set<String> hashtags = notes.getHashtags();
     if (hashtags == null) {
       hashtags = Collections.emptySet();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java
index eaa3a28..76114ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java
@@ -40,7 +40,7 @@
   @Override
   public Response<List<AccountInfo>> apply(ChangeResource rsrc) throws OrmException {
 
-    Set<Account.Id> pastAssignees = rsrc.getControl().getNotes().load().getPastAssignees();
+    Set<Account.Id> pastAssignees = rsrc.getNotes().load().getPastAssignees();
     if (pastAssignees == null) {
       return Response.ok(Collections.emptyList());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
index 942c3b4..0048657 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
@@ -49,7 +49,7 @@
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
       throws AuthException, OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
     return commentJson
         .get()
         .setFillAccounts(true)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
index 2bf7aa0..02713de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
@@ -50,13 +50,12 @@
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
       throws AuthException, OrmException {
-    if (!rsrc.getControl().getUser().isIdentifiedUser()) {
+    if (!rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
     List<Comment> drafts =
-        commentsUtil.draftByChangeAuthor(
-            db.get(), cd.notes(), rsrc.getControl().getUser().getAccountId());
+        commentsUtil.draftByChangeAuthor(db.get(), cd.notes(), rsrc.getUser().getAccountId());
     return commentJson
         .get()
         .setFillAccounts(false)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.java
index 881c6f53..fff7f82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.java
@@ -47,7 +47,7 @@
   @Override
   public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
       throws AuthException, OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
     return commentJson
         .get()
         .setFillAccounts(true)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index 10c30b2..a4aa60a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -98,7 +98,7 @@
       throws RestApiException, OrmException, UpdateException, PermissionBackendException {
     Change change = rsrc.getChange();
     Project.NameKey project = rsrc.getProject();
-    IdentifiedUser caller = rsrc.getUser();
+    IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
     input.destinationBranch = RefNames.fullName(input.destinationBranch);
 
     if (change.getStatus().isClosed()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 7ae0abe..1ff0fdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -55,7 +55,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(), req.getChange().getProject(), req.getControl().getUser(), TimeUtil.nowTs())) {
+            db.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
       SetHashtagsOp op = hashtagsFactory.create(input);
       bu.addOp(req.getId(), op);
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
index ad8e72c..ac0b4c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -69,15 +68,11 @@
       return Response.ok("");
     }
 
-    ChangeControl control = rsrc.getControl();
     SetPrivateOp op = new SetPrivateOp(cmUtil, true, input);
     try (BatchUpdate u =
         updateFactory.create(
-            dbProvider.get(),
-            control.getProject().getNameKey(),
-            control.getUser(),
-            TimeUtil.nowTs())) {
-      u.addOp(control.getId(), op).execute();
+            dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      u.addOp(rsrc.getId(), op).execute();
     }
 
     return Response.created("");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index f7e4469..7b9f099 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -60,7 +60,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -286,13 +285,12 @@
     }
 
     Set<Account.Id> reviewers = new HashSet<>();
-    ChangeControl control = rsrc.getControl();
     Set<Account> members;
     try {
       members =
           groupMembersFactory
-              .create(control.getUser())
-              .listAccounts(group.getGroupUUID(), control.getProject().getNameKey());
+              .create(rsrc.getUser())
+              .listAccounts(group.getGroupUUID(), rsrc.getProject());
     } catch (NoSuchGroupException e) {
       return fail(
           reviewer,
@@ -424,7 +422,7 @@
       this.reviewersByEmail = reviewersByEmail == null ? ImmutableList.of() : reviewersByEmail;
       this.state = state;
       notes = rsrc.getNotes();
-      caller = rsrc.getUser();
+      caller = rsrc.getUser().asIdentifiedUser();
       op =
           postReviewersOpFactory.create(
               rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index cbb5fa3..d3f87cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -89,7 +89,7 @@
         throws IOException, OrmException, RestApiException, UpdateException,
             ConfigInvalidException {
       CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
-      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         throw new ResourceConflictException(
             String.format("no edit exists for change %s", rsrc.getChange().getChangeId()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 3a614a3..fb151ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -155,7 +155,7 @@
         throws RestApiException, UpdateException {
       return publish.apply(
           updateFactory,
-          rsrc.getControl().getUser(),
+          rsrc.getUser(),
           rsrc.getChange(),
           rsrc.getChange().currentPatchSetId(),
           null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index 8b1f4e4..d53c85c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -94,10 +94,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(),
-            rsrc.getChange().getProject(),
-            rsrc.getControl().getUser(),
-            TimeUtil.nowTs())) {
+            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       SetAssigneeOp op = assigneeFactory.create(assignee);
       bu.addOp(rsrc.getId(), op);
 
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 d9d4f43..7d17dfb 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
@@ -110,7 +110,7 @@
     }
     String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
 
-    ensureCanEditCommitMessage(resource.getControl().getNotes());
+    ensureCanEditCommitMessage(resource.getNotes());
     ensureChangeIdIsCorrect(
         resource.getControl().getProjectControl().getProjectState().isRequireChangeID(),
         resource.getChange().getKey().get(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
index 9e840c3..509d11f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -87,7 +87,7 @@
             PermissionBackendException {
       Project.NameKey project = rsrc.getProject();
       try (Repository repository = repositoryManager.openRepository(project)) {
-        editModifier.rebaseEdit(repository, rsrc.getControl());
+        editModifier.rebaseEdit(repository, rsrc.getNotes());
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 0b8fdfe..05e8b4a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -88,11 +87,10 @@
       throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     req.permissions().database(dbProvider).check(ChangePermission.RESTORE);
 
-    ChangeControl ctl = req.getControl();
     Op op = new Op(input);
     try (BatchUpdate u =
         updateFactory.create(
-            dbProvider.get(), req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+            dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
       u.addOp(req.getId(), op).execute();
     }
     return json.noOptions().format(op.change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index b126efd..941f4dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -42,10 +42,10 @@
 import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.send.RevertedSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 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.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -83,7 +83,6 @@
 
   private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final GitRepositoryManager repoManager;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeMessagesUtil cmUtil;
@@ -99,7 +98,6 @@
   Revert(
       Provider<ReviewDb> db,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       GitRepositoryManager repoManager,
       ChangeInserter.Factory changeInserterFactory,
       ChangeMessagesUtil cmUtil,
@@ -114,7 +112,6 @@
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
     this.cmUtil = cmUtil;
@@ -138,24 +135,24 @@
     }
 
     CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
-    permissionBackend.user(user).ref(change.getDest()).check(CREATE_CHANGE);
+    permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE);
 
     Change.Id revertId =
-        revert(updateFactory, rsrc.getControl(), Strings.emptyToNull(input.message));
+        revert(updateFactory, rsrc.getNotes(), rsrc.getUser(), Strings.emptyToNull(input.message));
     return json.noOptions().format(rsrc.getProject(), revertId);
   }
 
-  private Change.Id revert(BatchUpdate.Factory updateFactory, ChangeControl ctl, String message)
+  private Change.Id revert(
+      BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, String message)
       throws OrmException, IOException, RestApiException, UpdateException {
-    Change.Id changeIdToRevert = ctl.getChange().getId();
-    PatchSet.Id patchSetId = ctl.getChange().currentPatchSetId();
-    PatchSet patch = psUtil.get(db.get(), ctl.getNotes(), patchSetId);
+    Change.Id changeIdToRevert = notes.getChangeId();
+    PatchSet.Id patchSetId = notes.getChange().currentPatchSetId();
+    PatchSet patch = psUtil.get(db.get(), notes, patchSetId);
     if (patch == null) {
       throw new ResourceNotFoundException(changeIdToRevert.toString());
     }
 
-    Project.NameKey project = ctl.getProject().getNameKey();
-    CurrentUser user = ctl.getUser();
+    Project.NameKey project = notes.getProjectName();
     try (Repository git = repoManager.openRepository(project);
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
@@ -180,7 +177,7 @@
       revertCommitBuilder.setAuthor(authorIdent);
       revertCommitBuilder.setCommitter(authorIdent);
 
-      Change changeToRevert = ctl.getChange();
+      Change changeToRevert = notes.getChange();
       if (message == null) {
         message =
             MessageFormat.format(
@@ -204,11 +201,11 @@
 
       ChangeInserter ins =
           changeInserterFactory
-              .create(changeId, revertCommit, ctl.getChange().getDest().get())
+              .create(changeId, revertCommit, notes.getChange().getDest().get())
               .setTopic(changeToRevert.getTopic());
       ins.setMessage("Uploaded patch set 1.");
 
-      ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), ctl.getNotes());
+      ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), notes);
 
       Set<Account.Id> reviewers = new HashSet<>();
       reviewers.add(changeToRevert.getOwner());
@@ -224,7 +221,7 @@
       try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
         bu.setRepository(git, revWalk, oi);
         bu.insertChange(ins);
-        bu.addOp(changeId, new NotifyOp(ctl.getChange(), ins));
+        bu.addOp(changeId, new NotifyOp(notes.getChange(), ins));
         bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
         bu.execute();
       }
@@ -243,7 +240,10 @@
         .setVisible(
             and(
                 change.getStatus() == Change.Status.MERGED,
-                permissionBackend.user(user).ref(change.getDest()).testCond(CREATE_CHANGE)));
+                permissionBackend
+                    .user(rsrc.getUser())
+                    .ref(change.getDest())
+                    .testCond(CREATE_CHANGE)));
   }
 
   private class NotifyOp implements BatchUpdateOp {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 851202b..47edc48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -140,8 +140,8 @@
   }
 
   private List<RevisionResource> loadEdit(ChangeResource change, RevId revid)
-      throws AuthException, IOException, OrmException {
-    Optional<ChangeEdit> edit = editUtil.byChange(change.getChange());
+      throws AuthException, IOException {
+    Optional<ChangeEdit> edit = editUtil.byChange(change.getNotes(), change.getUser());
     if (edit.isPresent()) {
       PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0));
       RevId editRevId = new RevId(ObjectId.toString(edit.get().getEditCommit()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 81f830c..74fd2b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -313,31 +313,24 @@
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     Change change = resource.getChange();
-    String topic = change.getTopic();
+    if (!change.getStatus().isOpen()
+        || !resource.isCurrent()
+        || resource.getPatchSet().isDraft()
+        || !resource.permissions().testOrFalse(ChangePermission.SUBMIT)) {
+      return null; // submit not visible
+    }
+
     ReviewDb db = dbProvider.get();
     ChangeData cd = changeDataFactory.create(db, resource.getControl());
-    boolean visible;
     try {
-      visible =
-          change.getStatus().isOpen()
-              && resource.isCurrent()
-              && !resource.getPatchSet().isDraft()
-              && resource.permissions().test(ChangePermission.SUBMIT);
       MergeOp.checkSubmitRule(cd, false);
     } catch (ResourceConflictException e) {
-      visible = false;
-    } catch (PermissionBackendException e) {
-      log.error("Error checking if change is submittable", e);
-      throw new OrmRuntimeException("Could not check submit permission", e);
+      return null; // submit not visible
     } catch (OrmException e) {
       log.error("Error checking if change is submittable", e);
       throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
-    if (!visible) {
-      return new UiAction.Description().setLabel("").setTitle("").setVisible(false);
-    }
-
     ChangeSet cs;
     try {
       cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getControl().getUser());
@@ -346,6 +339,7 @@
           "Could not determine complete set of changes to be submitted", e);
     }
 
+    String topic = change.getTopic();
     int topicSize = 0;
     if (!Strings.isNullOrEmpty(topic)) {
       topicSize = getChangesByTopic(topic).size();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index ea53dc3..98e47a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -125,9 +125,7 @@
 
       if (c.getStatus().isOpen()) {
         ChangeSet cs =
-            mergeSuperSet
-                .get()
-                .completeChangeSet(dbProvider.get(), c, resource.getControl().getUser());
+            mergeSuperSet.get().completeChangeSet(dbProvider.get(), c, resource.getUser());
         cds = cs.changes().asList();
         hidden = cs.nonVisibleChanges().size();
       } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index d5c2439..d3f9186 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -96,6 +96,16 @@
     userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
     allowRegisterNewEmail = cfg.getBoolean("auth", "allowRegisterNewEmail", true);
 
+    if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP
+        && authType != AuthType.LDAP
+        && authType != AuthType.LDAP_BIND) {
+      throw new IllegalStateException(
+          "use auth.gitBasicAuthPolicy HTTP_LDAP only with auth.type LDAP or LDAP_BIND");
+    } else if (gitBasicAuthPolicy == GitBasicAuthPolicy.OAUTH && authType != AuthType.OAUTH) {
+      throw new IllegalStateException(
+          "use auth.gitBasicAuthPolicy OAUTH only with auth.type OAUTH");
+    }
+
     String key = cfg.getString("auth", null, "registerEmailPrivateKey");
     if (key != null && !key.isEmpty()) {
       int age =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index c7edc24..e58ab74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -181,8 +181,8 @@
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
-import com.google.gitiles.blame.BlameCache;
-import com.google.gitiles.blame.BlameCacheImpl;
+import com.google.gitiles.blame.cache.BlameCache;
+import com.google.gitiles.blame.cache.BlameCacheImpl;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 79dc9c2..79176e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -35,10 +35,10 @@
 import com.google.gerrit.server.edit.tree.TreeCreator;
 import com.google.gerrit.server.edit.tree.TreeModification;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
 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.InvalidChangeOperationException;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gwtorm.server.OrmException;
@@ -106,34 +106,32 @@
    * Creates a new change edit.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change for which the change edit should
-   *     be created
+   * @param notes the {@link ChangeNotes} of the change for which the change edit should be created
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if a change edit already existed for the change
    * @throws PermissionBackendException
    */
-  public void createEdit(Repository repository, ChangeControl changeControl)
+  public void createEdit(Repository repository, ChangeNotes notes)
       throws AuthException, IOException, InvalidChangeOperationException, OrmException,
           PermissionBackendException {
-    assertCanEdit(changeControl);
+    assertCanEdit(notes);
 
-    Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl);
+    Optional<ChangeEdit> changeEdit = lookupChangeEdit(notes);
     if (changeEdit.isPresent()) {
       throw new InvalidChangeOperationException(
-          String.format("A change edit already exists for change %s", changeControl.getId()));
+          String.format("A change edit already exists for change %s", notes.getChangeId()));
     }
 
-    PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
+    PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
     ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet);
-    createEdit(repository, changeControl, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
+    createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
   }
 
   /**
    * Rebase change edit on latest patch set
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit should be
-   *     rebased
+   * @param notes the {@link ChangeNotes} of the change whose change edit should be rebased
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if a change edit doesn't exist for the specified
    *     change, the change edit is already based on the latest patch set, or the change represents
@@ -141,24 +139,24 @@
    * @throws MergeConflictException if rebase fails due to merge conflicts
    * @throws PermissionBackendException
    */
-  public void rebaseEdit(Repository repository, ChangeControl changeControl)
+  public void rebaseEdit(Repository repository, ChangeNotes notes)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
           MergeConflictException, PermissionBackendException {
-    assertCanEdit(changeControl);
+    assertCanEdit(notes);
 
-    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
+    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
     if (!optionalChangeEdit.isPresent()) {
       throw new InvalidChangeOperationException(
-          String.format("No change edit exists for change %s", changeControl.getId()));
+          String.format("No change edit exists for change %s", notes.getChangeId()));
     }
     ChangeEdit changeEdit = optionalChangeEdit.get();
 
-    PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
+    PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
     if (isBasedOn(changeEdit, currentPatchSet)) {
       throw new InvalidChangeOperationException(
           String.format(
               "Change edit for change %s is already based on latest patch set %s",
-              changeControl.getId(), currentPatchSet.getId()));
+              notes.getChangeId(), currentPatchSet.getId()));
     }
 
     rebase(repository, changeEdit, currentPatchSet);
@@ -198,23 +196,22 @@
    * be created based on the current patch set.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit's message should
-   *     be modified
+   * @param notes the {@link ChangeNotes} of the change whose change edit's message should be
+   *     modified
    * @param newCommitMessage the new commit message
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws UnchangedCommitMessageException if the commit message is the same as before
    * @throws PermissionBackendException
    * @throws BadRequestException if the commit message is malformed
    */
-  public void modifyMessage(
-      Repository repository, ChangeControl changeControl, String newCommitMessage)
+  public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
       throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
           PermissionBackendException, BadRequestException {
-    assertCanEdit(changeControl);
+    assertCanEdit(notes);
     newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
 
-    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
-    PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
+    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
+    PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, notes);
     RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet);
     RevCommit baseCommit =
         optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit);
@@ -232,7 +229,7 @@
     if (optionalChangeEdit.isPresent()) {
       updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
     } else {
-      createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
+      createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp);
     }
   }
 
@@ -241,8 +238,7 @@
    * will be created based on the current patch set.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit should be
-   *     modified
+   * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param filePath the path of the file whose contents should be modified
    * @param newContent the new file content
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
@@ -250,10 +246,10 @@
    * @throws PermissionBackendException
    */
   public void modifyFile(
-      Repository repository, ChangeControl changeControl, String filePath, RawInput newContent)
+      Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
           PermissionBackendException {
-    modifyTree(repository, changeControl, new ChangeFileContentModification(filePath, newContent));
+    modifyTree(repository, notes, new ChangeFileContentModification(filePath, newContent));
   }
 
   /**
@@ -261,17 +257,16 @@
    * will be created based on the current patch set.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit should be
-   *     modified
+   * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param file path of the file which should be deleted
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if the file does not exist
    * @throws PermissionBackendException
    */
-  public void deleteFile(Repository repository, ChangeControl changeControl, String file)
+  public void deleteFile(Repository repository, ChangeNotes notes, String file)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
           PermissionBackendException {
-    modifyTree(repository, changeControl, new DeleteFileModification(file));
+    modifyTree(repository, notes, new DeleteFileModification(file));
   }
 
   /**
@@ -279,8 +274,7 @@
    * exist, a new one will be created based on the current patch set.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit should be
-   *     modified
+   * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param currentFilePath the current path/name of the file
    * @param newFilePath the desired path/name of the file
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
@@ -289,13 +283,10 @@
    * @throws PermissionBackendException
    */
   public void renameFile(
-      Repository repository,
-      ChangeControl changeControl,
-      String currentFilePath,
-      String newFilePath)
+      Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
           PermissionBackendException {
-    modifyTree(repository, changeControl, new RenameFileModification(currentFilePath, newFilePath));
+    modifyTree(repository, notes, new RenameFileModification(currentFilePath, newFilePath));
   }
 
   /**
@@ -304,27 +295,26 @@
    * current patch set.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change whose change edit should be
-   *     modified
+   * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
    * @param file the path of the file which should be restored
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if the file was already restored
    * @throws PermissionBackendException
    */
-  public void restoreFile(Repository repository, ChangeControl changeControl, String file)
+  public void restoreFile(Repository repository, ChangeNotes notes, String file)
       throws AuthException, InvalidChangeOperationException, IOException, OrmException,
           PermissionBackendException {
-    modifyTree(repository, changeControl, new RestoreFileModification(file));
+    modifyTree(repository, notes, new RestoreFileModification(file));
   }
 
   private void modifyTree(
-      Repository repository, ChangeControl changeControl, TreeModification treeModification)
+      Repository repository, ChangeNotes notes, TreeModification treeModification)
       throws AuthException, IOException, OrmException, InvalidChangeOperationException,
           PermissionBackendException {
-    assertCanEdit(changeControl);
+    assertCanEdit(notes);
 
-    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
-    PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
+    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
+    PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, notes);
     RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet);
     RevCommit baseCommit =
         optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit);
@@ -339,7 +329,7 @@
     if (optionalChangeEdit.isPresent()) {
       updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
     } else {
-      createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
+      createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp);
     }
   }
 
@@ -349,7 +339,7 @@
    * change edit doesn't exist, a new one will be created.
    *
    * @param repository the affected Git repository
-   * @param changeControl the {@code ChangeControl} of the change to which the patch set belongs
+   * @param notes the {@link ChangeNotes} of the change to which the patch set belongs
    * @param patchSet the {@code PatchSet} which should be modified
    * @param treeModifications the modifications which should be applied
    * @return the resulting {@code ChangeEdit}
@@ -361,15 +351,15 @@
    */
   public ChangeEdit combineWithModifiedPatchSetTree(
       Repository repository,
-      ChangeControl changeControl,
+      ChangeNotes notes,
       PatchSet patchSet,
       List<TreeModification> treeModifications)
       throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
           OrmException, PermissionBackendException {
-    assertCanEdit(changeControl);
+    assertCanEdit(notes);
 
-    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
-    ensureAllowedPatchSet(changeControl, optionalChangeEdit, patchSet);
+    Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
+    ensureAllowedPatchSet(notes, optionalChangeEdit, patchSet);
 
     RevCommit patchSetCommit = lookupCommit(repository, patchSet);
     ObjectId newTreeId = createNewTree(repository, patchSetCommit, treeModifications);
@@ -392,11 +382,10 @@
     if (optionalChangeEdit.isPresent()) {
       return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
     }
-    return createEdit(repository, changeControl, patchSet, newEditCommit, nowTimestamp);
+    return createEdit(repository, notes, patchSet, newEditCommit, nowTimestamp);
   }
 
-  private void assertCanEdit(ChangeControl changeControl)
-      throws AuthException, PermissionBackendException {
+  private void assertCanEdit(ChangeNotes notes) throws AuthException, PermissionBackendException {
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -404,7 +393,7 @@
       permissionBackend
           .user(currentUser)
           .database(reviewDb)
-          .change(changeControl.getNotes())
+          .change(notes)
           .check(ChangePermission.ADD_PATCH_SET);
     } catch (AuthException denied) {
       throw new AuthException("edit not permitted", denied);
@@ -412,7 +401,7 @@
   }
 
   private static void ensureAllowedPatchSet(
-      ChangeControl changeControl, Optional<ChangeEdit> optionalChangeEdit, PatchSet patchSet)
+      ChangeNotes notes, Optional<ChangeEdit> optionalChangeEdit, PatchSet patchSet)
       throws InvalidChangeOperationException {
     if (optionalChangeEdit.isPresent()) {
       ChangeEdit changeEdit = optionalChangeEdit.get();
@@ -425,7 +414,7 @@
       }
     } else {
       PatchSet.Id patchSetId = patchSet.getId();
-      PatchSet.Id currentPatchSetId = changeControl.getChange().currentPatchSetId();
+      PatchSet.Id currentPatchSetId = notes.getChange().currentPatchSetId();
       if (!patchSetId.equals(currentPatchSetId)) {
         throw new InvalidChangeOperationException(
             String.format(
@@ -435,21 +424,19 @@
     }
   }
 
-  private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl)
+  private Optional<ChangeEdit> lookupChangeEdit(ChangeNotes notes)
       throws AuthException, IOException {
-    return changeEditUtil.byChange(changeControl);
+    return changeEditUtil.byChange(notes);
   }
 
-  private PatchSet getBasePatchSet(
-      Optional<ChangeEdit> optionalChangeEdit, ChangeControl changeControl) throws OrmException {
+  private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, ChangeNotes notes)
+      throws OrmException {
     Optional<PatchSet> editBasePatchSet = optionalChangeEdit.map(ChangeEdit::getBasePatchSet);
-    return editBasePatchSet.isPresent()
-        ? editBasePatchSet.get()
-        : lookupCurrentPatchSet(changeControl);
+    return editBasePatchSet.isPresent() ? editBasePatchSet.get() : lookupCurrentPatchSet(notes);
   }
 
-  private PatchSet lookupCurrentPatchSet(ChangeControl changeControl) throws OrmException {
-    return patchSetUtil.current(reviewDb.get(), changeControl.getNotes());
+  private PatchSet lookupCurrentPatchSet(ChangeNotes notes) throws OrmException {
+    return patchSetUtil.current(reviewDb.get(), notes);
   }
 
   private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
@@ -531,12 +518,12 @@
 
   private ChangeEdit createEdit(
       Repository repository,
-      ChangeControl changeControl,
+      ChangeNotes notes,
       PatchSet basePatchSet,
       ObjectId newEditCommitId,
       Timestamp timestamp)
       throws IOException, OrmException {
-    Change change = changeControl.getChange();
+    Change change = notes.getChange();
     String editRefName = getEditRefName(change, basePatchSet);
     updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp);
     reindex(change);
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 0d84767..743df20 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
@@ -38,8 +38,8 @@
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.RepoContext;
@@ -70,10 +70,9 @@
 public class ChangeEditUtil {
   private final GitRepositoryManager gitManager;
   private final PatchSetInserter.Factory patchSetInserterFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeIndexer indexer;
   private final Provider<ReviewDb> db;
-  private final Provider<CurrentUser> user;
+  private final Provider<CurrentUser> userProvider;
   private final ChangeKindCache changeKindCache;
   private final PatchSetUtil psUtil;
 
@@ -81,40 +80,32 @@
   ChangeEditUtil(
       GitRepositoryManager gitManager,
       PatchSetInserter.Factory patchSetInserterFactory,
-      ChangeControl.GenericFactory changeControlFactory,
       ChangeIndexer indexer,
       Provider<ReviewDb> db,
-      Provider<CurrentUser> user,
+      Provider<CurrentUser> userProvider,
       ChangeKindCache changeKindCache,
       PatchSetUtil psUtil) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
-    this.changeControlFactory = changeControlFactory;
     this.indexer = indexer;
     this.db = db;
-    this.user = user;
+    this.userProvider = userProvider;
     this.changeKindCache = changeKindCache;
     this.psUtil = psUtil;
   }
 
   /**
-   * Retrieve edit for a change and the user from the request scope.
+   * Retrieve edit for a given change.
    *
    * <p>At most one change edit can exist per user and change.
    *
-   * @param change
+   * @param notes change notes of change to retrieve change edits for.
    * @return edit for this change for this user, if present.
-   * @throws AuthException
-   * @throws IOException
-   * @throws OrmException
+   * @throws AuthException if this is not a logged-in user.
+   * @throws IOException if an error occurs.
    */
-  public Optional<ChangeEdit> byChange(Change change)
-      throws AuthException, IOException, OrmException {
-    try {
-      return byChange(changeControlFactory.controlFor(db.get(), change, user.get()));
-    } catch (NoSuchChangeException e) {
-      throw new IOException(e);
-    }
+  public Optional<ChangeEdit> byChange(ChangeNotes notes) throws AuthException, IOException {
+    return byChange(notes, userProvider.get());
   }
 
   /**
@@ -122,17 +113,19 @@
    *
    * <p>At most one change edit can exist per user and change.
    *
-   * @param ctl control with user to retrieve change edits for.
+   * @param notes change notes of change to retrieve change edits for.
+   * @param user user to retrieve edits as.
    * @return edit for this change for this user, if present.
    * @throws AuthException if this is not a logged-in user.
    * @throws IOException if an error occurs.
    */
-  public Optional<ChangeEdit> byChange(ChangeControl ctl) throws AuthException, IOException {
-    if (!ctl.getUser().isIdentifiedUser()) {
+  public Optional<ChangeEdit> byChange(ChangeNotes notes, CurrentUser user)
+      throws AuthException, IOException {
+    if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    IdentifiedUser u = ctl.getUser().asIdentifiedUser();
-    Change change = ctl.getChange();
+    IdentifiedUser u = user.asIdentifiedUser();
+    Change change = notes.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject())) {
       int n = change.currentPatchSetId().get();
       String[] refNames = new String[n];
@@ -146,7 +139,7 @@
       }
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(ref.getObjectId());
-        PatchSet basePs = getBasePatchSet(ctl, ref);
+        PatchSet basePs = getBasePatchSet(notes, ref);
         return Optional.of(new ChangeEdit(change, ref.getName(), commit, basePs));
       }
     }
@@ -244,13 +237,13 @@
     indexer.index(db.get(), change);
   }
 
-  private PatchSet getBasePatchSet(ChangeControl ctl, Ref ref) throws IOException {
+  private PatchSet getBasePatchSet(ChangeNotes notes, Ref ref) throws IOException {
     try {
       int pos = ref.getName().lastIndexOf("/");
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
       return psUtil.get(
-          db.get(), ctl.getNotes(), new PatchSet.Id(ctl.getId(), Integer.parseInt(psId)));
+          db.get(), notes, new PatchSet.Id(notes.getChange().getId(), Integer.parseInt(psId)));
     } catch (OrmException | NumberFormatException e) {
       throw new IOException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index d4e085c..286ba12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2434,7 +2434,7 @@
       Optional<ChangeEdit> edit = null;
 
       try {
-        edit = editUtil.byChange(projectControl.controlFor(notes));
+        edit = editUtil.byChange(notes, user);
       } catch (AuthException | IOException e) {
         logError("Cannot retrieve edit", e);
         return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
index bb6b427..ed1d8b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -73,7 +73,8 @@
               }
             }
           };
-      t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), newVersion));
+      t.setName(
+          String.format("Reindex %s v%d-v%d", name, version(indexes.getSearchIndex()), newVersion));
       t.start();
     }
   }
@@ -97,23 +98,26 @@
     index =
         checkNotNull(
             indexes.getWriteIndex(newVersion),
-            "not an active write schema version: %s",
+            "not an active write schema version: %s %s",
+            name,
             newVersion);
     log.info(
-        "Starting online reindex from schema version {} to {}",
+        "Starting online reindex of {} from schema version {} to {}",
+        name,
         version(indexes.getSearchIndex()),
         version(index));
     SiteIndexer.Result result = batchIndexer.indexAll(index);
     if (!result.success()) {
       log.error(
-          "Online reindex of schema version {} failed. Successfully"
-              + " indexed {} changes, failed to index {} changes",
+          "Online reindex of {} schema version {} failed. Successfully"
+              + " indexed {}, failed to index {}",
+          name,
           version(index),
           result.doneCount(),
           result.failedCount());
       return;
     }
-    log.info("Reindex to version {} complete", version(index));
+    log.info("Reindex {} to version {} complete", name, version(index));
     activateIndex();
     for (OnlineUpgradeListener listener : listeners) {
       listener.onSuccess(name, oldVersion, newVersion);
@@ -122,11 +126,11 @@
 
   public void activateIndex() {
     indexes.setSearchIndex(index);
-    log.info("Using schema version {}", version(index));
+    log.info("Using {} schema version {}", name, version(index));
     try {
       index.markReady(true);
     } catch (IOException e) {
-      log.warn("Error activating new schema version {}", version(index));
+      log.warn("Error activating new {} schema version {}", name, version(index));
     }
 
     List<I> toRemove = Lists.newArrayListWithExpectedSize(1);
@@ -140,7 +144,7 @@
         i.markReady(false);
         indexes.removeWriteIndex(version(i));
       } catch (IOException e) {
-        log.warn("Error deactivating old schema version {}", version(i));
+        log.warn("Error deactivating old {} schema version {}", name, version(i));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 081ba7a..e727d1a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -270,7 +270,7 @@
   }
 
   private ObjectId getEditRev() throws AuthException, IOException, OrmException {
-    edit = editReader.byChange(change);
+    edit = editReader.byChange(control.getNotes());
     if (edit.isPresent()) {
       return edit.get().getEditCommit();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index a932923..88bff63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -615,14 +615,11 @@
       }
       throw new IllegalStateException("user already specified: " + changeControl.getUser());
     }
-    try {
-      if (change != null) {
-        changeControl = changeControlFactory.controlFor(db, change, user);
-      } else {
-        changeControl = changeControlFactory.controlFor(db, project(), legacyId, user);
-      }
-    } catch (NoSuchChangeException e) {
-      throw new OrmException(e);
+
+    if (change != null) {
+      changeControl = changeControlFactory.controlFor(db, change, user);
+    } else {
+      changeControl = changeControlFactory.controlFor(db, project(), legacyId, user);
     }
     return changeControl;
   }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index bb293cc..2d1574b 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -48,9 +48,11 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
+import java.util.Set;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.IllegalAnnotationError;
@@ -381,7 +383,7 @@
     }
 
     private static String getPrefixedName(String prefix, String name) {
-      return "--" + prefix + name;
+      return prefix + name;
     }
   }
 
@@ -393,11 +395,19 @@
 
     MyParser(Object bean) {
       super(bean);
+      parseAdditionalOptions("", bean, new HashSet<>());
       ensureOptionsInitialized();
     }
 
     // NOTE: Argument annotations on bean are ignored.
     public void parseWithPrefix(String prefix, Object bean) {
+      parseWithPrefix(prefix, bean, new HashSet<>());
+    }
+
+    private void parseWithPrefix(String prefix, Object bean, Set<Object> parsedBeans) {
+      if (!parsedBeans.add(bean)) {
+        return;
+      }
       // recursively process all the methods/fields.
       for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) {
         for (Method m : c.getDeclaredMethods()) {
@@ -411,6 +421,31 @@
           if (o != null) {
             addOption(Setters.create(f, bean), new PrefixedOption(prefix, o));
           }
+          if (f.isAnnotationPresent(Options.class)) {
+            try {
+              parseWithPrefix(
+                  prefix + f.getAnnotation(Options.class).prefix(), f.get(bean), parsedBeans);
+            } catch (IllegalAccessException e) {
+              throw new IllegalAnnotationError(e);
+            }
+          }
+        }
+      }
+    }
+
+    private void parseAdditionalOptions(String prefix, Object bean, Set<Object> parsedBeans) {
+      for (Class c = bean.getClass(); c != null; c = c.getSuperclass()) {
+        for (Field f : c.getDeclaredFields()) {
+          if (f.isAnnotationPresent(Options.class)) {
+            Object additionalBean = null;
+            try {
+              additionalBean = f.get(bean);
+            } catch (IllegalAccessException e) {
+              throw new IllegalAnnotationError(e);
+            }
+            parseWithPrefix(
+                prefix + f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
+          }
         }
       }
     }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/Options.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/Options.java
new file mode 100644
index 0000000..96613df
--- /dev/null
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/Options.java
@@ -0,0 +1,33 @@
+// 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.util.cli;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a field that refers to a class with @Option annotations
+ *
+ * <p>Any @Option annotations found on the referred class will be handled as if they were found on
+ * the referring class.
+ */
+@Retention(RUNTIME)
+@Target({FIELD})
+public @interface Options {
+  String prefix() default "";
+}
diff --git a/plugins/hooks b/plugins/hooks
index 18edefa..b7fe209 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 18edefac123218ef61af7e4926b2c56824eef03a
+Subproject commit b7fe2092e856db2b76c24a95ae79456df8637b87
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 527c83a..0290cb1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -324,7 +324,7 @@
           margin-right: 0;
         }
         .scrollable {
-          @apply(--layout-scroll);
+          overflow: auto;
         }
       }
     </style>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 0f30171..d20d7bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -313,7 +313,7 @@
       const isOnParent =
         this._getIsParentCommentByLineAndContent(lineEl, contentEl);
       const threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum,
-          side, isOnParent);
+          side, isOnParent, opt_range);
       threadEl.addOrEditDraft(opt_lineNum, opt_range);
     },
 
@@ -326,6 +326,20 @@
     },
 
     /**
+     * @param {string} commentSide
+     * @param {!Object=} opt_range
+     */
+    _getRangeString(commentSide, opt_range) {
+      return opt_range ?
+        'range-' +
+        opt_range.startLine + '-' +
+        opt_range.startChar + '-' +
+        opt_range.endLine + '-' +
+        opt_range.endChar + '-' +
+        commentSide : 'line-' + commentSide;
+    },
+
+    /**
      * @param {!Object} contentEl
      * @param {number} patchNum
      * @param {string} commentSide
@@ -334,13 +348,7 @@
      */
     _getOrCreateThreadAtLineRange(contentEl, patchNum, commentSide,
         isOnParent, opt_range) {
-      const rangeToCheck = opt_range ?
-          'range-' +
-          opt_range.startLine + '-' +
-          opt_range.startChar + '-' +
-          opt_range.endLine + '-' +
-          opt_range.endChar + '-' +
-          commentSide : 'line-' + commentSide;
+      const rangeToCheck = this._getRangeString(commentSide, opt_range);
 
       // Check if thread group exists.
       let threadGroupEl = this._getThreadGroupForLine(contentEl);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index a769a96..f540c34 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -224,6 +224,20 @@
         });
       });
 
+      test('_getRangeString', () => {
+        const side = 'PARENT';
+        const range = {
+          startLine: 1,
+          startChar: 1,
+          endLine: 1,
+          endChar: 2,
+        };
+        assert.equal(element._getRangeString(side, range),
+            'range-1-1-1-2-PARENT');
+        assert.equal(element._getRangeString(side, null),
+            'line-PARENT');
+      }),
+
       test('thread groups', () => {
         const contentEl = document.createElement('div');
         const commentSide = 'left';
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 0820e47..e838818 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
@@ -1531,7 +1531,7 @@
     getChangeFileContents(changeId, patchNum, path, opt_parentIndex) {
       const parent = typeof opt_parentIndex === 'number' ?
           '?parent=' + opt_parentIndex : '';
-      return this._changeBaseUrl(changeId, patchNum).then(url => {
+      return this._changeBaseURL(changeId, patchNum).then(url => {
         url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
         return this._fetchB64File(url);
       });