Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Upgrade bazlets to latest stable-2.14 to build with 2.14.21 API

Change-Id: I015695a94f4c6219baf9fef118a2523dc36a6c98
diff --git a/.gitignore b/.gitignore
index 1c54bab..681ddba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
+# `LC_COLLATE=C sort`
+/.apt_generated/
 /.classpath
 /.project
 /.settings/
 /bazel-*
+/bin/
 /eclipse-out/
diff --git a/README.md b/README.md
index b133981..e14491a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 # Rename project plugin for Gerrit Code Review
 
-This plugin currently only supports Gerrit version 2.14.X.
+This plugin currently supports Gerrit version 2.14.X and 2.15.X with changes in reviewDb.
+Also supported is the noteDb alternative for Gerrit versions 2.15.X and above.
 
 For more information, see: `src/main/resources/Documentation/about.md`
diff --git a/WORKSPACE b/WORKSPACE
index 29d847a..291766e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,10 +3,19 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "78c35a7eb33ee5ea0980923e246c7dba37347193",
+    commit = "f53f51fb660552d0581aa0ba52c3836ed63d56a3",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
+# Snapshot Plugin API
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+#    "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
 # Release Plugin API
 load(
     "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 1d8851b..c1932a9 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,8 +3,8 @@
 def external_plugin_deps():
     maven_jar(
         name = "mockito",
-        artifact = "org.mockito:mockito-core:2.27.0",
-        sha1 = "835fc3283b481f4758b8ef464cd560c649c08b00",
+        artifact = "org.mockito:mockito-core:2.28.2",
+        sha1 = "91110215a8cb9b77a46e045ee758f77d79167cc0",
         deps = [
             "@byte-buddy//jar",
             "@byte-buddy-agent//jar",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
index db344d5..6962811 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
@@ -44,5 +44,6 @@
     bind(FilesystemRenameHandler.class);
     bind(RenamePreconditions.class);
     bind(IndexUpdateHandler.class);
+    bind(RevertRenameProject.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
index 3afc396..e21aa50 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
@@ -17,8 +17,11 @@
 import static com.googlesource.gerrit.plugins.renameproject.RenameOwnProjectCapability.RENAME_OWN_PROJECT;
 import static com.googlesource.gerrit.plugins.renameproject.RenameProjectCapability.RENAME_PROJECT;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -26,13 +29,15 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.extensions.events.PluginEvent;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import com.googlesource.gerrit.plugins.renameproject.cache.CacheRenameHandler;
 import com.googlesource.gerrit.plugins.renameproject.conditions.RenamePreconditions;
 import com.googlesource.gerrit.plugins.renameproject.database.DatabaseRenameHandler;
@@ -40,6 +45,7 @@
 import com.googlesource.gerrit.plugins.renameproject.fs.FilesystemRenameHandler;
 import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
@@ -54,6 +60,7 @@
 
   static final int WARNING_LIMIT = 5000;
   private static final Logger log = LoggerFactory.getLogger(RenameProject.class);
+  private static final String CACHE_NAME = "changeid_project";
 
   private final DatabaseRenameHandler dbHandler;
   private final FilesystemRenameHandler fsHandler;
@@ -65,6 +72,11 @@
   private final PluginEvent pluginEvent;
   private final String pluginName;
   private final RenameLog renameLog;
+  private final PermissionBackend permissionBackend;
+  private final Cache<Change.Id, String> changeIdProjectCache;
+  private final RevertRenameProject revertRenameProject;
+
+  private List<Step> stepsPerformed;
 
   @Inject
   RenameProject(
@@ -77,7 +89,10 @@
       LockUnlockProject lockUnlockProject,
       PluginEvent pluginEvent,
       @PluginName String pluginName,
-      RenameLog renameLog) {
+      RenameLog renameLog,
+      PermissionBackend permissionBackend,
+      @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
+      RevertRenameProject revertRenameProject) {
     this.dbHandler = dbHandler;
     this.fsHandler = fsHandler;
     this.cacheHandler = cacheHandler;
@@ -88,6 +103,10 @@
     this.pluginEvent = pluginEvent;
     this.pluginName = pluginName;
     this.renameLog = renameLog;
+    this.permissionBackend = permissionBackend;
+    this.changeIdProjectCache = changeIdProjectCache;
+    this.revertRenameProject = revertRenameProject;
+    this.stepsPerformed = new ArrayList<>();
   }
 
   private void assertNewNameNotNull(Input input) throws BadRequestException {
@@ -103,10 +122,11 @@
   }
 
   protected boolean canRename(ProjectResource rsrc) {
-    CapabilityControl ctl = userProvider.get().getCapabilities();
-    return ctl.canAdministrateServer()
-        || ctl.canPerform(pluginName + "-" + RENAME_PROJECT)
-        || (ctl.canPerform(pluginName + "-" + RENAME_OWN_PROJECT) && rsrc.getControl().isOwner());
+    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider);
+    return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+        || userPermission.testOrFalse(new PluginPermission(pluginName, RENAME_PROJECT))
+        || (userPermission.testOrFalse(new PluginPermission(pluginName, RENAME_OWN_PROJECT))
+            && rsrc.getControl().isOwner());
   }
 
   void assertCanRename(ProjectResource rsrc, Input input, ProgressMonitor pm)
@@ -127,24 +147,45 @@
     Project.NameKey oldProjectKey = rsrc.getControl().getProject().getNameKey();
     Project.NameKey newProjectKey = new Project.NameKey(input.name);
     Exception ex = null;
+    stepsPerformed.clear();
     try {
-      fsHandler.rename(oldProjectKey, newProjectKey, pm);
-      log.debug("Renamed the git repo to {} successfully.", newProjectKey.get());
-      cacheHandler.update(rsrc.getControl().getProject(), newProjectKey);
+      fsRenameStep(oldProjectKey, newProjectKey, pm);
 
-      List<Change.Id> updatedChangeIds =
-          dbHandler.rename(changeIds, oldProjectKey, newProjectKey, pm);
-      log.debug("Updated the changes in DB successfully for project {}.", oldProjectKey.get());
+      cacheRenameStep(rsrc.getControl().getProject().getNameKey(), newProjectKey);
+
+      List<Change.Id> updatedChangeIds = dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
 
       // if the DB update is successful, update the secondary index
-      indexHandler.updateIndex(updatedChangeIds, newProjectKey, pm);
-      log.debug("Updated the secondary index successfully for project {}.", oldProjectKey.get());
+      indexRenameStep(updatedChangeIds, oldProjectKey, newProjectKey, pm);
 
+      // no need to revert this since newProjectKey will be removed from project cache before
       lockUnlockProject.unlock(newProjectKey);
       log.debug("Unlocked the repo {} after rename operation.", newProjectKey.get());
 
+      // flush old changeId -> Project cache for given changeIds
+      changeIdProjectCache.invalidateAll(changeIds);
+
       pluginEvent.fire(pluginName, pluginName, oldProjectKey.get() + ":" + newProjectKey.get());
     } catch (Exception e) {
+      if (stepsPerformed.isEmpty()) {
+        log.error("Renaming procedure failed. Exception caught: {}", e.toString());
+      } else {
+        log.error(
+            "Renaming procedure failed, last successful step {}. Exception caught: {}",
+            stepsPerformed.get(stepsPerformed.size() - 1).toString(),
+            e.toString());
+      }
+      try {
+        revertRenameProject.performRevert(
+            stepsPerformed, changeIds, oldProjectKey, newProjectKey, pm);
+      } catch (Exception revertEx) {
+        log.error(
+            "Failed to revert renaming procedure for {}. Exception caught: {}",
+            oldProjectKey.get(),
+            revertEx.toString());
+        ex = revertEx;
+        throw new RenameRevertException(revertEx, e);
+      }
       ex = e;
       throw e;
     } finally {
@@ -152,7 +193,72 @@
     }
   }
 
-  List<Change.Id> getChanges(ProjectResource rsrc, ProgressMonitor pm) throws OrmException {
+  void fsRenameStep(
+      Project.NameKey oldProjectKey, Project.NameKey newProjectKey, ProgressMonitor pm)
+      throws IOException {
+    fsHandler.rename(oldProjectKey, newProjectKey, pm);
+    logPerformedStep(Step.FILESYSTEM, newProjectKey, oldProjectKey);
+  }
+
+  void cacheRenameStep(Project.NameKey oldProjectKey, Project.NameKey newProjectKey) {
+    cacheHandler.update(oldProjectKey, newProjectKey);
+    logPerformedStep(Step.CACHE, newProjectKey, oldProjectKey);
+  }
+
+  List<Change.Id> dbRenameStep(
+      List<Change.Id> changeIds,
+      Project.NameKey oldProjectKey,
+      Project.NameKey newProjectKey,
+      ProgressMonitor pm)
+      throws OrmException {
+    List<Change.Id> updatedChangeIds =
+        dbHandler.rename(changeIds, oldProjectKey, newProjectKey, pm);
+    logPerformedStep(Step.DATABASE, newProjectKey, oldProjectKey);
+    return updatedChangeIds;
+  }
+
+  void indexRenameStep(
+      List<Change.Id> updatedChangeIds,
+      Project.NameKey oldProjectKey,
+      Project.NameKey newProjectKey,
+      ProgressMonitor pm)
+      throws InterruptedException {
+    indexHandler.updateIndex(updatedChangeIds, newProjectKey, pm);
+    logPerformedStep(Step.INDEX, newProjectKey, oldProjectKey);
+  }
+
+  enum Step {
+    FILESYSTEM,
+    CACHE,
+    DATABASE,
+    INDEX
+  }
+
+  private void logPerformedStep(
+      Step step, Project.NameKey newProjectKey, Project.NameKey oldProjectKey) {
+    stepsPerformed.add(step);
+    switch (step) {
+      case FILESYSTEM:
+        log.debug("Renamed the git repo to {} successfully.", newProjectKey.get());
+        break;
+      case CACHE:
+        log.debug("Successfully updated project cache for project {}.", newProjectKey.get());
+        break;
+      case DATABASE:
+        log.debug("Updated the changes in DB successfully for project {}.", oldProjectKey.get());
+        break;
+      case INDEX:
+        log.debug("Updated the secondary index successfully for project {}.", oldProjectKey.get());
+    }
+  }
+
+  @VisibleForTesting
+  List<Step> getStepsPerformed() {
+    return stepsPerformed;
+  }
+
+  List<Change.Id> getChanges(ProjectResource rsrc, ProgressMonitor pm)
+      throws OrmException, IOException {
     pm.beginTask("Retrieving the list of changes from DB");
     Project.NameKey oldProjectKey = rsrc.getControl().getProject().getNameKey();
     return dbHandler.getChangeIds(oldProjectKey);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameRevertException.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameRevertException.java
new file mode 100644
index 0000000..0cb6256
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameRevertException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.renameproject;
+
+import com.google.gwtorm.server.OrmException;
+
+/** Add cause for exception during revert operation */
+public class RenameRevertException extends OrmException {
+  private static final long serialVersionUID = 1L;
+
+  public RenameRevertException(Throwable revertException, Throwable cause) {
+    super(
+        "Failed to revert after failed rename. Revert cause: " + cause.getMessage(),
+        revertException.initCause(cause));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java
new file mode 100644
index 0000000..8b7b700
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.renameproject;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.renameproject.RenameProject.Step;
+import com.googlesource.gerrit.plugins.renameproject.cache.CacheRenameHandler;
+import com.googlesource.gerrit.plugins.renameproject.database.DatabaseRenameHandler;
+import com.googlesource.gerrit.plugins.renameproject.database.IndexUpdateHandler;
+import com.googlesource.gerrit.plugins.renameproject.fs.FilesystemRenameHandler;
+import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RevertRenameProject {
+  private static final Logger log = LoggerFactory.getLogger(RevertRenameProject.class);
+
+  private final DatabaseRenameHandler dbHandler;
+  private final FilesystemRenameHandler fsHandler;
+  private final CacheRenameHandler cacheHandler;
+  private final IndexUpdateHandler indexHandler;
+
+  @Inject
+  RevertRenameProject(
+      DatabaseRenameHandler dbHandler,
+      FilesystemRenameHandler fsHandler,
+      CacheRenameHandler cacheHandler,
+      IndexUpdateHandler indexHandler) {
+    this.dbHandler = dbHandler;
+    this.fsHandler = fsHandler;
+    this.cacheHandler = cacheHandler;
+    this.indexHandler = indexHandler;
+  }
+
+  void performRevert(
+      List<Step> stepsPerformed,
+      List<Id> changeIds,
+      Project.NameKey oldProjectKey,
+      Project.NameKey newProjectKey,
+      ProgressMonitor pm)
+      throws IOException, OrmException {
+    pm.beginTask("Reverting the rename procedure.");
+    List<Change.Id> updatedChangeIds = Collections.emptyList();
+    if (stepsPerformed.contains(Step.FILESYSTEM)) {
+      try {
+        fsHandler.rename(newProjectKey, oldProjectKey, pm);
+        log.debug("Reverted the git repo name to {} successfully.", oldProjectKey.get());
+      } catch (IOException e) {
+        log.error(
+            "Failed to revert git repo name. Aborting revert. Exception caught: {}", e.toString());
+        throw e;
+      }
+    }
+    if (stepsPerformed.contains(Step.CACHE)) {
+      cacheHandler.update(newProjectKey, oldProjectKey);
+      log.debug("Successfully removed project {} from project cache.", newProjectKey.get());
+    }
+    if (stepsPerformed.contains(Step.DATABASE)) {
+      try {
+        updatedChangeIds = dbHandler.rename(changeIds, newProjectKey, oldProjectKey, pm);
+        log.debug(
+            "Reverted the changes in DB successfully from project {} to project {}.",
+            newProjectKey.get(),
+            oldProjectKey.get());
+      } catch (OrmException e) {
+        log.error(
+            "Failed to revert changes in DB for project {}. Secondary indexes not reverted. Exception caught: {}",
+            oldProjectKey.get(),
+            e.toString());
+        throw e;
+      }
+    }
+    if (stepsPerformed.contains(Step.INDEX)) {
+      try {
+        indexHandler.updateIndex(updatedChangeIds, oldProjectKey, pm);
+        log.debug(
+            "Reverted the secondary index successfully from project {} to project {}.",
+            newProjectKey.get(),
+            oldProjectKey.get());
+      } catch (InterruptedException e) {
+        log.error(
+            "Secondary index revert failed for {}. Exception caught: {}",
+            oldProjectKey.get(),
+            e.toString());
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/cache/CacheRenameHandler.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/cache/CacheRenameHandler.java
index c9ff8cf..0165835 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/cache/CacheRenameHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/cache/CacheRenameHandler.java
@@ -29,8 +29,8 @@
     this.projectCache = projectCache;
   }
 
-  public void update(Project oldProject, Project.NameKey newProjectKey) {
-    projectCache.remove(oldProject);
+  public void update(Project.NameKey oldProjectKey, Project.NameKey newProjectKey) {
+    projectCache.remove(oldProjectKey);
     projectCache.onCreateProject(newProjectKey);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/conditions/RenamePreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/conditions/RenamePreconditions.java
index e1104dc..2246d50 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/conditions/RenamePreconditions.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/conditions/RenamePreconditions.java
@@ -22,7 +22,9 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOpRepoManager;
+import com.google.gerrit.server.git.SubmoduleException;
 import com.google.gerrit.server.git.SubmoduleOp;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
@@ -87,14 +89,19 @@
   }
 
   private void assertHasNoChildProjects(ProjectResource rsrc) throws CannotRenameProjectException {
-    List<ProjectInfo> children = listChildProjectsProvider.get().apply(rsrc);
-    if (!children.isEmpty()) {
-      String childrenString =
-          String.join(", ", children.stream().map(info -> info.name).collect(Collectors.toList()));
-      String message =
-          String.format("Cannot rename project because it has children: %s", childrenString);
-      log.error(message);
-      throw new CannotRenameProjectException(message);
+    try {
+      List<ProjectInfo> children = listChildProjectsProvider.get().apply(rsrc);
+      if (!children.isEmpty()) {
+        String childrenString =
+            String.join(
+                ", ", children.stream().map(info -> info.name).collect(Collectors.toList()));
+        String message =
+            String.format("Cannot rename project because it has children: %s", childrenString);
+        log.error(message);
+        throw new CannotRenameProjectException(message);
+      }
+    } catch (PermissionBackendException e) {
+      throw new CannotRenameProjectException(e);
     }
   }
 
@@ -113,7 +120,7 @@
           throw new CannotRenameProjectException(message);
         }
       }
-    } catch (IOException e) {
+    } catch (IOException | SubmoduleException e) {
       throw new CannotRenameProjectException(e);
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
index 3215c42..671dc27 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
@@ -21,7 +21,12 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.WatchConfig;
 import com.google.gerrit.server.account.WatchConfig.Accessor;
+import com.google.gerrit.server.account.WatchConfig.NotifyType;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -29,6 +34,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.renameproject.RenameRevertException;
 import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
 import java.io.IOException;
 import java.sql.Connection;
@@ -36,7 +42,11 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -46,24 +56,41 @@
   private static final Logger log = LoggerFactory.getLogger(DatabaseRenameHandler.class);
 
   private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ChangeNotes.Factory schemaFactoryNoteDb;
+  private final GitRepositoryManager repoManager;
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final Provider<Accessor> watchConfig;
+  private NotesMigration migration;
 
   @Inject
   public DatabaseRenameHandler(
       SchemaFactory<ReviewDb> schemaFactory,
+      ChangeNotes.Factory schemaFactoryNoteDb,
+      GitRepositoryManager repoManager,
+      NotesMigration migration,
       Provider<InternalAccountQuery> accountQueryProvider,
       Provider<WatchConfig.Accessor> watchConfig) {
     this.accountQueryProvider = accountQueryProvider;
     this.watchConfig = watchConfig;
     this.schemaFactory = schemaFactory;
+    this.schemaFactoryNoteDb = schemaFactoryNoteDb;
+    this.repoManager = repoManager;
+    this.migration = migration;
   }
 
-  public List<Change.Id> getChangeIds(Project.NameKey oldProjectKey) throws OrmException {
+  public List<Change.Id> getChangeIds(Project.NameKey oldProjectKey)
+      throws OrmException, IOException {
     log.debug("Starting to retrieve changes from the DB for project {}", oldProjectKey.get());
+    ReviewDb db = schemaFactory.open();
+    return (isNoteDb())
+        ? getChangeIdsFromNoteDb(oldProjectKey, db)
+        : getChangeIdsFromReviewDb(oldProjectKey, db);
+  }
 
+  private List<Change.Id> getChangeIdsFromReviewDb(Project.NameKey oldProjectKey, ReviewDb db)
+      throws OrmException {
     List<Change.Id> changeIds = new ArrayList<>();
-    Connection conn = ((JdbcSchema) schemaFactory.open()).getConnection();
+    Connection conn = ((JdbcSchema) db).getConnection();
     String query =
         "select change_id from changes where dest_project_name ='" + oldProjectKey.get() + "';";
     try (Statement stmt = conn.createStatement();
@@ -73,7 +100,7 @@
         changeIds.add(changeId);
       }
       log.debug(
-          "Number of changes related to the project {} are {}",
+          "Number of changes in reviewDb related to project {} are {}",
           oldProjectKey.get(),
           changeIds.size());
       return changeIds;
@@ -82,71 +109,165 @@
     }
   }
 
+  private List<Change.Id> getChangeIdsFromNoteDb(Project.NameKey oldProjectKey, ReviewDb db)
+      throws IOException {
+    List<Change.Id> changeIds = new ArrayList<>();
+    Stream<ChangeNotesResult> changes =
+        schemaFactoryNoteDb.scan(repoManager.openRepository(oldProjectKey), db, oldProjectKey);
+    Iterator<ChangeNotesResult> iterator = changes.iterator();
+    while (iterator.hasNext()) {
+      ChangeNotesResult change = iterator.next();
+      changeIds.add(change.id());
+    }
+    log.debug(
+        "Number of changes in noteDb related to project {} are {}",
+        oldProjectKey.get(),
+        changeIds.size());
+    return changeIds;
+  }
+
+  private boolean isNoteDb() {
+    return migration.disableChangeReviewDb();
+  }
+
   public List<Change.Id> rename(
       List<Change.Id> changes,
       Project.NameKey oldProjectKey,
       Project.NameKey newProjectKey,
       ProgressMonitor pm)
-      throws OrmException {
+      throws OrmException, RenameRevertException {
     pm.beginTask("Updating changes in the database");
-    Connection conn = ((JdbcSchema) schemaFactory.open()).getConnection();
+    ReviewDb db = schemaFactory.open();
+    return (isNoteDb())
+        ? renameInNoteDb(changes, oldProjectKey, newProjectKey)
+        : renameInReviewDb(changes, oldProjectKey, newProjectKey, db);
+  }
+
+  private List<Change.Id> renameInReviewDb(
+      List<Change.Id> changes,
+      Project.NameKey oldProjectKey,
+      Project.NameKey newProjectKey,
+      ReviewDb db)
+      throws OrmException {
+    Connection conn = ((JdbcSchema) db).getConnection();
     try (Statement stmt = conn.createStatement()) {
       conn.setAutoCommit(false);
       try {
-        log.debug("Updating the changes in the DB related to project {}", oldProjectKey.get());
-        for (Change.Id cd : changes) {
-          stmt.addBatch(
-              "update changes set dest_project_name='"
-                  + newProjectKey.get()
-                  + "' where change_id ="
-                  + cd.id
-                  + ";");
+        try {
+          log.debug("Updating the changes in reviewDb related to project {}", oldProjectKey.get());
+          for (Change.Id cd : changes) {
+            stmt.addBatch(
+                "update changes set dest_project_name='"
+                    + newProjectKey.get()
+                    + "' where change_id ="
+                    + cd.id
+                    + ";");
+          }
+          stmt.executeBatch();
+          conn.commit();
+        } catch (SQLException e) {
+          throw new OrmException(e);
         }
-        stmt.executeBatch();
         updateWatchEntries(oldProjectKey, newProjectKey);
-        conn.commit();
         log.debug(
-            "Successfully updated the changes in the DB related to project {}",
+            "Successfully updated the changes in reviewDb related to project {}",
             oldProjectKey.get());
         return changes;
+      } catch (OrmException e) {
+        try {
+          log.error(
+              "Failed to update changes in reviewDb for project {}, exception caught: {}. Rolling back the operation.",
+              oldProjectKey.get(),
+              e.toString());
+          conn.rollback();
+        } catch (SQLException revertEx) {
+          log.error(
+              "Failed to rollback changes in reviewDb from project {} to project {}, exception caught: {}",
+              newProjectKey.get(),
+              oldProjectKey.get(),
+              revertEx.toString());
+          throw new RenameRevertException(revertEx, e);
+        }
+        try {
+          updateWatchEntries(newProjectKey, oldProjectKey);
+        } catch (OrmException revertEx) {
+          log.error(
+              "Failed to update watched changes in reviewDb from project {} to project {}, exception caught: {}",
+              newProjectKey.get(),
+              oldProjectKey.get(),
+              revertEx.toString());
+          throw new RenameRevertException(revertEx, e);
+        }
+        throw e;
       } finally {
         conn.setAutoCommit(true);
       }
     } catch (SQLException e) {
-      try {
-        log.error(
-            "Failed to update changes in the DB for the project {}, rolling back the operation.",
-            oldProjectKey.get());
-        conn.rollback();
-      } catch (SQLException ex) {
-        throw new OrmException(ex);
-      }
       throw new OrmException(e);
     }
   }
 
+  private List<Change.Id> renameInNoteDb(
+      List<Change.Id> changes, Project.NameKey oldProjectKey, Project.NameKey newProjectKey)
+      throws OrmException {
+    log.debug("Updating the changes in noteDb related to project {}", oldProjectKey.get());
+    try {
+      updateWatchEntries(oldProjectKey, newProjectKey);
+    } catch (OrmException e) {
+      log.error(
+          "Failed to update changes in noteDb for project {}, exception caught: {}. Rolling back the operation.",
+          oldProjectKey.get(),
+          e.toString());
+      try {
+        updateWatchEntries(newProjectKey, oldProjectKey);
+      } catch (OrmException revertEx) {
+        log.error(
+            "Failed to rollback changes in noteDb from project {} to project {}, exception caught: {}",
+            newProjectKey.get(),
+            oldProjectKey.get(),
+            revertEx.toString());
+        throw new RenameRevertException(revertEx, e);
+      }
+      throw e;
+    }
+
+    log.debug(
+        "Successfully updated the changes in noteDb related to project {}", oldProjectKey.get());
+    return changes;
+  }
+
   private void updateWatchEntries(Project.NameKey oldProjectKey, Project.NameKey newProjectKey)
       throws OrmException {
-    for (AccountState a : accountQueryProvider.get().byWatchedProject(newProjectKey)) {
+    for (AccountState a : accountQueryProvider.get().byWatchedProject(oldProjectKey)) {
       Account.Id accountId = a.getAccount().getId();
       for (ProjectWatchKey watchKey : a.getProjectWatches().keySet()) {
         if (oldProjectKey.equals(watchKey.project())) {
           try {
-            watchConfig
-                .get()
-                .upsertProjectWatches(accountId, watchConfig.get().getProjectWatches(accountId));
+            Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches =
+                watchConfig.get().getProjectWatches(accountId);
+
+            newProjectWatches.put(
+                ProjectWatchKey.create(newProjectKey, watchKey.filter()),
+                a.getProjectWatches().get(watchKey));
+
+            newProjectWatches.remove(watchKey);
+
+            watchConfig.get().deleteAllProjectWatches(accountId);
+            watchConfig.get().upsertProjectWatches(accountId, newProjectWatches);
           } catch (ConfigInvalidException e) {
             log.error(
                 "Updating watch entry for user {} in project {} failed. Watch config found invalid.",
                 a.getUserName(),
                 newProjectKey.get(),
                 e);
+            throw new OrmException(e);
           } catch (IOException e) {
             log.error(
                 "Updating watch entry for user {} in project {} failed.",
                 a.getUserName(),
                 newProjectKey.get(),
                 e);
+            throw new OrmException(e);
           }
         }
       }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index e151be1..da04a05 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -27,12 +27,6 @@
 
      If choosing to rename "All-Users", you cannot rename the project as this action is prohibited.
 
-* You cannot rename projects that are watched by users
-
-     If you rename a project that is actively watched by users, the watches are
-     not updated to the new project name, leading to the loss of the notifications
-     by all users that were watching the original project.
-
 * You should limit project renames to administrator users
 
      Because of all the above caveats, it is not recommended to allow any non-admin
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
index 879e660..c0ae8f0 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
@@ -16,13 +16,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import java.util.List;
+import javax.inject.Named;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
@@ -35,6 +42,11 @@
 
   private static final String PLUGIN_NAME = "rename-project";
   private static final String NEW_PROJECT_NAME = "newProject";
+  private static final String CACHE_NAME = "changeid_project";
+
+  @Inject
+  @Named(CACHE_NAME)
+  private Cache<Id, String> changeIdProjectCache;
 
   @Test
   @UseLocalDisk
@@ -100,4 +112,37 @@
     adminSshSession.exec(PLUGIN_NAME + " " + subProject.get() + " " + NEW_PROJECT_NAME);
     adminSshSession.assertFailure();
   }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameWatchedProject() throws Exception {
+    String oldProject = project.get();
+    watch(oldProject);
+
+    List<ProjectWatchInfo> watchedProjects = gApi.accounts().self().getWatchedProjects();
+    assertThat(watchedProjects.stream().allMatch(pwi -> pwi.project.equals(oldProject))).isTrue();
+
+    adminSshSession.exec(PLUGIN_NAME + " " + oldProject + " " + NEW_PROJECT_NAME);
+    adminSshSession.assertSuccess();
+
+    watchedProjects = gApi.accounts().self().getWatchedProjects();
+    assertThat(watchedProjects.stream().allMatch(pwi -> pwi.project.equals(NEW_PROJECT_NAME)))
+        .isTrue();
+    assertThat(watchedProjects.size()).isEqualTo(1);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameClearedOldChangeIdLinkInCaches() throws Exception {
+    Result result = createChange();
+    String oldProject = project.get();
+
+    Id changeID = result.getChange().getId();
+    changeIdProjectCache.put(changeID, oldProject);
+
+    adminSshSession.exec(PLUGIN_NAME + " " + oldProject + " " + NEW_PROJECT_NAME);
+    adminSshSession.assertSuccess();
+
+    assertThat(changeIdProjectCache.getIfPresent(changeID)).isNull();
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java
new file mode 100644
index 0000000..64bc221
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java
@@ -0,0 +1,157 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.renameproject;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
+import com.googlesource.gerrit.plugins.renameproject.RenameProject.Step;
+import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+@TestPlugin(
+    name = "rename-project",
+    sysModule = "com.googlesource.gerrit.plugins.renameproject.Module",
+    sshModule = "com.googlesource.gerrit.plugins.renameproject.SshModule")
+public class RevertRenameProjectTest extends LightweightPluginDaemonTest {
+  private static final String NEW_PROJECT_NAME = "newProject";
+
+  private RenameProject renameProject;
+  private RevertRenameProject revertRenameProject;
+  private Project.NameKey oldProjectKey;
+  private Project.NameKey newProjectKey;
+  private ProgressMonitor pm;
+  private ProjectResource oldRsrc;
+
+  @Before
+  public void init() {
+    renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
+    revertRenameProject = plugin.getSysInjector().getInstance(RevertRenameProject.class);
+
+    oldProjectKey = project;
+    newProjectKey = new Project.NameKey(NEW_PROJECT_NAME);
+
+    pm = Mockito.mock(ProgressMonitor.class);
+
+    ProjectControl control = Mockito.mock(ProjectControl.class);
+    when(control.getProject()).thenReturn(new Project(oldProjectKey));
+    oldRsrc = new ProjectResource(control);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRevertFromFsHandler() throws Exception {
+    Result result = createChange();
+    List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
+
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
+    assertRenamed(result);
+
+    revertRenameProject.performRevert(
+        renameProject.getStepsPerformed(), changeIds, oldProjectKey, newProjectKey, pm);
+    assertReverted();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRevertFromCacheHandler() throws Exception {
+    Result result = createChange();
+    List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
+
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
+    renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
+    assertRenamed(result);
+
+    revertRenameProject.performRevert(
+        renameProject.getStepsPerformed(), changeIds, oldProjectKey, newProjectKey, pm);
+    assertReverted();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRevertFromDbHandler() throws Exception {
+    Result result = createChange();
+    List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
+
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
+    renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
+    renameProject.dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
+    assertRenamed(result);
+
+    revertRenameProject.performRevert(
+        renameProject.getStepsPerformed(), changeIds, oldProjectKey, newProjectKey, pm);
+    assertReverted();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRevertFromIndexHandler() throws Exception {
+    Result result = createChange();
+    List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
+
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
+    renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
+    renameProject.dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
+    renameProject.indexRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
+    assertRenamed(result);
+
+    revertRenameProject.performRevert(
+        renameProject.getStepsPerformed(), changeIds, oldProjectKey, newProjectKey, pm);
+    assertReverted();
+  }
+
+  private void assertReverted() throws Exception {
+    ProjectState oldProjectState = projectCache.get(oldProjectKey);
+    assertThat(oldProjectState).isNotNull();
+
+    ProjectState newProjectState = projectCache.get(newProjectKey);
+    assertThat(newProjectState).isNull();
+
+    assertThat(queryProvider.get().byProject(oldProjectKey)).isNotEmpty();
+    assertThat(queryProvider.get().byProject(newProjectKey)).isEmpty();
+  }
+
+  private void assertRenamed(Result result) throws Exception {
+    ProjectState oldProjectState = projectCache.get(oldProjectKey);
+    assertThat(oldProjectState).isNull();
+
+    ProjectState newProjectState = projectCache.get(newProjectKey);
+    assertThat(newProjectState).isNotNull();
+
+    if (renameProject.getStepsPerformed().contains(Step.DATABASE)) {
+      ChangeApi changeApi = gApi.changes().id(NEW_PROJECT_NAME, result.getChange().getId().get());
+      ChangeInfo changeInfo = changeApi.info();
+      assertThat(changeInfo.changeId).isEqualTo(result.getChangeId());
+    }
+
+    if (renameProject.getStepsPerformed().contains(Step.INDEX)) {
+      assertThat(queryProvider.get().byProject(oldProjectKey)).isEmpty();
+    }
+  }
+}
\ No newline at end of file
diff --git a/tools/BUILD b/tools/BUILD
new file mode 100644
index 0000000..c5ed0b7
--- /dev/null
+++ b/tools/BUILD
@@ -0,0 +1 @@
+# Empty file required by Bazel
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
index 2b895ff..3003cc2 100755
--- a/tools/eclipse/project.sh
+++ b/tools/eclipse/project.sh
@@ -13,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n rename-project -r .
+path=$(bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project \
+    --output location | sed s/BUILD:.*//)
+${path}project.py -n rename-project -r . "$@"