Abandon any open changes if a project has been removed

If the Git repository has been deleted and a change has been
submitted in that repository MergeOp will contiuously try to
submit changes in that project, failing on every attempt when
openRepository() fails.

Catch this failure and abandon all changes in the project, as the
Git data no longer exists.

Change-Id: I83087211b281c1e478c0e83b48b8f7d158a93c13
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index e1d8627..45e4371 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -211,7 +212,7 @@
     }
   }
 
-  public void merge() throws MergeException, NoSuchProjectException {
+  public void merge() throws MergeException {
     setDestProject();
     try {
       openSchema();
@@ -270,6 +271,11 @@
               message(commit.change, capable.getMessage()), false);
         }
       }
+    } catch (NoSuchProjectException noProject) {
+      log.warn(String.format(
+          "Project %s no longer exists, abandoning open changes",
+          destBranch.getParentKey().get()));
+      abandonAllOpenChanges();
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
@@ -343,13 +349,12 @@
         canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
   }
 
-  private void openRepository() throws MergeException {
+  private void openRepository() throws MergeException, NoSuchProjectException {
     final Project.NameKey name = destBranch.getParentKey();
     try {
       repo = repoManager.openRepository(name);
-    } catch (RepositoryNotFoundException notGit) {
-      final String m = "Repository \"" + name.get() + "\" unknown.";
-      throw new MergeException(m, notGit);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
       final String m = "Error opening repository \"" + name.get() + '"';
       throw new MergeException(m, err);
@@ -1041,4 +1046,53 @@
       }
     }
   }
+
+  private void abandonAllOpenChanges() {
+    try {
+      openSchema();
+      for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
+        abandonOneChange(c);
+      }
+      db.close();
+      db = null;
+    } catch (OrmException e) {
+      log.warn(String.format(
+          "Cannot abandon changes for deleted project %s",
+          destBranch.getParentKey().get()), e);
+    }
+  }
+
+  private void abandonOneChange(Change change) throws OrmException {
+    db.changes().beginTransaction(change.getId());
+    try {
+      change = db.changes().atomicUpdate(
+        change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus().isOpen()) {
+              change.setStatus(Change.Status.ABANDONED);
+              return change;
+            }
+            return null;
+          }
+        });
+      if (change != null) {
+        ChangeMessage msg = new ChangeMessage(
+            new ChangeMessage.Key(
+                change.getId(),
+                ChangeUtil.messageUUID(db)),
+            null,
+            change.getLastUpdatedOn(),
+            change.currentPatchSetId());
+        msg.setMessage("Project was deleted.");
+        db.changeMessages().insert(Collections.singleton(msg));
+        new ApprovalsUtil(db).syncChangeStatus(change);
+        db.commit();
+        indexer.index(change);
+      }
+    } finally {
+      db.rollback();
+    }
+  }
 }