Add Cherry-Pick mode.
Users can specify 'cherryPickMode = True' in the global section of
automerger.config to enable this mode.
Also, logging all exceptions thrown in event handler worker threads.
Bug: 361415628
Change-Id: I7ce71d24f6f95184e6673af6286fcd363b43feb2
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
index dad77dc..346b4b9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
@@ -72,7 +72,7 @@
}
String revision = rev.getPatchSet().commitId().name();
- MultipleDownstreamMergeInput mdsMergeInput = new MultipleDownstreamMergeInput();
+ MultipleDownstreamChangeInput mdsMergeInput = new MultipleDownstreamChangeInput();
mdsMergeInput.dsBranchMap = branchMap;
mdsMergeInput.changeNumber = change.getId().get();
mdsMergeInput.patchsetNumber = rev.getPatchSet().number();
@@ -85,7 +85,7 @@
logger.atFine().log("Multiple downstream merge input: %s", mdsMergeInput.dsBranchMap);
try {
- dsCreator.createMergesAndHandleConflicts(mdsMergeInput, config.getContextUserId());
+ dsCreator.createChangesAndHandleConflicts(mdsMergeInput, config.getContextUserId());
} catch (ConfigInvalidException e) {
throw new ResourceConflictException(
"Automerger configuration file is invalid: " + e.getMessage());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeMode.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeMode.java
new file mode 100644
index 0000000..8601f8b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeMode.java
@@ -0,0 +1,51 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * AutomergeMode will return the configured mode to create downstream changes.
+ */
+class AutomergeMode implements RestReadView<ConfigResource> {
+
+ protected ConfigLoader config;
+
+ /**
+ * Initializer for this class that sets the config.
+ *
+ * @param config Config for this plugin.
+ */
+ @Inject
+ public AutomergeMode(ConfigLoader config) {
+ this.config = config;
+ }
+
+ /**
+ * Return the Automerger mode: either MERGE or CHERRY-PICK.
+ *
+ * @return Either MERGE or CHERRY-PICK.
+ * @throws RestApiException
+ * @throws IOException
+ */
+ @Override
+ public Response<String> apply(ConfigResource configResource)
+ throws RestApiException, IOException {
+
+ try {
+ if(config.changeMode() == ChangeMode.CHERRY_PICK){
+ return Response.ok("CHERRY-PICK");
+ } else {
+ return Response.ok("MERGE");
+ }
+ } catch (ConfigInvalidException e) {
+ throw new ResourceConflictException(
+ "Automerger configuration file is invalid: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
index b588a2f..4c34126 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.automerger;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
@@ -40,6 +41,7 @@
DynamicSet.bind(binder(), RevisionCreatedListener.class).to(DownstreamCreator.class);
DynamicSet.bind(binder(), TopicEditedListener.class).to(DownstreamCreator.class);
DynamicSet.bind(binder(), MergeValidationListener.class).to(MergeValidator.class);
+ bind(ChangeCreatorApi.class).toProvider(ChangeCreatorProvider.class);
install(
new RestApiModule() {
@Override
@@ -47,6 +49,7 @@
post(REVISION_KIND, "automerge-change").to(AutomergeChangeAction.class);
post(REVISION_KIND, "config-downstream").to(ConfigDownstreamAction.class);
get(BRANCH_KIND, "all-config-downstream").to(AllConfigDownstreamAction.class);
+ get(CONFIG_KIND, "automerge-mode").to(AutomergeMode.class);
}
});
DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("automerger.js"));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorApi.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorApi.java
new file mode 100644
index 0000000..3d3f939
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorApi.java
@@ -0,0 +1,16 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** ChangeCreatorApi is the interface used to create or update downstream changes. */
+public interface ChangeCreatorApi {
+ ChangeApi create(SingleDownstreamChangeInput sdsChangeInput, String currentTopic)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
+ StorageException;
+
+ void update(UpdateDownstreamChangeInput updateDownstreamChangeInput)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorProvider.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorProvider.java
new file mode 100644
index 0000000..6d1d770
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeCreatorProvider.java
@@ -0,0 +1,44 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * ChangeCreatorProvider provides the appropriate ChangeCreatorApi implementation based on the
+ * plugin's configuration.
+ */
+public class ChangeCreatorProvider implements Provider<ChangeCreatorApi> {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final GerritApi gApi;
+ private final ConfigLoader config;
+
+ @Inject
+ public ChangeCreatorProvider(
+ GerritApi gApi,
+ ConfigLoader config
+ ) {
+ this.gApi = gApi;
+ this.config = config;
+ }
+
+ @Override
+ public ChangeCreatorApi get() {
+ ChangeMode changeMode = ChangeMode.MERGE;
+ try {
+ changeMode = config.changeMode();
+ } catch (ConfigInvalidException e) {
+ logger.atWarning().log(
+ "Unable to read the config for cherryPickMode value. Defaulting to legacy merge-mode behavior");
+ }
+
+ if(changeMode == ChangeMode.CHERRY_PICK){
+ return new CherryPickChangeCreator(gApi);
+ } else {
+ return new MergeChangeCreator(gApi);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeMode.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeMode.java
new file mode 100644
index 0000000..a4218ef
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeMode.java
@@ -0,0 +1,9 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+/**
+ * ChangeMode defines the mode for creating downstream changes.
+ */
+public enum ChangeMode {
+ MERGE,
+ CHERRY_PICK
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeUtils.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeUtils.java
new file mode 100644
index 0000000..f240498
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ChangeUtils.java
@@ -0,0 +1,214 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+/** ChangeUtils is a utility class for interacting with Gerrit changes */
+public final class ChangeUtils {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ public static final String AUTOMERGER_TAG = "autogenerated:Automerger";
+ private static final String SKIPPED_PREFIX = "skipped";
+ private static final String CURRENT = "current";
+
+ private ChangeUtils(){
+ throw new UnsupportedOperationException("ChangeUtils should not be instantiated.");
+ }
+
+ public static QueryBuilder constructTopicQuery(String topic) throws InvalidQueryParameterException {
+ QueryBuilder queryBuilder = new QueryBuilder();
+ queryBuilder.addParameter("topic", topic);
+ queryBuilder.addParameter("status", "open");
+ return queryBuilder;
+ }
+
+ public static List<ChangeInfo> getChangesInTopicAndBranch(GerritApi gApi, String topic, String downstreamBranch)
+ throws InvalidQueryParameterException, RestApiException {
+ QueryBuilder queryBuilder = constructTopicQuery(topic);
+ queryBuilder.addParameter("branch", downstreamBranch);
+ return gApi.changes()
+ .query(queryBuilder.get())
+ .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
+ .get();
+ }
+
+ public static List<String> getChangeParents(GerritApi gApi, int changeNumber, String currentRevision)
+ throws RestApiException {
+ ChangeApi change = gApi.changes().id(changeNumber);
+ List<String> parents = new ArrayList<>();
+ Map<String, RevisionInfo> revisionMap =
+ change.get(EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT))
+ .revisions;
+ List<CommitInfo> changeParents = revisionMap.get(currentRevision).commit.parents;
+ for (CommitInfo commit : changeParents) {
+ parents.add(commit.commit);
+ }
+ return parents;
+ }
+
+ /**
+ * Create subject line for downstream change with metadata from upstream change.
+ *
+ * <p>The downstream subject will be in the format: "[subjectPrefix] upstreamSubject am:
+ * upstreamRevision". If it is a skip, "am" will be replaced with "skipped", and [subjectPrefix]
+ * replaced with [subjectPrefix skipped].
+ *
+ * @param upstreamSubject Subject line of the upstream change
+ * @param upstreamRevision Commit SHA1 of the upstream change
+ * @param skipped Whether or not the merge is done with "-s ours"
+ * @return Subject line for downstream merge
+ */
+ public static String getSubjectForDownstreamChange(
+ String subjectPrefix, String upstreamSubject, String upstreamRevision, boolean skipped) {
+ if (!upstreamSubject.startsWith("[" + subjectPrefix)) {
+ String prefix = "[" + subjectPrefix + "]";
+ if (skipped) {
+ prefix = "[" + subjectPrefix + " " + SKIPPED_PREFIX + "]";
+ }
+ upstreamSubject = Joiner.on(" ").join(prefix, upstreamSubject);
+ }
+ String denotationString = skipped ? "skipped:" : "am:";
+ return Joiner.on(" ")
+ .join(upstreamSubject, denotationString, upstreamRevision.substring(0, 10));
+ }
+
+ public static String getTopic(GerritApi gApi, String revision) throws InvalidQueryParameterException, RestApiException {
+ QueryBuilder queryBuilder = new QueryBuilder();
+ queryBuilder.addParameter("commit", revision);
+ List<ChangeInfo> changes =
+ gApi.changes()
+ .query(queryBuilder.get())
+ .withOption(ListChangesOption.CURRENT_REVISION)
+ .get();
+ if (!changes.isEmpty()) {
+ for (ChangeInfo change : changes) {
+ if (change.currentRevision.equals(revision) && !"".equals(change.topic)) {
+ return change.topic;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static void tagChange(GerritApi gApi, ChangeInfo change, String message) throws RestApiException {
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.message(message);
+ reviewInput.notify = NotifyHandling.NONE;
+ reviewInput.tag = AUTOMERGER_TAG;
+ try {
+ gApi.changes().id(change.id).revision(CURRENT).review(reviewInput);
+ } catch (AuthException e) {
+ logger.atSevere().withCause(e).log("Automerger could not set label, but still continuing.");
+ }
+ }
+
+ public static String getSkipHashtag(String downstreamBranch){
+ return "am_skip_" + downstreamBranch;
+ }
+
+ public static boolean isDownstreamCherryPick(GerritApi gApi, String upstreamRevision, ChangeInfo downstreamChange){
+ try {
+ ChangeInfo upstreamChange = gApi.changes().id(upstreamRevision).get();
+ int upstreamPatchset = upstreamChange.revisions.get(upstreamRevision)._number;
+
+ if(downstreamChange.cherryPickOfChange == null)
+ return false;
+
+ return downstreamChange.cherryPickOfChange.equals(upstreamChange._number) &&
+ downstreamChange.cherryPickOfPatchSet == upstreamPatchset;
+ } catch (RestApiException e) {
+ logger.atSevere().withCause(e).log(
+ "Unable to lookup change with revision %s", upstreamRevision);
+ return false;
+ }
+ }
+
+ public static boolean isDownstreamMerge(String upstreamRevision, ChangeInfo downstreamChange){
+ String changeRevision = downstreamChange.currentRevision;
+ RevisionInfo revision = downstreamChange.revisions.get(changeRevision);
+ List<CommitInfo> parents = revision.commit.parents;
+ if (parents.size() > 1) {
+ String secondParent = parents.get(1).commit;
+ if (secondParent.equals(upstreamRevision)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isDownstreamChange(GerritApi gApi, String upstreamRevision, ChangeInfo downstreamChange, ChangeMode changeMode) {
+ boolean downstreamExists = (changeMode == ChangeMode.CHERRY_PICK) && isDownstreamCherryPick(gApi, upstreamRevision, downstreamChange);
+ downstreamExists |= (changeMode == ChangeMode.MERGE) && isDownstreamMerge(upstreamRevision, downstreamChange);
+
+ return downstreamExists;
+ }
+
+ /**
+ * Get the base change ID that the downstream change should be based off of, given the parents.
+ *
+ * <p>Given changes A and B where A is the first parent of B (stacked changes), and where A' is
+ * the downstream change autogenerated by A, and B' is the downstream change autogenerated by A,
+ * the first parent of B' should be A'.
+ *
+ * @param parents Parent commit SHAs of the change
+ * @return The base change ID that the change should be based off of, null if there is none.
+ * @throws InvalidQueryParameterException
+ * @throws RestApiException
+ */
+ private static ChangeInfo getBaseChangeInfo(GerritApi gApi, List<String> parents, String branch, ChangeMode changeMode)
+ throws InvalidQueryParameterException, RestApiException {
+ if (parents.isEmpty()) {
+ logger.atInfo().log("No base change id for change with no parents.");
+ return null;
+ }
+ // 1) Get topic of first parent
+ String firstParentTopic = ChangeUtils.getTopic(gApi, parents.get(0));
+ if (firstParentTopic == null) {
+ return null;
+ }
+ // 2) query that topic and use that to find A'
+ List<ChangeInfo> changesInTopic = ChangeUtils.getChangesInTopicAndBranch(gApi, firstParentTopic, branch);
+ String firstParent = parents.get(0);
+ for (ChangeInfo change : changesInTopic) {
+ if(isDownstreamChange(gApi, firstParent, change, changeMode)){
+ return change;
+ }
+ }
+ return null;
+ }
+ public static String getBaseChangeIdForMerge(GerritApi gApi, List<String> parents, String branch)
+ throws InvalidQueryParameterException, RestApiException {
+ ChangeInfo change = getBaseChangeInfo(gApi, parents, branch, ChangeMode.MERGE);
+
+ if(change == null)
+ return null;
+
+ return String.valueOf(change._number);
+ }
+
+ public static String getBaseChangeRevisionForCherryPick(GerritApi gApi, List<String> parents, String branch)
+ throws InvalidQueryParameterException, RestApiException {
+ ChangeInfo change = getBaseChangeInfo(gApi, parents, branch, ChangeMode.CHERRY_PICK);
+
+ if(change == null)
+ return null;
+
+ return change.currentRevision;
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/CherryPickChangeCreator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/CherryPickChangeCreator.java
new file mode 100644
index 0000000..6eac9f6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/CherryPickChangeCreator.java
@@ -0,0 +1,159 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * CherryPickChangeCreator handles creation and updating of downstream changes as cherry-picks.
+ */
+public class CherryPickChangeCreator implements ChangeCreatorApi {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String SUBJECT_PREFIX = "autocherry";
+ private final GerritApi gApi;
+ @Inject
+ public CherryPickChangeCreator(
+ GerritApi gApi) {
+ this.gApi = gApi;
+ }
+
+ /**
+ * Create a single downstream cherry-pick.
+ *
+ * <p>On a skip, only a hashtag is applied to the upstream change.</p>
+ *
+ * @param sdsChangeInput Input containing metadata for the cherry-pick.
+ * @param currentTopic Current topic to create change in.
+ * @throws RestApiException
+ * @throws ConfigInvalidException
+ * @throws InvalidQueryParameterException
+ * @throws StorageException
+ */
+ @Override
+ public ChangeApi create(SingleDownstreamChangeInput sdsChangeInput, String currentTopic)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException, StorageException {
+
+ if (!sdsChangeInput.doChange) {
+ logger.atFine().log(
+ "Skipping cherry-pick for %s to %s",
+ sdsChangeInput.currentRevision, sdsChangeInput.downstreamBranch);
+
+ applySkipHashtag(sdsChangeInput);
+
+ return null;
+ }
+
+ // This mirrors the MergeChangeCreator, although I don't believe it is possible with
+ // cherry-picks. Merge mode can encounter this scenario in diamond merges.
+ if (isAlreadyCherryPicked(sdsChangeInput, currentTopic)) {
+ logger.atInfo().log(
+ "Commit %s already cherry-picked into %s, not cherry-picking again.",
+ sdsChangeInput.currentRevision, sdsChangeInput.downstreamBranch);
+ return null;
+ }
+
+ removeSkipHashtag(sdsChangeInput);
+
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.base =
+ ChangeUtils.getBaseChangeRevisionForCherryPick(gApi,
+ ChangeUtils.getChangeParents(gApi, sdsChangeInput.changeNumber, sdsChangeInput.currentRevision),
+ sdsChangeInput.downstreamBranch);
+ cherryPickInput.message =
+ ChangeUtils.getSubjectForDownstreamChange(SUBJECT_PREFIX, sdsChangeInput.subject, sdsChangeInput.currentRevision, !sdsChangeInput.doChange);
+ cherryPickInput.destination = sdsChangeInput.downstreamBranch;
+ cherryPickInput.notify = NotifyHandling.NONE;
+ cherryPickInput.topic = currentTopic;
+
+ return gApi.changes().id(sdsChangeInput.changeNumber).current().cherryPick(cherryPickInput);
+ }
+
+ private Set<String> getSkipHashtagSet(String downstreamBranch) {
+ Set<String> set = new HashSet<>();
+ set.add(ChangeUtils.getSkipHashtag(downstreamBranch));
+ return set;
+ }
+
+ private void applySkipHashtag(SingleDownstreamChangeInput sdsChangeInput) throws RestApiException {
+ ChangeApi originalChange = gApi.changes().id(sdsChangeInput.changeNumber);
+ Set<String> set = getSkipHashtagSet(sdsChangeInput.downstreamBranch);
+ originalChange.setHashtags(new HashtagsInput(set));
+ }
+
+ private void removeSkipHashtag(SingleDownstreamChangeInput sdsChangeInput) throws RestApiException {
+ ChangeApi originalChange = gApi.changes().id(sdsChangeInput.changeNumber);
+ Set<String> set = getSkipHashtagSet(sdsChangeInput.downstreamBranch);
+
+ // It is safe to blindly attempt to remove the hashtag.
+ originalChange.setHashtags(new HashtagsInput(null, set));
+ }
+
+ @Override
+ public void update(UpdateDownstreamChangeInput updateDownstreamChangeInput)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
+
+ // For cherry-picks, we don't update the prior existing commit with a patch application.
+ // Doing this will not update the 'Cherry pick of' metadata with the correct patchset.
+ // Instead, we abandon the old one and create a new one.
+ AbandonInput abandonInput = new AbandonInput();
+ abandonInput.notify = NotifyHandling.NONE;
+ if(!updateDownstreamChangeInput.doChange){
+ abandonInput.message = "The cherry-pick from upstream is now skipped";
+ } else {
+ abandonInput.message = "The upstream patch set is no longer current";
+ }
+ gApi.changes().id(updateDownstreamChangeInput.downstreamChangeNumber).abandon(abandonInput);
+
+ SingleDownstreamChangeInput sdsChangeInput = getSingleDownstreamChangeInput(
+ updateDownstreamChangeInput);
+
+ // We still "create" in the event of a skip to apply the appropriate hashtag.
+ ChangeApi newDownstream = create(sdsChangeInput, updateDownstreamChangeInput.topic);
+ if(newDownstream != null) {
+ ChangeUtils.tagChange(gApi, newDownstream.get(), "Automerger change created!");
+ }
+ }
+
+ private static SingleDownstreamChangeInput getSingleDownstreamChangeInput(
+ UpdateDownstreamChangeInput updateDownstreamChangeInput) {
+
+ SingleDownstreamChangeInput sdsChangeInput = new SingleDownstreamChangeInput();
+ sdsChangeInput.subject =
+ ChangeUtils.getSubjectForDownstreamChange(SUBJECT_PREFIX,
+ updateDownstreamChangeInput.upstreamSubject,
+ updateDownstreamChangeInput.upstreamRevision, !updateDownstreamChangeInput.doChange);
+ sdsChangeInput.changeNumber = updateDownstreamChangeInput.upstreamChangeNumber;
+ sdsChangeInput.patchsetNumber = updateDownstreamChangeInput.patchSetNumber;
+ sdsChangeInput.doChange = updateDownstreamChangeInput.doChange;
+ sdsChangeInput.currentRevision = updateDownstreamChangeInput.upstreamRevision;
+ sdsChangeInput.downstreamBranch = updateDownstreamChangeInput.downstreamBranch;
+ return sdsChangeInput;
+ }
+
+ boolean isAlreadyCherryPicked(SingleDownstreamChangeInput sdsChangeInput, String currentTopic)
+ throws InvalidQueryParameterException, RestApiException {
+
+ List<ChangeInfo> changes =
+ ChangeUtils.getChangesInTopicAndBranch(gApi, currentTopic, sdsChangeInput.downstreamBranch);
+ for (ChangeInfo change : changes) {
+ if(change.cherryPickOfChange.equals(sdsChangeInput.changeNumber)
+ && change.cherryPickOfPatchSet == sdsChangeInput.patchsetNumber) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
index 5845f59..d2171e5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -400,4 +400,10 @@
projects.removeAll(ignoreProjects);
return projects;
}
+
+ public ChangeMode changeMode() throws ConfigInvalidException {
+ boolean cherryPickMode = getConfig().getBoolean("global", "cherryPickMode", false);
+
+ return cherryPickMode ? ChangeMode.CHERRY_PICK : ChangeMode.MERGE;
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
index d46f242..6c480c6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
@@ -23,16 +23,10 @@
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RestoreInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.common.MergeInput;
-import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
@@ -45,6 +39,7 @@
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.FanOutExecutor;
+import com.google.gerrit.server.submit.IntegrationConflictException;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.gson.Gson;
@@ -77,10 +72,7 @@
RevisionCreatedListener,
TopicEditedListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final String AUTOMERGER_TAG = "autogenerated:Automerger";
private static final String MERGE_CONFLICT_TAG = "autogenerated:MergeConflict";
- private static final String SUBJECT_PREFIX = "automerger";
- private static final String SKIPPED_PREFIX = "skipped";
private static final String CURRENT = "current";
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
@@ -89,6 +81,7 @@
private final ExecutorService executorService;
private final OneOffRequestContext oneOffRequestContext;
private final Provider<CurrentUser> user;
+ private final Provider<ChangeCreatorApi> changeCreator;
@Inject
public DownstreamCreator(
@@ -96,12 +89,15 @@
ConfigLoader config,
OneOffRequestContext oneOffRequestContext,
@FanOutExecutor ExecutorService executorService,
- Provider<CurrentUser> user) {
+ Provider<CurrentUser> user,
+ Provider<ChangeCreatorApi> changeCreator
+ ) {
this.gApi = gApi;
this.config = config;
this.oneOffRequestContext = oneOffRequestContext;
this.executorService = executorService;
this.user = user;
+ this.changeCreator = changeCreator;
}
/**
@@ -131,7 +127,7 @@
gApi.changes().id(change._number).revision(revisionNumber).commit(false).commit;
logger.atFine().log("Detected revision %s abandoned on %s.", revision, change.project);
abandonDownstream(change, revision, accountId);
- } catch (ConfigInvalidException | StorageException | RestApiException e) {
+ } catch (Exception e) {
logger.atSevere().withCause(e).log(
"Automerger plugin failed onChangeAbandoned for %s", change.id);
}
@@ -187,7 +183,7 @@
return;
}
- // If change is empty, prevent someone breaking topic.
+ // If change is empty, prevent someone breaking topic by reapplying the old topic.
if (isNullOrEmpty(change.topic)) {
try {
gApi.changes().id(change._number).topic(oldTopic);
@@ -205,7 +201,7 @@
for (String downstreamBranch : downstreamBranches) {
try {
List<Integer> existingDownstream =
- getExistingMergesOnBranch(revision, oldTopic, downstreamBranch, accountId);
+ getExistingChangesOnBranch(revision, oldTopic, downstreamBranch, accountId);
for (Integer changeNumber : existingDownstream) {
logger.atFine().log("Setting topic %s on %s", change.topic, changeNumber);
gApi.changes().id(changeNumber).topic(change.topic);
@@ -215,7 +211,7 @@
}
}
}
- } catch (StorageException | ConfigInvalidException e) {
+ } catch (Exception e) {
logger.atSevere().withCause(e).log(
"Automerger plugin failed onTopicEdited for %s", eventChange.id);
}
@@ -270,7 +266,7 @@
for (String downstreamBranch : downstreamBranches) {
try {
List<Integer> existingDownstream =
- getExistingMergesOnBranch(revision, change.topic, downstreamBranch, accountId);
+ getExistingChangesOnBranch(revision, change.topic, downstreamBranch, accountId);
for (Integer changeNumber : existingDownstream) {
ChangeInfo downstreamChange =
gApi.changes().id(changeNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
@@ -293,7 +289,7 @@
"Exception when updating downstream votes of %s", change.id);
}
}
- } catch (StorageException | ConfigInvalidException | RestApiException | IOException e) {
+ } catch (Exception e) {
logger.atSevere().withCause(e).log(
"Automerger plugin failed onCommentAdded for %s", change.id);
}
@@ -323,11 +319,7 @@
private void onChangeRestoredImpl(ChangeInfo change, RevisionInfo revision, Account.Id accountId) {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
automergeChanges(change, revision, accountId);
- } catch (RestApiException
- | IOException
- | ConfigInvalidException
- | InvalidQueryParameterException
- | StorageException e) {
+ } catch (Exception e) {
logger.atSevere().withCause(e).log(
"Automerger plugin failed onChangeRestored for %s", change.id);
}
@@ -358,18 +350,14 @@
public void onRevisionCreatedImpl(ChangeInfo change, RevisionInfo revision, Account.Id accountId) {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
automergeChanges(change, revision, accountId);
- } catch (RestApiException
- | IOException
- | ConfigInvalidException
- | InvalidQueryParameterException
- | StorageException e) {
+ } catch (Exception e){
logger.atSevere().withCause(e).log(
"Automerger plugin failed onRevisionCreated for %s", change.id);
}
}
public String getOrSetTopic(int sourceId, String topic, Account.Id accountId)
- throws RestApiException, ConfigInvalidException {
+ throws RestApiException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
if (isNullOrEmpty(topic)) {
topic = "am-" + UUID.randomUUID();
@@ -381,29 +369,29 @@
}
/**
- * Creates merges downstream, and votes on the automerge label if we have a failed merge.
+ * Creates changes downstream, and votes on the automerge label if we have a failed merge.
*
- * @param mdsMergeInput Input containing the downstream branch map and source change ID.
+ * @param mdsChangeInput Input containing the downstream branch map and source change ID.
* @param accountId Account ID to authorize Gerrit API calls.
* @throws RestApiException Throws if we fail a REST API call.
* @throws ConfigInvalidException Throws if we get a malformed configuration
* @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
* @throws StorageException Throws if we fail to open the request context
*/
- public void createMergesAndHandleConflicts(MultipleDownstreamMergeInput mdsMergeInput, Account.Id accountId)
+ public void createChangesAndHandleConflicts(MultipleDownstreamChangeInput mdsChangeInput, Account.Id accountId)
throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
StorageException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
ReviewInput reviewInput = new ReviewInput();
Map<String, Short> labels = new HashMap<>();
try {
- createDownstreamMerges(mdsMergeInput, accountId);
+ createDownstreamChanges(mdsChangeInput, accountId);
reviewInput.message =
"Automerging change "
- + mdsMergeInput.changeNumber
+ + mdsChangeInput.changeNumber
+ " to "
- + Joiner.on(", ").join(mdsMergeInput.dsBranchMap.keySet())
+ + Joiner.on(", ").join(mdsChangeInput.dsBranchMap.keySet())
+ " succeeded!";
reviewInput.notify = NotifyHandling.NONE;
} catch (FailedMergeException e) {
@@ -419,7 +407,7 @@
// Make the vote on the original change
ChangeInfo originalChange =
- getOriginalChange(mdsMergeInput.changeNumber, mdsMergeInput.currentRevision);
+ getOriginalChange(mdsChangeInput.changeNumber, mdsChangeInput.currentRevision);
// if this fails, i.e. -2 is restricted, catch it and still post message without a vote.
try {
gApi.changes().id(originalChange._number).revision(CURRENT).review(reviewInput);
@@ -436,9 +424,9 @@
}
/**
- * Creates merge downstream.
+ * Creates changes downstream.
*
- * @param mdsMergeInput Input containing the downstream branch map and source change ID.
+ * @param mdsChangeInput Input containing the downstream branch map and source change ID.
* @param accountId Account ID to authorize Gerrit API calls.
* @throws RestApiException Throws if we fail a REST API call.
* @throws FailedMergeException Throws if we get a merge conflict when merging downstream.
@@ -446,7 +434,7 @@
* @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
* @throws StorageException Throws if we fail to open the request context
*/
- private void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput, Account.Id accountId)
+ private void createDownstreamChanges(MultipleDownstreamChangeInput mdsChangeInput, Account.Id accountId)
throws RestApiException, FailedMergeException, ConfigInvalidException,
InvalidQueryParameterException, StorageException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
@@ -454,31 +442,35 @@
Map<String, String> failedMergeBranchMap = new TreeMap<>();
List<Integer> existingDownstream;
- for (String downstreamBranch : mdsMergeInput.dsBranchMap.keySet()) {
- // If there are existing downstream merges, update them
+ for (String downstreamBranch : mdsChangeInput.dsBranchMap.keySet()) {
+ // If there are existing downstream changes, update them
// Otherwise, create them.
boolean createDownstreams = true;
- if (mdsMergeInput.obsoleteRevision != null) {
+ if (mdsChangeInput.obsoleteRevision != null) {
existingDownstream =
- getExistingMergesOnBranch(
- mdsMergeInput.obsoleteRevision, mdsMergeInput.topic, downstreamBranch, accountId);
+ getExistingChangesOnBranch(
+ mdsChangeInput.obsoleteRevision, mdsChangeInput.topic, downstreamBranch, accountId);
if (!existingDownstream.isEmpty()) {
logger.atFine().log(
"Attempting to update downstream merge of %s on branch %s",
- mdsMergeInput.currentRevision, downstreamBranch);
+ mdsChangeInput.currentRevision, downstreamBranch);
// existingDownstream should almost always be of length one, but
// it's possible to construct it so that it's not
for (Integer dsChangeNumber : existingDownstream) {
try {
- updateDownstreamMerge(
- mdsMergeInput.currentRevision,
- mdsMergeInput.subject,
- dsChangeNumber,
- mdsMergeInput.dsBranchMap.get(downstreamBranch),
- mdsMergeInput.changeNumber,
- downstreamBranch);
+ UpdateDownstreamChangeInput updateDownstreamChangeInput = new UpdateDownstreamChangeInput();
+ updateDownstreamChangeInput.upstreamRevision = mdsChangeInput.currentRevision;
+ updateDownstreamChangeInput.upstreamSubject = mdsChangeInput.subject;
+ updateDownstreamChangeInput.downstreamChangeNumber = dsChangeNumber;
+ updateDownstreamChangeInput.doChange = mdsChangeInput.dsBranchMap.get(downstreamBranch);
+ updateDownstreamChangeInput.upstreamChangeNumber = mdsChangeInput.changeNumber;
+ updateDownstreamChangeInput.patchSetNumber = mdsChangeInput.patchsetNumber;
+ updateDownstreamChangeInput.downstreamBranch = downstreamBranch;
+ updateDownstreamChangeInput.topic = mdsChangeInput.topic;
+
+ changeCreator.get().update(updateDownstreamChangeInput);
createDownstreams = false;
- } catch (MergeConflictException e) {
+ } catch (MergeConflictException | IntegrationConflictException e) {
failedMergeBranchMap.put(downstreamBranch, e.getMessage());
logger.atFine().log(
"Abandoning existing, obsolete %s due to merge conflict.", dsChangeNumber);
@@ -489,19 +481,20 @@
}
if (createDownstreams) {
logger.atFine().log(
- "Attempting to create downstream merge of %s on branch %s",
- mdsMergeInput.currentRevision, downstreamBranch);
- SingleDownstreamMergeInput sdsMergeInput = new SingleDownstreamMergeInput();
- sdsMergeInput.currentRevision = mdsMergeInput.currentRevision;
- sdsMergeInput.changeNumber = mdsMergeInput.changeNumber;
- sdsMergeInput.project = mdsMergeInput.project;
- sdsMergeInput.topic = mdsMergeInput.topic;
- sdsMergeInput.subject = mdsMergeInput.subject;
- sdsMergeInput.downstreamBranch = downstreamBranch;
- sdsMergeInput.doMerge = mdsMergeInput.dsBranchMap.get(downstreamBranch);
+ "Attempting to create downstream change of %s on branch %s",
+ mdsChangeInput.currentRevision, downstreamBranch);
+ SingleDownstreamChangeInput sdsChangeInput = new SingleDownstreamChangeInput();
+ sdsChangeInput.currentRevision = mdsChangeInput.currentRevision;
+ sdsChangeInput.changeNumber = mdsChangeInput.changeNumber;
+ sdsChangeInput.patchsetNumber = mdsChangeInput.patchsetNumber;
+ sdsChangeInput.project = mdsChangeInput.project;
+ sdsChangeInput.topic = mdsChangeInput.topic;
+ sdsChangeInput.subject = mdsChangeInput.subject;
+ sdsChangeInput.downstreamBranch = downstreamBranch;
+ sdsChangeInput.doChange = mdsChangeInput.dsBranchMap.get(downstreamBranch);
try {
- createSingleDownstreamMerge(sdsMergeInput, accountId);
- } catch (MergeConflictException e) {
+ createSingleDownstreamChange(sdsChangeInput, accountId);
+ } catch (MergeConflictException | IntegrationConflictException e) {
failedMergeBranchMap.put(downstreamBranch, e.getMessage());
}
}
@@ -509,28 +502,27 @@
if (!failedMergeBranchMap.isEmpty()) {
String conflictMessage = config.getConflictMessage();
- if (mdsMergeInput.project.equals(config.getManifestProject())) {
+ if (mdsChangeInput.project.equals(config.getManifestProject())) {
conflictMessage = config.getManifestConflictMessage();
}
throw new FailedMergeException(
failedMergeBranchMap,
- mdsMergeInput.currentRevision,
+ mdsChangeInput.currentRevision,
config.getHostName(),
- mdsMergeInput.project,
- mdsMergeInput.changeNumber,
- mdsMergeInput.patchsetNumber,
+ mdsChangeInput.project,
+ mdsChangeInput.changeNumber,
+ mdsChangeInput.patchsetNumber,
conflictMessage,
- mdsMergeInput.topic);
+ mdsChangeInput.topic);
}
}
}
/**
- * Get change IDs of the immediately downstream changes of the revision on the branch.
+ * Get change numbers of the immediately downstream changes of the revision on the branch.
*
- * @param upstreamRevision Revision of the original change.
* @param topic Topic of the original change.
- * @param downstreamBranch Branch to check for existing merge CLs.
+ * @param downstreamBranch Branch to check for existing automerger CLs.
* @param accountId Account ID to authorize Gerrit API calls.
* @return List of change numbers that are downstream of the given branch.
* @throws RestApiException Throws when we fail a REST API call.
@@ -538,25 +530,21 @@
* @throws ConfigInvalidException Throws if we fail to read the config
* @throws StorageException Throws if we fail to open the request context
*/
- private List<Integer> getExistingMergesOnBranch(
+ private List<Integer> getExistingChangesOnBranch(
String upstreamRevision, String topic, String downstreamBranch, Account.Id accountId)
throws RestApiException, InvalidQueryParameterException, StorageException,
ConfigInvalidException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
List<Integer> downstreamChangeNumbers = new ArrayList<>();
- List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch);
+ List<ChangeInfo> changes = ChangeUtils.getChangesInTopicAndBranch(gApi, topic, downstreamBranch);
+ ChangeMode changeMode = config.changeMode();
for (ChangeInfo change : changes) {
- String changeRevision = change.currentRevision;
- RevisionInfo revision = change.revisions.get(changeRevision);
- List<CommitInfo> parents = revision.commit.parents;
- if (parents.size() > 1) {
- String secondParent = parents.get(1).commit;
- if (secondParent.equals(upstreamRevision)) {
- downstreamChangeNumbers.add(change._number);
- }
+ if(ChangeUtils.isDownstreamChange(gApi, upstreamRevision, change, changeMode)) {
+ downstreamChangeNumbers.add(change._number);
}
}
+
return downstreamChangeNumbers;
}
}
@@ -564,94 +552,24 @@
/**
* Create a single downstream merge.
*
- * @param sdsMergeInput Input containing metadata for the merge.
+ * @param sdsChangeInput Input containing metadata for the merge.
* @param accountId Account ID to authorize Gerrit API calls.
* @throws RestApiException
* @throws ConfigInvalidException
* @throws InvalidQueryParameterException
* @throws StorageException
*/
- private void createSingleDownstreamMerge(SingleDownstreamMergeInput sdsMergeInput, Account.Id accountId)
+ private void createSingleDownstreamChange(SingleDownstreamChangeInput sdsChangeInput, Account.Id accountId)
throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
StorageException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(accountId)) {
- String currentTopic = getOrSetTopic(sdsMergeInput.changeNumber, sdsMergeInput.topic, accountId);
+ String currentTopic = getOrSetTopic(sdsChangeInput.changeNumber, sdsChangeInput.topic, accountId);
- if (isAlreadyMerged(sdsMergeInput, currentTopic)) {
- logger.atInfo().log(
- "Commit %s already merged into %s, not automerging again.",
- sdsMergeInput.currentRevision, sdsMergeInput.downstreamBranch);
- return;
- }
-
- MergeInput mergeInput = new MergeInput();
- mergeInput.source = sdsMergeInput.currentRevision;
- mergeInput.strategy = "recursive";
-
- logger.atFine().log("Creating downstream merge for %s", sdsMergeInput.currentRevision);
- ChangeInput downstreamChangeInput = new ChangeInput();
- downstreamChangeInput.project = sdsMergeInput.project;
- downstreamChangeInput.branch = sdsMergeInput.downstreamBranch;
- downstreamChangeInput.subject =
- getSubjectForDownstreamMerge(sdsMergeInput.subject, sdsMergeInput.currentRevision, false);
- downstreamChangeInput.topic = currentTopic;
- downstreamChangeInput.merge = mergeInput;
- downstreamChangeInput.notify = NotifyHandling.NONE;
-
- downstreamChangeInput.baseChange =
- getBaseChangeId(
- getChangeParents(sdsMergeInput.changeNumber, sdsMergeInput.currentRevision),
- sdsMergeInput.downstreamBranch);
-
- if (!sdsMergeInput.doMerge) {
- mergeInput.strategy = "ours";
- downstreamChangeInput.subject =
- getSubjectForDownstreamMerge(
- sdsMergeInput.subject, sdsMergeInput.currentRevision, true);
- logger.atFine().log(
- "Skipping merge for %s to %s",
- sdsMergeInput.currentRevision, sdsMergeInput.downstreamBranch);
- }
-
- ChangeApi downstreamChange = gApi.changes().create(downstreamChangeInput);
- tagChange(downstreamChange.get(), "Automerger change created!");
- }
- }
-
- /**
- * Get the base change ID that the downstream change should be based off of, given the parents.
- *
- * <p>Given changes A and B where A is the first parent of B, and where A' is the change whose
- * second parent is A, and B' is the change whose second parent is B, the first parent of B'
- * should be A'.
- *
- * @param parents Parent commit SHAs of the change
- * @return The base change ID that the change should be based off of, null if there is none.
- * @throws InvalidQueryParameterException
- * @throws RestApiException
- */
- private String getBaseChangeId(List<String> parents, String branch)
- throws InvalidQueryParameterException, RestApiException {
- if (parents.isEmpty()) {
- logger.atInfo().log("No base change id for change with no parents.");
- return null;
- }
- // 1) Get topic of first parent
- String firstParentTopic = getTopic(parents.get(0));
- if (firstParentTopic == null) {
- return null;
- }
- // 2) query that topic and use that to find A'
- List<ChangeInfo> changesInTopic = getChangesInTopicAndBranch(firstParentTopic, branch);
- String firstParent = parents.get(0);
- for (ChangeInfo change : changesInTopic) {
- List<CommitInfo> topicChangeParents =
- change.revisions.get(change.currentRevision).commit.parents;
- if (topicChangeParents.size() > 1 && topicChangeParents.get(1).commit.equals(firstParent)) {
- return String.valueOf(change._number);
+ ChangeApi downstreamChange = changeCreator.get().create(sdsChangeInput, currentTopic);
+ if(downstreamChange != null) {
+ ChangeUtils.tagChange(gApi, downstreamChange.get(), "Automerger change created!");
}
}
- return null;
}
private void automergeChanges(ChangeInfo change, RevisionInfo revisionInfo, Account.Id accountId)
@@ -681,7 +599,7 @@
ChangeApi currentChange = gApi.changes().id(change._number);
String previousRevision = getPreviousRevision(currentChange, revisionInfo._number);
- MultipleDownstreamMergeInput mdsMergeInput = new MultipleDownstreamMergeInput();
+ MultipleDownstreamChangeInput mdsMergeInput = new MultipleDownstreamChangeInput();
mdsMergeInput.dsBranchMap = dsBranchMap;
mdsMergeInput.changeNumber = change._number;
mdsMergeInput.patchsetNumber = revisionInfo._number;
@@ -691,7 +609,7 @@
mdsMergeInput.obsoleteRevision = previousRevision;
mdsMergeInput.currentRevision = currentRevision;
- createMergesAndHandleConflicts(mdsMergeInput, accountId);
+ createChangesAndHandleConflicts(mdsMergeInput, accountId);
}
private void abandonDownstream(ChangeInfo change, String revision, Account.Id accountId)
@@ -706,7 +624,7 @@
for (String downstreamBranch : downstreamBranches) {
List<Integer> existingDownstream =
- getExistingMergesOnBranch(revision, change.topic, downstreamBranch, accountId);
+ getExistingChangesOnBranch(revision, change.topic, downstreamBranch, accountId);
logger.atFine().log("Abandoning existing downstreams: %s", existingDownstream);
for (Integer changeNumber : existingDownstream) {
abandonChange(changeNumber);
@@ -725,7 +643,7 @@
labels.put(label, vote);
reviewInput.labels = labels;
reviewInput.notify = NotifyHandling.NONE;
- reviewInput.tag = AUTOMERGER_TAG;
+ reviewInput.tag = ChangeUtils.AUTOMERGER_TAG;
try {
gApi.changes().id(change.id).revision(CURRENT).review(reviewInput);
} catch (AuthException e) {
@@ -733,56 +651,6 @@
}
}
- private void tagChange(ChangeInfo change, String message) throws RestApiException {
- ReviewInput reviewInput = new ReviewInput();
- reviewInput.message(message);
- reviewInput.notify = NotifyHandling.NONE;
- reviewInput.tag = AUTOMERGER_TAG;
- try {
- gApi.changes().id(change.id).revision(CURRENT).review(reviewInput);
- } catch (AuthException e) {
- logger.atSevere().withCause(e).log("Automerger could not set label, but still continuing.");
- }
- }
-
- private void updateDownstreamMerge(
- String newParentRevision,
- String upstreamSubject,
- Integer sourceNum,
- boolean doMerge,
- Integer upstreamChangeNumber,
- String downstreamBranch)
- throws RestApiException, InvalidQueryParameterException {
- MergeInput mergeInput = new MergeInput();
- mergeInput.source = newParentRevision;
-
- MergePatchSetInput mergePatchSetInput = new MergePatchSetInput();
-
- mergePatchSetInput.subject =
- getSubjectForDownstreamMerge(upstreamSubject, newParentRevision, false);
- if (!doMerge) {
- mergeInput.strategy = "ours";
- mergePatchSetInput.subject =
- getSubjectForDownstreamMerge(upstreamSubject, newParentRevision, true);
- logger.atFine().log("Skipping merge for %s on %s", newParentRevision, sourceNum);
- }
- mergePatchSetInput.merge = mergeInput;
-
- mergePatchSetInput.baseChange =
- getBaseChangeId(
- getChangeParents(upstreamChangeNumber, newParentRevision), downstreamBranch);
-
- ChangeApi originalChange = gApi.changes().id(sourceNum);
-
- if (originalChange.info().status == ChangeStatus.ABANDONED) {
- RestoreInput restoreInput = new RestoreInput();
- restoreInput.message = "Restoring change due to upstream automerge.";
- originalChange.restore(restoreInput);
- }
-
- originalChange.createMergePatchSet(mergePatchSetInput);
- }
-
private String getPreviousRevision(ChangeApi change, int currentPatchSetNumber)
throws RestApiException {
String previousRevision = null;
@@ -802,9 +670,24 @@
return previousRevision;
}
- private ChangeInfo getOriginalChange(int changeNumber, String currentRevision)
+ private ChangeInfo getOriginalChangeCherryPickMode(int changeNumber)
throws RestApiException, InvalidQueryParameterException {
- List<String> parents = getChangeParents(changeNumber, currentRevision);
+
+ ChangeInfo current = gApi.changes().id(changeNumber).get();
+ String topic = current.topic;
+ List<ChangeInfo> changesInTopic = getChangesInTopic(topic);
+ for (ChangeInfo change : changesInTopic) {
+ if(ChangeUtils.isDownstreamCherryPick(gApi, change.currentRevision, current)){
+ return getOriginalChangeCherryPickMode(change._number);
+ }
+ }
+
+ return current;
+ }
+
+ private ChangeInfo getOriginalChangeMergeMode(int changeNumber, String currentRevision)
+ throws RestApiException, InvalidQueryParameterException {
+ List<String> parents = ChangeUtils.getChangeParents(gApi, changeNumber, currentRevision);
if (parents.size() >= 2) {
String secondParentRevision = parents.get(1);
String topic = gApi.changes().id(changeNumber).topic();
@@ -818,114 +701,36 @@
return gApi.changes().id(changeNumber).get();
}
- private List<String> getChangeParents(int changeNumber, String currentRevision)
- throws RestApiException {
- ChangeApi change = gApi.changes().id(changeNumber);
- List<String> parents = new ArrayList<>();
- Map<String, RevisionInfo> revisionMap =
- change.get(EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT))
- .revisions;
- List<CommitInfo> changeParents = revisionMap.get(currentRevision).commit.parents;
- for (CommitInfo commit : changeParents) {
- parents.add(commit.commit);
+ private ChangeInfo getOriginalChange(int changeNumber, String currentRevision)
+ throws RestApiException, InvalidQueryParameterException {
+ ChangeMode changeMode = ChangeMode.MERGE;
+ try {
+ changeMode = config.changeMode();
+ } catch (ConfigInvalidException e) {
+ logger.atSevere().withCause(e).log("Automerger could not read config, but still continuing.");
}
- return parents;
+
+ if(changeMode == ChangeMode.CHERRY_PICK){
+ return getOriginalChangeCherryPickMode(changeNumber);
+ } else {
+ return getOriginalChangeMergeMode(changeNumber, currentRevision);
+ }
}
private void abandonChange(Integer changeNumber) throws RestApiException {
logger.atFine().log("Abandoning change: %s", changeNumber);
AbandonInput abandonInput = new AbandonInput();
abandonInput.notify = NotifyHandling.NONE;
- abandonInput.message = "Merge parent updated; abandoning due to upstream conflict.";
+ abandonInput.message = "Upstream change updated; abandoning due to upstream conflict.";
gApi.changes().id(changeNumber).abandon(abandonInput);
}
- private String getTopic(String revision) throws InvalidQueryParameterException, RestApiException {
- QueryBuilder queryBuilder = new QueryBuilder();
- queryBuilder.addParameter("commit", revision);
- List<ChangeInfo> changes =
- gApi.changes()
- .query(queryBuilder.get())
- .withOption(ListChangesOption.CURRENT_REVISION)
- .get();
- if (!changes.isEmpty()) {
- for (ChangeInfo change : changes) {
- if (change.currentRevision.equals(revision) && !"".equals(change.topic)) {
- return change.topic;
- }
- }
- }
- return null;
- }
-
- private QueryBuilder constructTopicQuery(String topic) throws InvalidQueryParameterException {
- QueryBuilder queryBuilder = new QueryBuilder();
- queryBuilder.addParameter("topic", topic);
- queryBuilder.addParameter("status", "open");
- return queryBuilder;
- }
-
private List<ChangeInfo> getChangesInTopic(String topic)
throws InvalidQueryParameterException, RestApiException {
- QueryBuilder queryBuilder = constructTopicQuery(topic);
+ QueryBuilder queryBuilder = ChangeUtils.constructTopicQuery(topic);
return gApi.changes()
.query(queryBuilder.get())
.withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
.get();
}
-
- private List<ChangeInfo> getChangesInTopicAndBranch(String topic, String downstreamBranch)
- throws InvalidQueryParameterException, RestApiException {
- QueryBuilder queryBuilder = constructTopicQuery(topic);
- queryBuilder.addParameter("branch", downstreamBranch);
- return gApi.changes()
- .query(queryBuilder.get())
- .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
- .get();
- }
-
- private boolean isAlreadyMerged(SingleDownstreamMergeInput sdsMergeInput, String currentTopic)
- throws InvalidQueryParameterException, RestApiException {
- // If we've already merged this commit to this branch, don't do it again.
- List<ChangeInfo> changes =
- getChangesInTopicAndBranch(currentTopic, sdsMergeInput.downstreamBranch);
- for (ChangeInfo change : changes) {
- if (change.branch.equals(sdsMergeInput.downstreamBranch)) {
- List<CommitInfo> parents = change.revisions.get(change.currentRevision).commit.parents;
- if (parents.size() > 1) {
- String secondParent = parents.get(1).commit;
- if (secondParent.equals(sdsMergeInput.currentRevision)) {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- /**
- * Create subject line for downstream merge with metadata from upstream change.
- *
- * <p>The downstream subject will be in the format: "[automerger] upstreamSubject am:
- * upstreamRevision". If it is a skip, "am" will be replaced with "skipped", and [automerger]
- * replaced with [automerger skipped].
- *
- * @param upstreamSubject Subject line of the upstream change
- * @param upstreamRevision Commit SHA1 of the upstream change
- * @param skipped Whether or not the merge is done with "-s ours"
- * @return Subject line for downstream merge
- */
- private String getSubjectForDownstreamMerge(
- String upstreamSubject, String upstreamRevision, boolean skipped) {
- if (!upstreamSubject.startsWith("[" + SUBJECT_PREFIX)) {
- String prefix = "[" + SUBJECT_PREFIX + "]";
- if (skipped) {
- prefix = "[" + SUBJECT_PREFIX + " " + SKIPPED_PREFIX + "]";
- }
- upstreamSubject = Joiner.on(" ").join(prefix, upstreamSubject);
- }
- String denotationString = skipped ? "skipped:" : "am:";
- return Joiner.on(" ")
- .join(upstreamSubject, denotationString, upstreamRevision.substring(0, 10));
- }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeChangeCreator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeChangeCreator.java
new file mode 100644
index 0000000..7ed8e1d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeChangeCreator.java
@@ -0,0 +1,136 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RestoreInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * MergeChangeCreator handles creation and updating of downstream changes as merges.
+ */
+public class MergeChangeCreator implements ChangeCreatorApi {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String SUBJECT_PREFIX = "automerger";
+ private final GerritApi gApi;
+ @Inject
+ public MergeChangeCreator(
+ GerritApi gApi) {
+ this.gApi = gApi;
+ }
+ /**
+ * Create a single downstream merge.
+ *
+ * @param sdsChangeInput Input containing metadata for the merge.
+ * @param currentTopic Current topic to create change in.
+ * @throws RestApiException
+ * @throws ConfigInvalidException
+ * @throws InvalidQueryParameterException
+ * @throws StorageException
+ */
+ @Override
+ public ChangeApi create(SingleDownstreamChangeInput sdsChangeInput,
+ String currentTopic)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException, StorageException {
+
+ if (isAlreadyMerged(sdsChangeInput, currentTopic)) {
+ logger.atInfo().log(
+ "Commit %s already merged into %s, not automerging again.",
+ sdsChangeInput.currentRevision, sdsChangeInput.downstreamBranch);
+ return null;
+ }
+
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = sdsChangeInput.currentRevision;
+ mergeInput.strategy = "recursive";
+
+ logger.atFine().log("Creating downstream merge for %s", sdsChangeInput.currentRevision);
+ ChangeInput downstreamChangeInput = new ChangeInput();
+ downstreamChangeInput.project = sdsChangeInput.project;
+ downstreamChangeInput.branch = sdsChangeInput.downstreamBranch;
+ downstreamChangeInput.subject =
+ ChangeUtils.getSubjectForDownstreamChange(SUBJECT_PREFIX, sdsChangeInput.subject, sdsChangeInput.currentRevision, !sdsChangeInput.doChange);
+ downstreamChangeInput.topic = currentTopic;
+ downstreamChangeInput.merge = mergeInput;
+ downstreamChangeInput.notify = NotifyHandling.NONE;
+
+ downstreamChangeInput.baseChange =
+ ChangeUtils.getBaseChangeIdForMerge(gApi,
+ ChangeUtils.getChangeParents(gApi, sdsChangeInput.changeNumber, sdsChangeInput.currentRevision),
+ sdsChangeInput.downstreamBranch);
+
+ if (!sdsChangeInput.doChange) {
+ mergeInput.strategy = "ours";
+ logger.atFine().log(
+ "Skipping merge for %s to %s",
+ sdsChangeInput.currentRevision, sdsChangeInput.downstreamBranch);
+ }
+
+ return gApi.changes().create(downstreamChangeInput);
+ }
+
+ @Override
+ public void update(UpdateDownstreamChangeInput updateDownstreamChangeInput)
+ throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
+
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = updateDownstreamChangeInput.upstreamRevision;
+
+ MergePatchSetInput mergePatchSetInput = new MergePatchSetInput();
+
+ mergePatchSetInput.subject =
+ ChangeUtils.getSubjectForDownstreamChange(SUBJECT_PREFIX,
+ updateDownstreamChangeInput.upstreamSubject,
+ updateDownstreamChangeInput.upstreamRevision, !updateDownstreamChangeInput.doChange);
+ if (!updateDownstreamChangeInput.doChange) {
+ mergeInput.strategy = "ours";
+ logger.atFine().log("Skipping merge for %s on %s",
+ updateDownstreamChangeInput.upstreamRevision, updateDownstreamChangeInput.downstreamChangeNumber);
+ }
+ mergePatchSetInput.merge = mergeInput;
+
+ mergePatchSetInput.baseChange =
+ ChangeUtils.getBaseChangeIdForMerge(gApi,
+ ChangeUtils.getChangeParents(gApi, updateDownstreamChangeInput.upstreamChangeNumber,
+ updateDownstreamChangeInput.upstreamRevision), updateDownstreamChangeInput.downstreamBranch);
+
+ ChangeApi originalChange = gApi.changes().id(updateDownstreamChangeInput.downstreamChangeNumber);
+
+ if (originalChange.info().status == ChangeStatus.ABANDONED) {
+ RestoreInput restoreInput = new RestoreInput();
+ restoreInput.message = "Restoring change due to upstream automerge.";
+ originalChange.restore(restoreInput);
+ }
+
+ originalChange.createMergePatchSet(mergePatchSetInput);
+ }
+
+ boolean isAlreadyMerged(SingleDownstreamChangeInput sdsChangeInput, String currentTopic)
+ throws InvalidQueryParameterException, RestApiException {
+ // If we've already merged this commit to this branch, don't do it again.
+ List<ChangeInfo> changes =
+ ChangeUtils.getChangesInTopicAndBranch(gApi, currentTopic, sdsChangeInput.downstreamBranch);
+ for (ChangeInfo change : changes) {
+ List<CommitInfo> parents = change.revisions.get(change.currentRevision).commit.parents;
+ if (parents.size() > 1) {
+ String secondParent = parents.get(1).commit;
+ if (secondParent.equals(sdsChangeInput.currentRevision)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
index 16459a6..b5c13fc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
@@ -23,8 +23,6 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.CodeReviewCommit;
@@ -105,6 +103,7 @@
throws RestApiException, IOException, ConfigInvalidException, InvalidQueryParameterException {
Set<String> missingDownstreamBranches = new HashSet<>();
+ ChangeMode changeMode = config.changeMode();
Set<String> downstreamBranches =
config.getDownstreamBranches(upstreamChange.branch, upstreamChange.project);
for (String downstreamBranch : downstreamBranches) {
@@ -115,6 +114,9 @@
missingDownstreamBranches.add(downstreamBranch);
continue;
}
+ if(cherryPickSkipped(upstreamChange, downstreamBranch)){
+ continue;
+ }
queryBuilder.addParameter("topic", upstreamChange.topic);
queryBuilder.addParameter("branch", downstreamBranch);
queryBuilder.addParameter("status", "open");
@@ -124,14 +126,9 @@
.withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
.get();
for (ChangeInfo change : changes) {
- RevisionInfo revision = change.revisions.get(change.currentRevision);
- List<CommitInfo> parents = revision.commit.parents;
- if (parents.size() > 1) {
- String secondParent = parents.get(1).commit;
- if (secondParent.equals(upstreamChange.currentRevision)) {
- dsExists = true;
- break;
- }
+ if(ChangeUtils.isDownstreamChange(gApi, upstreamChange.currentRevision, change, changeMode)) {
+ dsExists = true;
+ break;
}
}
if (!dsExists) {
@@ -140,4 +137,16 @@
}
return missingDownstreamBranches;
}
+
+ private boolean cherryPickSkipped(ChangeInfo change, String downstreamBranch){
+ try {
+ if(config.changeMode() == ChangeMode.MERGE){
+ return false;
+ }
+ } catch (ConfigInvalidException e) {
+ return false;
+ }
+
+ return change.hashtags.contains(ChangeUtils.getSkipHashtag(downstreamBranch));
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamChangeInput.java
similarity index 85%
rename from src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java
rename to src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamChangeInput.java
index a62d9a7..476c5d4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamChangeInput.java
@@ -17,9 +17,10 @@
import java.util.Map;
/**
- * Class to hold input for a set of merges from a single source change, with associated metadata.
+ * Class to hold input for a set of downstream changes from a single source change, with associated
+ * metadata.
*/
-public class MultipleDownstreamMergeInput {
+public class MultipleDownstreamChangeInput {
public Map<String, Boolean> dsBranchMap;
public int changeNumber;
public int patchsetNumber;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamChangeInput.java
similarity index 84%
rename from src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java
rename to src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamChangeInput.java
index 5a5f0e7..6804d70 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamChangeInput.java
@@ -14,8 +14,8 @@
package com.googlesource.gerrit.plugins.automerger;
-/** Class to hold input for a merge for a single source change and destination branch. */
-public class SingleDownstreamMergeInput {
+/** Class to hold input for a downstream change from a single source change. */
+public class SingleDownstreamChangeInput {
public String currentRevision;
public int changeNumber;
public int patchsetNumber;
@@ -23,5 +23,5 @@
public String topic;
public String subject;
public String downstreamBranch;
- public boolean doMerge;
+ public boolean doChange;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/UpdateDownstreamChangeInput.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/UpdateDownstreamChangeInput.java
new file mode 100644
index 0000000..edaa6cc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/UpdateDownstreamChangeInput.java
@@ -0,0 +1,13 @@
+package com.googlesource.gerrit.plugins.automerger;
+
+/** Class to hold input for a downstream change update from a single source change. */
+public class UpdateDownstreamChangeInput {
+ public String upstreamRevision;
+ public String upstreamSubject;
+ public int downstreamChangeNumber;
+ public int upstreamChangeNumber;
+ public int patchSetNumber;
+ public String downstreamBranch;
+ public String topic;
+ public boolean doChange;
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 13f69f8..1e17aa7 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -17,3 +17,5 @@
A UI button "Recreate automerges" has been added so that users can skip
downstream merges. Unchecking a branch's checkbox will skip that branch and
all automerges downstream of that branch.
+
+The plugin can be configured to perform cherry-picks instead of merges.
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 6e629b3..b494fcb 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -17,6 +17,7 @@
blankMerge = .*RESTRICT AUTOMERGE.*
blankMerge = .*SKIP UNLESS MERGEALL SET.*
missingDownstreamsMessage = there is no ${missingDownstreams}
+ cherryPickMode = False
[automerger "branch1:branch2"]
setProjects = some/project
@@ -133,6 +134,12 @@
credentials of this user ID instead of the credentials of the user doing the
upstream operation.
+global.cherryPickMode
+: If this is true, cherry-picks will be executed instead of merges.
+
+When a change is skipped in cherry-pick mode, a downstream change is not created
+and a "am_skip_<branch>" hashtag is added to the upstream commit.
+
automerger.branch1:branch2.setProjects
: Projects to automerge for.
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
index 3b0b5b6..a25522f 100644
--- a/src/main/resources/Documentation/rest-api.md
+++ b/src/main/resources/Documentation/rest-api.md
@@ -85,4 +85,25 @@
```
HTTP/1.1 204 No Content
-```
\ No newline at end of file
+```
+
+### <a id="config-downstream"> Cherry Pick Mode
+GET /config/server/automerger~automerge-mode
+
+Returns either "CHERRY-PICK" or "MERGE".
+
+#### Request
+
+```
+ GET /config/server/automerger~automerge-mode HTTP/1.1
+```
+
+#### Response
+
+```
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=utf-8
+ )]}'
+ "CHERRY-PICK"
+```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
index 254942f..3a5f8b3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.OptionalSubject.optionals;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
@@ -41,12 +42,14 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -88,8 +91,7 @@
return cfg;
}
- @Test
- public void testExpectedFlow() throws Exception {
+ private void expectedFlow(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
PushOneCommit.Result result =
@@ -98,7 +100,7 @@
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
createBranch(BranchNameKey.create(projectName, "ds_two"));
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
@@ -109,7 +111,6 @@
.withOption(CURRENT_REVISION)
.get();
assertThat(changesInTopic).hasSize(3);
-
List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
ChangeInfo dsOneChangeInfo = sortedChanges.get(0);
@@ -127,21 +128,32 @@
// Ensure that commit subjects are correct
String masterSubject = masterChangeInfo.subject;
String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
- assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger");
+ String tag = getTag(changeMode);
+ assertThat(masterChangeInfo.subject).doesNotContainMatch(tag);
assertThat(dsOneChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
assertThat(dsTwoChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
// +2 and submit
merge(result);
assertCodeReview(masterChangeInfo.id, 2, null);
assertCodeReview(dsOneChangeInfo.id, 2, "autogenerated:Automerger");
assertCodeReview(dsTwoChangeInfo.id, 2, "autogenerated:Automerger");
+
}
@Test
- public void testDiamondMerge() throws Exception {
+ public void testExpectedFlow() throws Exception {
+ expectedFlow(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testExpectedFlowCherryPickMode() throws Exception {
+ expectedFlow(ChangeMode.CHERRY_PICK);
+ }
+
+ private void diamondMerge(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
// Create initial change
@@ -172,7 +184,7 @@
// For this test, right != left
assertThat(leftRevision).isNotEqualTo(rightRevision);
createBranch(BranchNameKey.create(projectName, "bottom"));
- pushDiamondConfig(manifestNameKey.get(), projectName);
+ pushDiamondConfig(manifestNameKey.get(), projectName, changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
testRepo.reset(masterWithMergedChange);
PushOneCommit.Result result =
@@ -210,39 +222,55 @@
assertThat(rightChangeInfo.branch).isEqualTo("right");
assertCodeReview(rightChangeInfo.id, 2, "autogenerated:Automerger");
+ String tag = getTag(changeMode);
+
// Ensure that commit subjects are correct
String masterSubject = masterChangeInfo.subject;
String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
String shortLeftSha = leftChangeInfo.currentRevision.substring(0, 10);
String shortRightSha = rightChangeInfo.currentRevision.substring(0, 10);
- assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger");
+ assertThat(masterChangeInfo.subject).doesNotContainMatch(tag);
assertThat(leftChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
assertThat(rightChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
// Either bottomChangeInfoA came from left and bottomChangeInfoB came from right, or vice versa
// We don't know which, so we use the if condition to check
- String bottomChangeInfoASecondParent = getParent(bottomChangeInfoA, 1);
- if (bottomChangeInfoASecondParent.equals(leftChangeInfo.currentRevision)) {
+ String bottomChangeInfoATarget = "";
+ if(changeMode == ChangeMode.CHERRY_PICK){
+ bottomChangeInfoATarget = getCherryPickFrom(bottomChangeInfoA);
+ } else {
+ bottomChangeInfoATarget = getParent(bottomChangeInfoA, 1);
+ }
+ if (bottomChangeInfoATarget.equals(leftChangeInfo.currentRevision)) {
assertThat(bottomChangeInfoA.subject)
.isEqualTo(
- "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha);
+ "[" + tag + "] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha);
assertThat(bottomChangeInfoB.subject)
.isEqualTo(
- "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha);
+ "[" + tag + "] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha);
} else {
assertThat(bottomChangeInfoA.subject)
.isEqualTo(
- "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha);
+ "[" + tag + "] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha);
assertThat(bottomChangeInfoB.subject)
.isEqualTo(
- "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha);
+ "[" + tag + "] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha);
}
}
@Test
- public void testChangeStack() throws Exception {
+ public void testDiamondMerge() throws Exception {
+ diamondMerge(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testDiamondMergeCherryPickMode() throws Exception {
+ diamondMerge(ChangeMode.CHERRY_PICK);
+ }
+
+ private void changeStack(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
PushOneCommit.Result result =
@@ -250,7 +278,7 @@
// Project name is scoped by test, so we need to get it from our initial change
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
- pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
+ pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
@@ -286,13 +314,23 @@
assertThat(bFirstParent).isEqualTo(a.currentRevision);
// Ensure that commit subjects are correct
+ String tag = getTag(changeMode);
String shortASha = a.currentRevision.substring(0, 10);
- assertThat(a.subject).doesNotContainMatch("automerger");
- assertThat(aPrime.subject).isEqualTo("[automerger] test commit am: " + shortASha);
+ assertThat(a.subject).doesNotContainMatch(tag);
+ assertThat(aPrime.subject).isEqualTo("[" + tag + "] test commit am: " + shortASha);
}
@Test
- public void testChangeStack_rebaseAfterUpload() throws Exception {
+ public void testChangeStack() throws Exception {
+ changeStack(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testChangeStackCherryPickMode() throws Exception {
+ changeStack(ChangeMode.CHERRY_PICK);
+ }
+
+ private void changeStack_rebaseAfterUpload(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Save initial ref at HEAD
ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
@@ -302,7 +340,7 @@
// Project name is scoped by test, so we need to get it from our initial change
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
- pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
+ pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
@@ -352,12 +390,36 @@
String bAfterRebaseFirstParent = getParent(bAfterRebase, 0);
assertThat(bAfterRebaseFirstParent).isEqualTo(a.currentRevision);
+ // In cherry-pick mode, we expect original bPrime to be removed, and a new bPrime to
+ // be created.
+ if(changeMode == ChangeMode.CHERRY_PICK){
+ assertThat(gApi.changes().id(bPrime.id).get().status).isEqualTo(ChangeStatus.ABANDONED);
+ changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic() + " status:open")
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(4);
+ sortedChanges = sortedChanges(changesInTopic);
+ bPrime = sortedChanges.get(1);
+ }
+
ChangeInfo bPrimeAfterRebase = gApi.changes().id(bPrime.changeId).get();
String bPrimeAfterRebaseFirstParent = getParent(bPrimeAfterRebase, 0);
assertThat(bPrimeAfterRebaseFirstParent).isEqualTo(aPrime.currentRevision);
}
@Test
+ public void testChangeStack_rebaseAfterUpload() throws Exception {
+ changeStack_rebaseAfterUpload(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testChangeStack_rebaseAfterUploadCherryPickMode() throws Exception {
+ changeStack_rebaseAfterUpload(ChangeMode.CHERRY_PICK);
+ }
+
+ @Test
public void testBlankMerge() throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
@@ -368,7 +430,7 @@
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
createBranch(BranchNameKey.create(projectName, "ds_two"));
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", ChangeMode.MERGE);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId(), "DO NOT MERGE subject", "filename", "content");
result.assertOkStatus();
@@ -415,6 +477,52 @@
}
@Test
+ public void testBlankMergeCherryPickMode() throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(
+ testRepo, "master", "DO NOT MERGE subject", "filename", "content", "testtopic");
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", ChangeMode.CHERRY_PICK);
+ // After we upload our config, we upload a new patchset to create the downstreams
+ amendChange(result.getChangeId(), "DO NOT MERGE subject", "filename", "content");
+ result.assertOkStatus();
+
+ ChangeApi change = gApi.changes().id(result.getChangeId());
+ BinaryResult content = change.current().file("filename").content();
+
+ List<ChangeInfo> changesInTopic =
+ gApi.changes().query("topic: " + change.topic()).withOption(CURRENT_REVISION).get();
+ assertThat(changesInTopic).hasSize(2);
+
+ List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+
+ ChangeInfo dsTwoChangeInfo = sortedChanges.get(0);
+ assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two");
+ assertAutomergerChangeCreatedMessage(dsTwoChangeInfo.id);
+ // It should not skip ds_two, since it is marked with mergeAll: true
+ ChangeApi dsTwoChange = gApi.changes().id(dsTwoChangeInfo._number);
+ assertThat(dsTwoChange.get().subject).doesNotContain("skipped:");
+ BinaryResult dsTwoContent = dsTwoChange.current().file("filename").content();
+ assertThat(dsTwoContent.asString()).isEqualTo(content.asString());
+
+ ChangeInfo masterChangeInfo = sortedChanges.get(1);
+ assertCodeReviewMissing(masterChangeInfo.id);
+ assertThat(masterChangeInfo.branch).isEqualTo("master");
+
+ // Ensure that commit subjects are correct
+ String masterSubject = masterChangeInfo.subject;
+ String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
+ assertThat(masterChangeInfo.subject).doesNotContainMatch("autocherry");
+ assertThat(dsTwoChangeInfo.subject)
+ .isEqualTo("[autocherry] " + masterSubject + " am: " + shortMasterSha);
+ }
+
+ @Test
public void testAlwaysBlankMerge() throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
@@ -430,7 +538,7 @@
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
createBranch(BranchNameKey.create(projectName, "ds_two"));
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", ChangeMode.MERGE);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId(), "DO NOT MERGE ANYWHERE subject", "filename", "content");
result.assertOkStatus();
@@ -475,7 +583,36 @@
}
@Test
- public void testDownstreamMergeConflict() throws Exception {
+ public void testAlwaysBlankMergeCherryPickMode() throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(
+ testRepo,
+ "master",
+ "DO NOT MERGE ANYWHERE subject",
+ "filename",
+ "content",
+ "testtopic");
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", ChangeMode.CHERRY_PICK);
+ // After we upload our config, we upload a new patchset to create the downstreams
+ amendChange(result.getChangeId(), "DO NOT MERGE ANYWHERE subject", "filename", "content");
+ result.assertOkStatus();
+
+ ChangeApi change = gApi.changes().id(result.getChangeId());
+ List<ChangeInfo> changesInTopic =
+ gApi.changes().query("topic: " + change.topic()).withOption(CURRENT_REVISION).get();
+ assertThat(changesInTopic).hasSize(1);
+ assertThat(changesInTopic.get(0).hashtags.size()).isEqualTo(2);
+ assertThat(changesInTopic.get(0).hashtags.contains("am_skip_ds_one")).isEqualTo(true);
+ assertThat(changesInTopic.get(0).hashtags.contains("am_skip_ds_two")).isEqualTo(true);
+ }
+
+ private void downstreamMergeConflict(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
// Create initial change
@@ -496,7 +633,7 @@
merge(ds1Result);
// Reset to allow our merge conflict to come
testRepo.reset(initial);
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", changeMode);
// After we upload our config, we upload a new change to create the downstreams
PushOneCommit.Result masterResult =
pushFactory
@@ -532,13 +669,23 @@
// Ensure that commit subjects are correct
String masterSubject = masterChangeInfo.subject;
String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
+ String tag = getTag(changeMode);
assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger");
assertThat(dsTwoChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
}
@Test
- public void testRestrictedVotePermissions() throws Exception {
+ public void testDownstreamMergeConflict() throws Exception {
+ downstreamMergeConflict(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testDownstreamMergeConflictCherryPickMode() throws Exception {
+ downstreamMergeConflict(ChangeMode.CHERRY_PICK);
+ }
+
+ private void restrictedVotePermissions(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
// Create initial change
@@ -559,7 +706,7 @@
merge(ds1Result);
// Reset to allow our merge conflict to come
testRepo.reset(initial);
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", changeMode);
// Block Code Review label to test restrictions
projectOperations
@@ -606,15 +753,25 @@
assertThat(messages).contains("Patch Set 1:\n\nMerge conflict found on ds_one");
// Ensure that commit subjects are correct
+ String tag = getTag(changeMode);
String masterSubject = masterChangeInfo.subject;
String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger");
assertThat(dsTwoChangeInfo.subject)
- .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha);
+ .isEqualTo("[" + tag + "] " + masterSubject + " am: " + shortMasterSha);
}
@Test
- public void testTopicEditedListener() throws Exception {
+ public void testRestrictedVotePermissions() throws Exception {
+ restrictedVotePermissions(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testRestrictedVotePermissionsCherryPickMode() throws Exception {
+ restrictedVotePermissions(ChangeMode.CHERRY_PICK);
+ }
+
+ private void topicEditedListener(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
PushOneCommit.Result result =
@@ -623,12 +780,47 @@
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
createBranch(BranchNameKey.create(projectName, "ds_two"));
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
- gApi.changes().id(result.getChangeId()).topic("multiple words");
- gApi.changes().id(result.getChangeId()).topic("singlewordagain");
+ gApi.changes().id(result.getChangeId()).topic(name("multiple words"));
+ gApi.changes().id(result.getChangeId()).topic(name("singlewordagain"));
+ // Check that there are the correct number of changes in the topic
+ List<ChangeInfo> changesInTopic =
+ gApi.changes()
+ .query("topic:\"" + gApi.changes().id(result.getChangeId()).topic() + "\"")
+ .get();
+ assertThat(changesInTopic).hasSize(3);
+ // +2 and submit
+ merge(result);
+ }
+
+ @Test
+ public void testTopicEditedListener() throws Exception {
+ topicEditedListener(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testTopicEditedListenerCherryPickMode() throws Exception {
+ topicEditedListener(ChangeMode.CHERRY_PICK);
+ }
+
+ private void topicEditedListener_withBraces(ChangeMode changeMode) throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two", changeMode);
+ // After we upload our config, we upload a new patchset to create the downstreams
+ amendChange(result.getChangeId());
+ result.assertOkStatus();
+ gApi.changes().id(result.getChangeId()).topic(name("multiple words"));
+ gApi.changes().id(result.getChangeId()).topic(name("with{braces}inside"));
// Check that there are the correct number of changes in the topic
List<ChangeInfo> changesInTopic =
gApi.changes()
@@ -641,32 +833,15 @@
@Test
public void testTopicEditedListener_withBraces() throws Exception {
- Project.NameKey manifestNameKey = defaultSetup();
- // Create initial change
- PushOneCommit.Result result =
- createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
- // Project name is scoped by test, so we need to get it from our initial change
- String projectName = result.getChange().project().get();
- createBranch(BranchNameKey.create(projectName, "ds_one"));
- createBranch(BranchNameKey.create(projectName, "ds_two"));
- pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
- // After we upload our config, we upload a new patchset to create the downstreams
- amendChange(result.getChangeId());
- result.assertOkStatus();
- gApi.changes().id(result.getChangeId()).topic("multiple words");
- gApi.changes().id(result.getChangeId()).topic("with{braces}inside");
- // Check that there are the correct number of changes in the topic
- List<ChangeInfo> changesInTopic =
- gApi.changes()
- .query("topic:\"" + gApi.changes().id(result.getChangeId()).topic() + "\"")
- .get();
- assertThat(changesInTopic).hasSize(3);
- // +2 and submit
- merge(result);
+ topicEditedListener_withBraces(ChangeMode.MERGE);
}
@Test
- public void testTopicEditedListener_branchWithBracesAndQuotes() throws Exception {
+ public void testTopicEditedListener_withBracesCherryPickMode() throws Exception {
+ topicEditedListener_withBraces(ChangeMode.CHERRY_PICK);
+ }
+
+ private void topicEditedListener_branchWithBracesAndQuotes(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
PushOneCommit.Result result =
@@ -680,7 +855,8 @@
manifestNameKey.get(),
projectName,
"branch{}braces",
- "branch\"quotes");
+ "branch\"quotes",
+ changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
@@ -695,7 +871,16 @@
}
@Test
- public void testTopicEditedListener_emptyTopic() throws Exception {
+ public void testTopicEditedListener_branchWithBracesAndQuotes() throws Exception {
+ topicEditedListener_branchWithBracesAndQuotes(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testTopicEditedListener_branchWithBracesAndQuotesCherryPickMode() throws Exception {
+ topicEditedListener_branchWithBracesAndQuotes(ChangeMode.CHERRY_PICK);
+ }
+
+ private void topicEditedListener_emptyTopic(ChangeMode changeMode) throws Exception {
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
PushOneCommit.Result result =
@@ -703,7 +888,7 @@
// Project name is scoped by test, so we need to get it from our initial change
String projectName = result.getChange().project().get();
createBranch(BranchNameKey.create(projectName, "ds_one"));
- pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
+ pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
amendChange(result.getChangeId());
result.assertOkStatus();
@@ -716,7 +901,16 @@
}
@Test
- public void testContextUser() throws Exception {
+ public void testTopicEditedListener_emptyTopic() throws Exception {
+ topicEditedListener_emptyTopic(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testTopicEditedListener_emptyTopicCherryPickMode() throws Exception {
+ topicEditedListener_emptyTopic(ChangeMode.CHERRY_PICK);
+ }
+
+ private void contextUser(ChangeMode changeMode) throws Exception {
// Branch flow for contextUser is master -> ds_one -> ds_two
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
@@ -746,9 +940,28 @@
.group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
.range(-2, 2))
.setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/ds_one"), true)
+ .add(
+ allow(Permission.EDIT_HASHTAGS)
+ .ref("refs/heads/*")
+ .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
+ )
.update();
+
+ if(changeMode == ChangeMode.CHERRY_PICK) {
+ // Grant EDIT_HASHTAGS permission
+ projectOperations
+ .project(projectNameKey)
+ .forUpdate()
+ .add(
+ allow(Permission.EDIT_HASHTAGS)
+ .ref("refs/heads/*")
+ .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
+ )
+ .update();
+ }
+
pushContextUserConfig(
- manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
+ manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString(), changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
PushOneCommit.Result result =
@@ -783,7 +996,16 @@
}
@Test
- public void testContextUser_downstreamHighestVote() throws Exception {
+ public void testContextUser() throws Exception {
+ contextUser(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testContextUserCherryPickMode() throws Exception {
+ contextUser(ChangeMode.CHERRY_PICK);
+ }
+
+ private void contextUser_downstreamHighestVote(ChangeMode changeMode) throws Exception {
// Branch flow for contextUser is master -> ds_one -> ds_two
Project.NameKey manifestNameKey = defaultSetup();
// Create initial change
@@ -813,8 +1035,22 @@
.group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
.range(-2, 2))
.update();
+
+ if(changeMode == ChangeMode.CHERRY_PICK) {
+ // Grant EDIT_HASHTAGS permission
+ projectOperations
+ .project(projectNameKey)
+ .forUpdate()
+ .add(
+ allow(Permission.EDIT_HASHTAGS)
+ .ref("refs/heads/*")
+ .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
+ )
+ .update();
+ }
+
pushContextUserConfig(
- manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
+ manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString(), changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
PushOneCommit.Result result =
@@ -858,7 +1094,16 @@
}
@Test
- public void testContextUser_mergeConflictOnDownstreamVotesOnTopLevel() throws Exception {
+ public void testContextUser_downstreamHighestVote() throws Exception {
+ contextUser_downstreamHighestVote(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testContextUser_downstreamHighestVoteCherryPickMode() throws Exception {
+ contextUser_downstreamHighestVote(ChangeMode.CHERRY_PICK);
+ }
+
+ private void contextUser_mergeConflictOnDownstreamVotesOnTopLevel(ChangeMode changeMode) throws Exception {
// Branch flow for contextUser is master -> ds_one -> ds_two
Project.NameKey manifestNameKey = defaultSetup();
ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
@@ -906,8 +1151,22 @@
.group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
.range(-2, 2))
.update();
+
+ if(changeMode == ChangeMode.CHERRY_PICK) {
+ // Grant EDIT_HASHTAGS permission
+ projectOperations
+ .project(projectNameKey)
+ .forUpdate()
+ .add(
+ allow(Permission.EDIT_HASHTAGS)
+ .ref("refs/heads/*")
+ .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id))
+ )
+ .update();
+ }
+
pushContextUserConfig(
- manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
+ manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString(), changeMode);
// After we upload our config, we upload a new patchset to create the downstreams
PushOneCommit.Result result =
@@ -919,15 +1178,195 @@
.query("topic: " + gApi.changes().id(result.getChangeId()).topic())
.withOptions(CURRENT_REVISION, CURRENT_COMMIT)
.get();
- // There should only be two, as ds_one to ds_two should be a merge conflict
- assertThat(changesInTopic).hasSize(2);
+ if(changeMode == ChangeMode.MERGE) {
+ // In merge mode, there should be a conflict
+ // There should only be two, as ds_one to ds_two should be a merge conflict
+ assertThat(changesInTopic).hasSize(2);
+
+ List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+
+ // Check that master is at Code-Review -2
+ ChangeInfo masterChangeInfo = sortedChanges.get(1);
+ assertThat(masterChangeInfo.branch).isEqualTo("master");
+ assertCodeReview(masterChangeInfo.id, -2, "autogenerated:MergeConflict");
+ } else {
+ // In cherry-pick mode, there will not be a conflict
+ assertThat(changesInTopic).hasSize(3);
+
+ // Check that master is at Code-Review 0
+ List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+ ChangeInfo masterChangeInfo = sortedChanges.get(2);
+ assertThat(masterChangeInfo.branch).isEqualTo("master");
+ assertCodeReviewMissing(masterChangeInfo.id);
+ }
+ }
+
+ @Test
+ public void testContextUser_mergeConflictOnDownstreamVotesOnTopLevel() throws Exception {
+ contextUser_mergeConflictOnDownstreamVotesOnTopLevel(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testContextUser_mergeConflictOnDownstreamVotesOnTopLevelCherryPickMode() throws Exception {
+ contextUser_mergeConflictOnDownstreamVotesOnTopLevel(ChangeMode.CHERRY_PICK);
+ }
+
+ private void abandon(ChangeMode changeMode) throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ createBranch(BranchNameKey.create(projectName, "ds_three"));
+ pushFourInChainConfig("automerger.config", manifestNameKey.get(), projectName, changeMode);
+ // After we upload our config, we upload a new patchset to create the downstreams
+ amendChange(result.getChangeId());
+ result.assertOkStatus();
+ // Check that there are the correct number of changes in the topic
+ List<ChangeInfo> changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(4);
List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+ ChangeInfo masterChangeInfo = sortedChanges.get(3);
- // Check that master is at Code-Review -2
- ChangeInfo masterChangeInfo = sortedChanges.get(1);
- assertThat(masterChangeInfo.branch).isEqualTo("master");
+ // All changes should be abandoned after the head of the chain is abandoned.
+ gApi.changes().id(masterChangeInfo.id).abandon();
+ changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic() + " status:open")
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(0);
+ }
+
+ @Test
+ public void testAbandon() throws Exception {
+ abandon(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testAbandonCherryPickMode() throws Exception {
+ abandon(ChangeMode.CHERRY_PICK);
+ }
+
+ private void multiAmend(ChangeMode changeMode) throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ createBranch(BranchNameKey.create(projectName, "ds_three"));
+ pushFourInChainConfig("automerger.config", manifestNameKey.get(), projectName, changeMode);
+ // After we upload our config, we upload a new patchset to create the downstreams
+ amendChange(result.getChangeId());
+ result.assertOkStatus();
+ // Check that there are the correct number of changes in the topic
+ List<ChangeInfo> changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(4);
+ List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+ ChangeInfo masterChangeInfo = sortedChanges.get(3);
+
+ // Amend the change again.
+ // Both merge and cherry-pick mode should see 4 open changes.
+ amendChange(result.getChangeId());
+ changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic() + " status:open")
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(4);
+
+ // Cherry-pick mode should see 3 abandoned changes.
+ changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic() + " status:abandoned")
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+
+ if(changeMode == ChangeMode.CHERRY_PICK) {
+ assertThat(changesInTopic).hasSize(3);
+ } else {
+ assertThat(changesInTopic).hasSize(0);
+ }
+ }
+
+ @Test
+ public void testMultiAmend() throws Exception {
+ multiAmend(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testMultiAmendCherryPick() throws Exception {
+ multiAmend(ChangeMode.CHERRY_PICK);
+ }
+
+ private void conflictFourInChainAtTail(ChangeMode changeMode) throws Exception {
+ Project.NameKey manifestNameKey = defaultSetup();
+ ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+ ObjectId masterChangeId = repo().exactRef("HEAD").getLeaf().getObjectId();
+ // Project name is scoped by test, so we need to get it from our initial change
+ String projectName = result.getChange().project().get();
+ createBranch(BranchNameKey.create(projectName, "ds_one"));
+ createBranch(BranchNameKey.create(projectName, "ds_two"));
+ createBranch(BranchNameKey.create(projectName, "ds_three"));
+
+ // Reset to create a sibling
+ testRepo.reset(initial);
+ // Create an edit in ds_three that will have a conflict
+ PushOneCommit.Result ds3Result =
+ createChange(
+ testRepo, "ds_three", "subject", "filename", "conflicting", "randtopic");
+ ds3Result.assertOkStatus();
+ merge(ds3Result);
+
+ pushFourInChainConfig("automerger.config", manifestNameKey.get(), projectName, changeMode);
+ // After we upload our config, we upload a new patchset to create the downstreams and force conflict
+ testRepo.reset(masterChangeId);
+ amendChange(result.getChangeId());
+ result.assertOkStatus();
+ // Check that there are the correct number of changes in the topic.
+ List<ChangeInfo> changesInTopic =
+ gApi.changes()
+ .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
+ .withOptions(ALL_REVISIONS, CURRENT_COMMIT)
+ .get();
+ assertThat(changesInTopic).hasSize(3);
+ List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+ ChangeInfo dsOneChangeInfo = sortedChanges.get(0);
+ ChangeInfo dsTwoChangeInfo = sortedChanges.get(1);
+ ChangeInfo masterChangeInfo = sortedChanges.get(2);
+
+ // All 3 should have -2.
assertCodeReview(masterChangeInfo.id, -2, "autogenerated:MergeConflict");
+ assertCodeReview(dsOneChangeInfo.id, -2, "autogenerated:Automerger");
+ assertCodeReview(dsTwoChangeInfo.id, -2, "autogenerated:Automerger");
+ }
+
+ @Test
+ public void testConflictFourInChainAtTail() throws Exception {
+ conflictFourInChainAtTail(ChangeMode.MERGE);
+ }
+
+ @Test
+ public void testConflictFourInChainAtTailCherryPickMode() throws Exception {
+ conflictFourInChainAtTail(ChangeMode.CHERRY_PICK);
}
private Project.NameKey defaultSetup() throws Exception {
@@ -976,26 +1415,29 @@
}
private void pushSimpleConfig(
- String resourceName, String manifestName, String project, String branch1) throws Exception {
+ String resourceName, String manifestName, String project, String branch1, ChangeMode changeMode) throws Exception {
List<ConfigOption> options = new ArrayList<>();
options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+ options.add(new ConfigOption("global", null, "cherryPickMode", cherryPickMode(changeMode)));
options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project));
pushConfig(options, resourceName);
}
private void pushDefaultConfig(
- String resourceName, String manifestName, String project, String branch1, String branch2)
+ String resourceName, String manifestName, String project, String branch1, String branch2, ChangeMode changeMode)
throws Exception {
List<ConfigOption> options = new ArrayList<>();
options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+ options.add(new ConfigOption("global", null, "cherryPickMode", cherryPickMode(changeMode)));
options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project));
options.add(new ConfigOption("automerger", "master:" + branch2, "setProjects", project));
pushConfig(options, resourceName);
}
- private void pushDiamondConfig(String manifestName, String project) throws Exception {
+ private void pushDiamondConfig(String manifestName, String project, ChangeMode changeMode) throws Exception {
List<ConfigOption> options = new ArrayList<>();
options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+ options.add(new ConfigOption("global", null, "cherryPickMode", cherryPickMode(changeMode)));
options.add(new ConfigOption("automerger", "master:left", "setProjects", project));
options.add(new ConfigOption("automerger", "master:right", "setProjects", project));
options.add(new ConfigOption("automerger", "left:bottom", "setProjects", project));
@@ -1003,16 +1445,29 @@
pushConfig(options, "diamond.config");
}
- private void pushContextUserConfig(String manifestName, String project, String contextUserId)
+ private void pushContextUserConfig(String manifestName, String project, String contextUserId, ChangeMode changeMode)
throws Exception {
List<ConfigOption> options = new ArrayList<>();
options.add(new ConfigOption("global", null, "manifestProject", manifestName));
options.add(new ConfigOption("global", null, "contextUserId", contextUserId));
+ options.add(new ConfigOption("global", null, "cherryPickMode", cherryPickMode(changeMode)));
options.add(new ConfigOption("automerger", "master:ds_one", "setProjects", project));
options.add(new ConfigOption("automerger", "ds_one:ds_two", "setProjects", project));
pushConfig(options, "context_user.config");
}
+ private void pushFourInChainConfig(
+ String resourceName, String manifestName, String project, ChangeMode changeMode)
+ throws Exception {
+ List<ConfigOption> options = new ArrayList<>();
+ options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+ options.add(new ConfigOption("global", null, "cherryPickMode", cherryPickMode(changeMode)));
+ options.add(new ConfigOption("automerger", "master:" + "ds_one", "setProjects", project));
+ options.add(new ConfigOption("automerger", "ds_one:" + "ds_two", "setProjects", project));
+ options.add(new ConfigOption("automerger", "ds_two:" + "ds_three", "setProjects", project));
+ pushConfig(options, resourceName);
+ }
+
private Optional<ApprovalInfo> getCodeReview(String id) throws RestApiException {
List<ApprovalInfo> approvals =
gApi.changes().id(id).get(DETAILED_LABELS).labels.get("Code-Review").all;
@@ -1055,4 +1510,20 @@
public String getParent(ChangeInfo info, int number) {
return info.revisions.get(info.currentRevision).commit.parents.get(number).commit;
}
+ private String getCherryPickFrom(ChangeInfo info){
+ try {
+ return gApi.changes().id(info.cherryPickOfChange).get().currentRevision;
+ } catch (RestApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getTag(ChangeMode changeMode){
+ return changeMode == ChangeMode.CHERRY_PICK ? "autocherry" : "automerger";
+ }
+
+ private String cherryPickMode(ChangeMode changeMode){
+ return changeMode == ChangeMode.CHERRY_PICK ? "True" : "False";
+ }
+
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
index c5b37ff..9519e14 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
@@ -24,6 +24,7 @@
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -33,7 +34,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
@@ -43,7 +46,7 @@
name = "automerger",
sysModule = "com.googlesource.gerrit.plugins.automerger.AutomergerModule")
public class MergeValidatorIT extends LightweightPluginDaemonTest {
- private void pushConfig(String resourceName, String project, String branch) throws Exception {
+ private void pushConfig(String resourceName, String project, String branch, ChangeMode changeMode) throws Exception {
TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
allProjectRepo.reset("config");
@@ -51,8 +54,10 @@
String resourceString =
CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8));
+ String cherryPickMode = (changeMode == ChangeMode.MERGE) ? "False" : "True";
Config cfg = new Config();
cfg.fromText(resourceString);
+ cfg.setString("global", null, "cherryPickMode", cherryPickMode);
// Update manifest project path to the result of createProject(resourceName), since it is
// scoped to the test method
cfg.setString("automerger", "master:" + branch, "setProjects", project);
@@ -63,6 +68,10 @@
}
}
+ private void pushConfig(String resourceName, String project, String branch) throws Exception {
+ pushConfig(resourceName, project, branch, ChangeMode.MERGE);
+ }
+
@Test
public void testNoMissingDownstreamMerges() throws Exception {
// Create initial change
@@ -197,6 +206,25 @@
+ ": there is no ds_one");
}
+ @Test
+ public void testSkippedCherryPick() throws Exception {
+ // Create initial change
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+
+ // Add the skip hashtag.
+ Set set = new HashSet<>();
+ set.add("am_skip_ds_one");
+ HashtagsInput input = new HashtagsInput(set);
+ gApi.changes().id(result.getChangeId()).setHashtags(input);
+
+ pushConfig("automerger.config", result.getChange().project().get(), "ds_one", ChangeMode.CHERRY_PICK);
+ result.assertOkStatus();
+
+ // Should be able to merge successfully.
+ merge(result);
+ }
+
private List<ChangeInfo> sortedChanges(List<ChangeInfo> changes) {
List<ChangeInfo> listCopy = new ArrayList<>(changes);
Collections.sort(
diff --git a/web/automerger.ts b/web/automerger.ts
index e43f1f6..91ea27e 100644
--- a/web/automerger.ts
+++ b/web/automerger.ts
@@ -71,6 +71,8 @@
private downstreamConfigMap: ConfigMap = {};
+ private mergeMode: string = "";
+
readonly plugin: PluginApi;
constructor(readonly p: PluginApi) {
@@ -145,7 +147,7 @@
}
};
const button = document.createElement('gr-button');
- button.appendChild(document.createTextNode('Merge'));
+ button.appendChild(document.createTextNode(this.mergeMode));
button.addEventListener('click', onClick);
return button;
}
@@ -193,9 +195,18 @@
});
}
+ private getMode() {
+ const url = `/config/server/automerger~automerge-mode`;
+ this.plugin.restApi().get<string>(url).then(resp => {
+ this.mergeMode = resp;
+ });
+ }
+
onShowChange(change: ChangeInfo) {
this.change = change;
this.downstreamConfigMap = {};
+ this.mergeMode = "";
+ this.getMode();
this.getDownstreamConfigMap();
}