Fix NPE when deleting current patch set and previous patch set doesn't exist

When the current patch set was deleted we used patch set ID - 1 as ID
for the previous patch set but this patch set may not exist (e.g. it was
a draft that was deleted).

Change-Id: Ief0776d131d805ed403fb45bb6112d953dc5564d
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 244efbf..11ff612 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -188,6 +188,26 @@
   }
 
   @Test
+  public void deleteCurrentDraftPatchSetWhenPreviousPatchSetDoesNotExist() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    String changeId = push.to("refs/for/master").getChangeId();
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "foo", changeId)
+        .to("refs/drafts/master");
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "bar", changeId)
+        .to("refs/drafts/master");
+
+    deletePatchSet(changeId, 2);
+    deletePatchSet(changeId, 3);
+
+    ChangeData cd = getChange(changeId);
+    assertThat(cd.patchSets()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cd.patchSets()).getId().get()).isEqualTo(1);
+    assertThat(cd.currentPatchSet().getId().get()).isEqualTo(1);
+  }
+
+  @Test
   public void deleteDraftPatchSetAndPushNewDraftPatchSet() throws Exception {
     String ref = "refs/drafts/master";
 
@@ -263,6 +283,10 @@
   }
 
   private void deletePatchSet(String changeId, PatchSet ps) throws Exception {
-    gApi.changes().id(changeId).revision(ps.getId().get()).delete();
+    deletePatchSet(changeId, ps.getId().get());
+  }
+
+  private void deletePatchSet(String changeId, int ps) throws Exception {
+    gApi.changes().id(changeId).revision(ps).delete();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 222230b..d452489 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -45,6 +47,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Map;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -106,7 +109,8 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws RestApiException, OrmException, IOException, NoSuchChangeException {
-      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      Map<PatchSet.Id, PatchSet> patchSets = psUtil.byChangeAsMap(db.get(), ctx.getNotes());
+      patchSet = patchSets.get(psId);
       if (patchSet == null) {
         return false; // Nothing to do.
       }
@@ -120,9 +124,9 @@
         throw new AuthException("Not permitted to delete this draft patch set");
       }
 
-      patchSetsBeforeDeletion = psUtil.byChange(ctx.getDb(), ctx.getNotes());
+      patchSetsBeforeDeletion = patchSets.values();
       deleteDraftPatchSet(patchSet, ctx);
-      deleteOrUpdateDraftChange(ctx);
+      deleteOrUpdateDraftChange(ctx, patchSets);
       return true;
     }
 
@@ -152,7 +156,7 @@
       db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
     }
 
-    private void deleteOrUpdateDraftChange(ChangeContext ctx)
+    private void deleteOrUpdateDraftChange(ChangeContext ctx, Map<PatchSet.Id, PatchSet> patchSets)
         throws OrmException, RestApiException, IOException, NoSuchChangeException {
       Change c = ctx.getChange();
       if (deletedOnlyPatchSet()) {
@@ -161,7 +165,7 @@
         return;
       }
       if (c.currentPatchSetId().equals(psId)) {
-        c.setCurrentPatchSet(previousPatchSetInfo(ctx));
+        c.setCurrentPatchSet(previousPatchSetInfo(ctx, patchSets));
       }
     }
 
@@ -170,12 +174,22 @@
           && patchSetsBeforeDeletion.iterator().next().getId().equals(psId);
     }
 
-    private PatchSetInfo previousPatchSetInfo(ChangeContext ctx) throws OrmException {
+    private PatchSetInfo previousPatchSetInfo(
+        ChangeContext ctx, Map<PatchSet.Id, PatchSet> patchSets) throws OrmException {
+      PatchSet.Id prevPsId = null;
+      for (PatchSet.Id id : patchSets.keySet()) {
+        if (id.get() < psId.get() && (prevPsId == null || id.get() > prevPsId.get())) {
+          prevPsId = id;
+        }
+      }
+
       try {
         // TODO(dborowitz): Get this in a way that doesn't involve re-opening
         // the repo after the updateRepo phase.
         return patchSetInfoFactory.get(
-            ctx.getDb(), ctx.getNotes(), new PatchSet.Id(psId.getParentKey(), psId.get() - 1));
+            ctx.getDb(),
+            ctx.getNotes(),
+            new PatchSet.Id(psId.getParentKey(), checkNotNull(prevPsId).get()));
       } catch (PatchSetInfoNotAvailableException e) {
         throw new OrmException(e);
       }