diff options
author | Edwin Kempin <ekempin@google.com> | 2022-04-01 07:59:52 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2022-04-01 07:59:52 +0000 |
commit | 0bfee827067d95806dbd14142ea66c278fa9e47c (patch) | |
tree | 71852309df86c596b382156f9f535cd01f17d41b | |
parent | 306b840ac4680d71f629bbf86f7e737ed31eec66 (diff) | |
parent | 46b036279c33827f223fcfa63202b006c4f85a6b (diff) |
Merge "Update existing change on cherry-pick with CommitApi" into stable-3.3
6 files changed, 320 insertions, 57 deletions
diff --git a/java/com/google/gerrit/server/change/ResetCherryPickOp.java b/java/com/google/gerrit/server/change/ResetCherryPickOp.java new file mode 100644 index 0000000000..d1177d498c --- /dev/null +++ b/java/com/google/gerrit/server/change/ResetCherryPickOp.java @@ -0,0 +1,37 @@ +// Copyright (C) 2020 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.google.gerrit.server.change; + +import com.google.gerrit.entities.Change; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; + +/** Reset cherryPickOf to an empty value. */ +public class ResetCherryPickOp implements BatchUpdateOp { + + @Override + public boolean updateChange(ChangeContext ctx) throws RestApiException { + Change change = ctx.getChange(); + if (change.getCherryPickOf() == null) { + return false; + } + + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); + update.resetCherryPickOf(); + return true; + } +} diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java index 78ba243777..6f9f7e8a30 100644 --- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java +++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java @@ -55,6 +55,7 @@ import com.google.common.collect.Table; import com.google.common.collect.Tables; import com.google.common.flogger.FluentLogger; import com.google.common.primitives.Ints; +import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Address; import com.google.gerrit.entities.AttentionSetUpdate; @@ -153,7 +154,10 @@ class ChangeNotesParser { private ReviewerByEmailSet pendingReviewersByEmail; private Change.Id revertOf; private int updateCount; - private PatchSet.Id cherryPickOf; + // Null indicates that the field was not parsed (yet). + // We only set the value once, based on the latest update (the actual value or Optional.empty() if + // the latest record unsets the field). + private Optional<PatchSet.Id> cherryPickOf; ChangeNotesParser( Change.Id changeId, @@ -254,7 +258,7 @@ class ChangeNotesParser { firstNonNull(workInProgress, false), firstNonNull(hasReviewStarted, true), revertOf, - cherryPickOf, + cherryPickOf != null ? cherryPickOf.orElse(null) : null, updateCount); } @@ -999,15 +1003,31 @@ class ChangeNotesParser { return Change.id(revertOf); } - private PatchSet.Id parseCherryPickOf(ChangeNotesCommit commit) throws ConfigInvalidException { - String cherryPickOf = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF); - if (cherryPickOf == null) { + /** + * Parses {@link ChangeNoteUtil#FOOTER_CHERRY_PICK_OF} of the commit. + * + * @param commit the commit to parse. + * @return {@link Optional} value of the parsed footer or {@code null} if the footer is missing in + * this commit. + * @throws ConfigInvalidException if the footer value could not be parsed as a valid {@link + * PatchSet.Id}. + */ + @Nullable + private Optional<PatchSet.Id> parseCherryPickOf(ChangeNotesCommit commit) + throws ConfigInvalidException { + String footer = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF); + if (footer == null) { + // The footer is missing, nothing to parse. return null; - } - try { - return PatchSet.Id.parse(cherryPickOf); - } catch (IllegalArgumentException e) { - throw new ConfigInvalidException("\"" + cherryPickOf + "\" is not a valid patchset", e); + } else if (footer.equals("")) { + // Empty footer value, cherryPickOf was unset at this commit. + return Optional.empty(); + } else { + try { + return Optional.of(PatchSet.Id.parse(footer)); + } catch (IllegalArgumentException e) { + throw new ConfigInvalidException("\"" + footer + "\" is not a valid patchset", e); + } } } diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java index 3312d6cfc9..52c551ef0e 100644 --- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java +++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java @@ -152,7 +152,9 @@ public class ChangeUpdate extends AbstractChangeUpdate { private Boolean isPrivate; private Boolean workInProgress; private Integer revertOf; - private String cherryPickOf; + // If null, the update does not modify the field. Otherwise, it updates the field with the + // new value or resets if cherryPickOf == Optional.empty(). + private Optional<String> cherryPickOf; private ChangeDraftUpdate draftUpdate; private RobotCommentUpdate robotCommentUpdate; @@ -473,7 +475,12 @@ public class ChangeUpdate extends AbstractChangeUpdate { } public void setCherryPickOf(String cherryPickOf) { - this.cherryPickOf = cherryPickOf; + checkArgument(cherryPickOf != null, "use resetCherryPickOf"); + this.cherryPickOf = Optional.of(cherryPickOf); + } + + public void resetCherryPickOf() { + this.cherryPickOf = Optional.empty(); } /** @return the tree id for the updated tree */ @@ -753,7 +760,12 @@ public class ChangeUpdate extends AbstractChangeUpdate { } if (cherryPickOf != null) { - addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf); + if (cherryPickOf.isPresent()) { + addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf.get()); + } else { + // Update cherryPickOf with an empty value. + addFooter(msg, FOOTER_CHERRY_PICK_OF).append('\n'); + } } if (plannedAttentionSetUpdates != null) { diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java index fdac552edf..6ee3284c5d 100644 --- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java +++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java @@ -42,6 +42,7 @@ import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.change.PatchSetInserter; +import com.google.gerrit.server.change.ResetCherryPickOp; import com.google.gerrit.server.change.SetCherryPickOp; import com.google.gerrit.server.config.UrlFormatter; import com.google.gerrit.server.git.CodeReviewCommit; @@ -371,7 +372,7 @@ public class CherryPickChange { git, destChanges.get(0).notes(), cherryPickCommit, - sourceChange.currentPatchSetId(), + sourceChange, newTopic, workInProgress); } else { @@ -456,7 +457,7 @@ public class CherryPickChange { Repository git, ChangeNotes destNotes, CodeReviewCommit cherryPickCommit, - PatchSet.Id sourcePatchSetId, + @Nullable Change sourceChange, String topic, @Nullable Boolean workInProgress) throws IOException { @@ -469,7 +470,11 @@ public class CherryPickChange { inserter.setWorkInProgress(workInProgress); } bu.addOp(destChange.getId(), inserter); - if (destChange.getCherryPickOf() == null + PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId(); + // If sourceChange is not provided, reset cherryPickOf to avoid stale value. + if (sourcePatchSetId == null) { + bu.addOp(destChange.getId(), new ResetCherryPickOp()); + } else if (destChange.getCherryPickOf() == null || !destChange.getCherryPickOf().equals(sourcePatchSetId)) { SetCherryPickOp cherryPickOfUpdater = setCherryPickOfFactory.create(sourcePatchSetId); bu.addOp(destChange.getId(), cherryPickOfUpdater); diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java index 23563275b8..3b5ac78b26 100644 --- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java +++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java @@ -24,6 +24,7 @@ import static org.eclipse.jgit.lib.Constants.R_TAGS; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; @@ -147,77 +148,237 @@ public class CommitIT extends AbstractDaemonTest { } @Test - public void cherryPickWithoutMessage() throws Exception { - String branch = "foo"; + public void cherryPickWithoutMessageSameBranch() throws Exception { + String destBranch = "master"; // Create change to cherry-pick - RevCommit revCommit = createChange().getCommit(); - - // Create target branch to cherry-pick to. - gApi.projects().name(project.get()).branch(branch).create(new BranchInput()); + PushOneCommit.Result r = createChange(); + ChangeInfo changeToCherryPick = info(r.getChangeId()); + RevCommit commitToCherryPick = r.getCommit(); // Cherry-pick without message. CherryPickInput input = new CherryPickInput(); - input.destination = branch; - String changeId = - gApi.projects().name(project.get()).commit(revCommit.name()).cherryPick(input).get().id; + input.destination = destBranch; + ChangeInfo cherryPickResult = + gApi.projects() + .name(project.get()) + .commit(commitToCherryPick.name()) + .cherryPick(input) + .get(); + + // Expect that the Change-Id of the cherry-picked commit was used for the cherry-pick change. + // New patch-set to existing change was uploaded. + assertThat(cherryPickResult._number).isEqualTo(changeToCherryPick._number); + assertThat(cherryPickResult.revisions).hasSize(2); + assertThat(cherryPickResult.changeId).isEqualTo(changeToCherryPick.changeId); + assertThat(cherryPickResult.messages).hasSize(2); + + // Cherry-pick of is not set, because the source change was not provided. + assertThat(cherryPickResult.cherryPickOfChange).isNull(); + assertThat(cherryPickResult.cherryPickOfPatchSet).isNull(); + // Expect that the message of the cherry-picked commit was used for the cherry-pick change. + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); + assertThat(revInfo).isNotNull(); + assertThat(revInfo.commit.message).isEqualTo(commitToCherryPick.getFullMessage()); + } + @Test + public void cherryPickWithoutMessageOtherBranch() throws Exception { + String destBranch = "foo"; + createBranch(BranchNameKey.create(project, destBranch)); + + // Create change to cherry-pick + PushOneCommit.Result r = createChange(); + ChangeInfo changeToCherryPick = info(r.getChangeId()); + RevCommit commitToCherryPick = r.getCommit(); + + // Cherry-pick without message. + CherryPickInput input = new CherryPickInput(); + input.destination = destBranch; + ChangeInfo cherryPickResult = + gApi.projects() + .name(project.get()) + .commit(commitToCherryPick.name()) + .cherryPick(input) + .get(); + + // Expect that the Change-Id of the cherry-picked commit was used for the cherry-pick change. + // New change in destination branch was created. + assertThat(cherryPickResult._number).isGreaterThan(changeToCherryPick._number); + assertThat(cherryPickResult.revisions).hasSize(1); + assertThat(cherryPickResult.changeId).isEqualTo(changeToCherryPick.changeId); + assertThat(cherryPickResult.messages).hasSize(1); + + // Cherry-pick of is not set, because the source change was not provided. + assertThat(cherryPickResult.cherryPickOfChange).isNull(); + assertThat(cherryPickResult.cherryPickOfPatchSet).isNull(); // Expect that the message of the cherry-picked commit was used for the cherry-pick change. - ChangeInfo changeInfo = gApi.changes().id(changeId).get(); - RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); assertThat(revInfo).isNotNull(); - assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage()); + assertThat(revInfo.commit.message).isEqualTo(commitToCherryPick.getFullMessage()); } @Test - public void cherryPickCommitWithoutChangeId() throws Exception { + public void cherryPickCommitWithoutChangeIdCreateNewChange() throws Exception { + String destBranch = "foo"; + createBranch(BranchNameKey.create(project, destBranch)); + CherryPickInput input = new CherryPickInput(); - input.destination = "foo"; + input.destination = destBranch; input.message = "it goes to foo branch"; - gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput()); - - RevCommit revCommit = createNewCommitWithoutChangeId("refs/heads/master", "a.txt", "content"); - ChangeInfo changeInfo = - gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get(); - assertThat(changeInfo.messages).hasSize(1); - Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator(); + RevCommit commitToCherryPick = + createNewCommitWithoutChangeId("refs/heads/master", "a.txt", "content"); + ChangeInfo cherryPickResult = + gApi.projects() + .name(project.get()) + .commit(commitToCherryPick.getName()) + .cherryPick(input) + .get(); + + assertThat(cherryPickResult.messages).hasSize(1); + Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator(); String expectedMessage = - String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName()); + String.format("Patch Set 1: Cherry Picked from commit %s.", commitToCherryPick.getName()); assertThat(messageIterator.next().message).isEqualTo(expectedMessage); - RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); assertThat(revInfo).isNotNull(); CommitInfo commitInfo = revInfo.commit; assertThat(commitInfo.message) - .isEqualTo(input.message + "\n\nChange-Id: " + changeInfo.changeId + "\n"); + .isEqualTo(input.message + "\n\nChange-Id: " + cherryPickResult.changeId + "\n"); } @Test - public void cherryPickCommitWithChangeId() throws Exception { - CherryPickInput input = new CherryPickInput(); - input.destination = "foo"; - - RevCommit revCommit = createChange().getCommit(); - List<String> footers = revCommit.getFooterLines("Change-Id"); + public void cherryPickCommitWithChangeIdCreateNewChange() throws Exception { + String destBranch = "foo"; + createBranch(BranchNameKey.create(project, destBranch)); + + PushOneCommit.Result r = createChange(); + ChangeInfo changeToCherryPick = info(r.getChangeId()); + RevCommit commitToCherryPick = r.getCommit(); + List<String> footers = commitToCherryPick.getFooterLines("Change-Id"); assertThat(footers).hasSize(1); String changeId = footers.get(0); - input.message = "it goes to foo branch\n\nChange-Id: " + changeId; - gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput()); - - ChangeInfo changeInfo = - gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get(); - - assertThat(changeInfo.messages).hasSize(1); - Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator(); + CherryPickInput input = new CherryPickInput(); + input.destination = destBranch; + input.message = + String.format( + "it goes to foo branch\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n\nChange-Id: %s\n", + changeId); + + ChangeInfo cherryPickResult = + gApi.projects() + .name(project.get()) + .commit(commitToCherryPick.getName()) + .cherryPick(input) + .get(); + + // No change was found in destination branch with the provided Change-Id. + assertThat(cherryPickResult._number).isGreaterThan(changeToCherryPick._number); + assertThat(cherryPickResult.changeId).isEqualTo(changeId); + assertThat(cherryPickResult.revisions).hasSize(1); + assertThat(cherryPickResult.messages).hasSize(1); + Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator(); String expectedMessage = - String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName()); + String.format("Patch Set 1: Cherry Picked from commit %s.", commitToCherryPick.getName()); assertThat(messageIterator.next().message).isEqualTo(expectedMessage); - RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + // Cherry-pick of is not set, because the source change was not provided. + assertThat(cherryPickResult.cherryPickOfChange).isNull(); + assertThat(cherryPickResult.cherryPickOfPatchSet).isNull(); + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); + assertThat(revInfo).isNotNull(); + assertThat(revInfo.commit.message).isEqualTo(input.message); + } + + @Test + public void cherryPickCommitToExistingChange() throws Exception { + String destBranch = "foo"; + createBranch(BranchNameKey.create(project, destBranch)); + + PushOneCommit.Result r = createChange("refs/for/" + destBranch); + ChangeInfo existingDestChange = info(r.getChangeId()); + + String commitToCherryPick = createChange().getCommit().getName(); + + CherryPickInput input = new CherryPickInput(); + input.destination = destBranch; + input.message = + String.format( + "it goes to foo branch\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n\nChange-Id: %s\n", + existingDestChange.changeId); + input.allowConflicts = true; + input.allowEmpty = true; + + ChangeInfo cherryPickResult = + gApi.projects().name(project.get()).commit(commitToCherryPick).cherryPick(input).get(); + + // New patch-set to existing change was uploaded. + assertThat(cherryPickResult._number).isEqualTo(existingDestChange._number); + assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId); + assertThat(cherryPickResult.messages).hasSize(2); + assertThat(cherryPickResult.revisions).hasSize(2); + Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator(); + + assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 1."); + assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 2."); + // Cherry-pick of is not set, because the source change was not provided. + assertThat(cherryPickResult.cherryPickOfChange).isNull(); + assertThat(cherryPickResult.cherryPickOfPatchSet).isNull(); + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); + assertThat(revInfo).isNotNull(); + assertThat(revInfo.commit.message).isEqualTo(input.message); + } + + @Test + public void cherryPickCommitToExistingCherryPickedChange() throws Exception { + String destBranch = "foo"; + createBranch(BranchNameKey.create(project, destBranch)); + + PushOneCommit.Result r = createChange("refs/for/" + destBranch); + ChangeInfo existingDestChange = info(r.getChangeId()); + + r = createChange(); + ChangeInfo changeToCherryPick = info(r.getChangeId()); + RevCommit commitToCherryPick = r.getCommit(); + + CherryPickInput input = new CherryPickInput(); + input.destination = destBranch; + input.message = + String.format("it goes to foo branch\n\nChange-Id: %s\n", existingDestChange.changeId); + input.allowConflicts = true; + input.allowEmpty = true; + // Use RevisionAPI to submit initial cherryPick. + ChangeInfo cherryPickResult = + gApi.changes().id(changeToCherryPick.changeId).current().cherryPick(input).get(); + assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId); + // Cherry-pick was set. + assertThat(cherryPickResult.cherryPickOfChange).isEqualTo(changeToCherryPick._number); + assertThat(cherryPickResult.cherryPickOfPatchSet).isEqualTo(1); + RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision); assertThat(revInfo).isNotNull(); - assertThat(revInfo.commit.message).isEqualTo(input.message + "\n"); + assertThat(revInfo.commit.message).isEqualTo(input.message); + // Use CommitApi to update the cherryPick change. + cherryPickResult = + gApi.projects() + .name(project.get()) + .commit(commitToCherryPick.getName()) + .cherryPick(input) + .get(); + + assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId); + assertThat(cherryPickResult.messages).hasSize(3); + Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator(); + + assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 1."); + assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 2."); + assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 3."); + // Cherry-pick was reset to empty value. + assertThat(cherryPickResult._number).isEqualTo(existingDestChange._number); + assertThat(cherryPickResult.cherryPickOfChange).isNull(); + assertThat(cherryPickResult.cherryPickOfPatchSet).isNull(); } @Test diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java index 938fffc443..6b9859f4e3 100644 --- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java +++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java @@ -3146,6 +3146,34 @@ public class ChangeNotesTest extends AbstractChangeNotesTest { } @Test + public void resetCherryPickOf() throws Exception { + Change destinationChange = newChange(); + Change cherryPickChange = newChange(); + assertThat(newNotes(destinationChange).getChange().getCherryPickOf()).isNull(); + + ChangeUpdate update = newUpdate(destinationChange, changeOwner); + update.setCherryPickOf( + cherryPickChange.currentPatchSetId().getCommaSeparatedChangeAndPatchSetId()); + update.commit(); + assertThat(newNotes(destinationChange).getChange().getCherryPickOf()) + .isEqualTo(cherryPickChange.currentPatchSetId()); + + update = newUpdate(destinationChange, changeOwner); + update.resetCherryPickOf(); + update.commit(); + assertThat(newNotes(destinationChange).getChange().getCherryPickOf()).isNull(); + + // Can set again after reset. + cherryPickChange = newChange(); + update = newUpdate(destinationChange, changeOwner); + update.setCherryPickOf( + cherryPickChange.currentPatchSetId().getCommaSeparatedChangeAndPatchSetId()); + update.commit(); + assertThat(newNotes(destinationChange).getChange().getCherryPickOf()) + .isEqualTo(cherryPickChange.currentPatchSetId()); + } + + @Test public void updateCount() throws Exception { Change c = newChange(); assertThat(newNotes(c).getUpdateCount()).isEqualTo(1); |