Fix rebasing chain when a checks refs exists for one of the changes
PatchSetUtil#getCurrentRevCommitIncludingPending had the wrong
assumption that a change only has:
* a change meta ref (ref/changes/<sharded-change-id>/meta)
* patch set refs (ref/changes/<sharded-change-id>/<patch-set-num>)
from which it's possible to parse a patch set number
If there are other change refs, e.g. a checks ref
(ref/changes/<sharded-change-id>/checks),
PatchSetUtil#getCurrentRevCommitIncludingPending failed with a
NullPointerException. That's because for all non-meta change refs it
tries to parse a patch set number from the ref, but it didn't expect
that the patch set number can be null if it's not a patch set ref (see
PatchSet.Id#fromRef).
Fix this by filtering out null patch set numbers.
Bug: Google b/276709753
Release-Notes: Fixed rebasing chain when a checks refs exists for one of the changes
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I47287d898112e9e2680c8738040ef1f05e173b8e
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index b34a56f..68d2314 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -42,6 +42,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
@@ -193,6 +194,7 @@
Optional<PatchSet.Id> latestPendingPatchSet =
refUpdates.keySet().stream()
.map(r -> PatchSet.Id.fromRef(changeId.toRefPrefix() + r))
+ .filter(Objects::nonNull)
.max(PatchSet.Id::compareTo);
if (latestPendingPatchSet.isPresent()) {
return ctx.getRevWalk().parseCommit(refUpdates.get(latestPendingPatchSet.get().getId()));
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index 4e95032..fd37fb0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -44,6 +44,7 @@
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
@@ -74,7 +75,9 @@
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
+import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -517,6 +520,36 @@
}
}
+ @Test
+ public void rebaseChangeWhenChecksRefExists() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Create checks ref
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ testRepo.update(
+ RefNames.changeRefPrefix(r2.getChange().getId()) + "checks",
+ testRepo.commit().message("Empty commit"));
+ }
+
+ // Rebase the second change
+ rebaseCall.call(r2.getChangeId());
+
+ verifyRebaseForChange(
+ r2.getChange().getId(),
+ r.getCommit().name(),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+ }
+
protected void verifyRebaseForChange(
Change.Id changeId, Change.Id baseChangeId, boolean shouldHaveApproval)
throws RestApiException {
@@ -1233,6 +1266,39 @@
}
@Test
+ public void rebaseChainWhenChecksRefExists() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+
+ // Create checks ref
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ testRepo.update(
+ RefNames.changeRefPrefix(r2.getChange().getId()) + "checks",
+ testRepo.commit().message("Empty commit"));
+ }
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+ gApi.changes().id(r3.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the chain through r3.
+ verifyRebaseChainResponse(gApi.changes().id(r3.getChangeId()).rebaseChain(), false, r2, r3);
+ }
+
+ @Test
public void testCountRebasesMetric() throws Exception {
// Create changes with the following hierarchy:
// * HEAD