Merge "RebaseUtil: Support base revisions which are part of the dest branch"
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 2a215c2..93fcbc6 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -51,6 +51,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -344,7 +345,7 @@
       }
     }
 
-    // Try parsing as SHA-1.
+    // Try parsing as SHA-1 based on the change-index.
     Base ret = null;
     for (ChangeData cd : queryProvider.get().byProjectCommit(rsrc.getProject(), base)) {
       for (PatchSet ps : cd.patchSets()) {
@@ -369,8 +370,8 @@
   /**
    * Parse or find the commit onto which a patch set should be rebased.
    *
-   * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, finds the latest patch set
-   * of the change corresponding to this commit's parent, or the destination branch tip in the case
+   * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, find the latest patch set of
+   * the change corresponding to this commit's parent, or the destination branch tip in the case
    * where the parent's change is merged.
    *
    * @param git the repository.
@@ -411,11 +412,16 @@
       throw new UnprocessableEntityException(
           String.format("Base change not found: %s", inputBase), e);
     }
-    if (base == null) {
-      throw new ResourceConflictException(
-          "base revision is missing from the destination branch: " + inputBase);
+    if (base != null) {
+      return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
     }
-    return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
+    if (isBaseRevisionInDestBranch(rw, inputBase, git, change.getDest())) {
+      // The requested base is a valid commit in the dest branch, which is not associated with any
+      // Gerrit change.
+      return ObjectId.fromString(inputBase);
+    }
+    throw new ResourceConflictException(
+        "base revision is missing from the destination branch: " + inputBase);
   }
 
   private ObjectId getDestRefTip(Repository git, BranchNameKey destRefKey)
@@ -531,6 +537,18 @@
     return baseId;
   }
 
+  private boolean isBaseRevisionInDestBranch(
+      RevWalk rw, String expectedBaseSha1, Repository git, BranchNameKey destRefKey)
+      throws IOException, ResourceConflictException {
+    RevCommit potentialBaseCommit;
+    try {
+      potentialBaseCommit = rw.parseCommit(ObjectId.fromString(expectedBaseSha1));
+    } catch (InvalidObjectIdException | IOException e) {
+      return false;
+    }
+    return rw.isMergedInto(potentialBaseCommit, rw.parseCommit(getDestRefTip(git, destRefKey)));
+  }
+
   public RebaseChangeOp getRebaseOp(
       RevWalk rw,
       RevisionResource revRsrc,
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index af57417..7f21eb6 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -840,6 +840,51 @@
     }
 
     @Test
+    public void rebaseChangeWithValidBaseCommit() throws Exception {
+      RevCommit desiredBase =
+          createNewCommitWithoutChangeId(/*branch=*/ "refs/heads/master", "file", "content");
+      PushOneCommit.Result child = createChange();
+      RebaseInput ri = new RebaseInput();
+
+      // rebase child onto desiredBase (referenced by commit)
+      ri.base = desiredBase.getName();
+      rebaseCallWithInput.call(child.getChangeId(), ri);
+
+      PatchSet ps2 = child.getPatchSet();
+      assertThat(ps2.id().get()).isEqualTo(2);
+      RevisionInfo childInfo =
+          get(child.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+      assertThat(childInfo.commit.parents.get(0).commit).isEqualTo(desiredBase.name());
+    }
+
+    @Test
+    public void cannotRebaseChangeWithInvalidBaseCommit() throws Exception {
+      // Create another branch and push the desired parent commit to it.
+      String branchName = "foo";
+      BranchInput branchInput = new BranchInput();
+      branchInput.ref = branchName;
+      branchInput.revision = projectOperations.project(project).getHead("master").name();
+      gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+      RevCommit desiredBase =
+          createNewCommitWithoutChangeId(/*branch=*/ "refs/heads/foo", "file", "content");
+      // Create the child commit on "master".
+      PushOneCommit.Result child = createChange();
+      RebaseInput ri = new RebaseInput();
+
+      // Try to rebase child onto desiredBase (referenced by commit)
+      ri.base = desiredBase.getName();
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> rebaseCallWithInput.call(child.getChangeId(), ri));
+
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains(
+              String.format("base revision is missing from the destination branch: %s", ri.base));
+    }
+
+    @Test
     public void rebaseUpToDateChange() throws Exception {
       PushOneCommit.Result r = createChange();
       verifyChangeIsUpToDate(r);