blob: d36fa1fe2005ea19b2dd66e794baf4f583c5348c [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.googlesource.gerrit.plugins.automerger;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Joiner;
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.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.ApprovalInfo;
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.common.RevisionInfo;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.DraftPublishedListener;
import com.google.gerrit.extensions.events.RevisionCreatedListener;
import com.google.gerrit.extensions.events.TopicEditedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DownstreamCreator will receive an event on an uploaded, published, or restored patchset, and
* upload a merge of the original patchset downstream, as determined by the configuration file. When
* a topic or vote is changed on a patchset, or a change is abandoned, all downstream patchsets will
* be modified as well.
*/
public class DownstreamCreator
implements ChangeAbandonedListener,
ChangeRestoredListener,
CommentAddedListener,
DraftPublishedListener,
RevisionCreatedListener,
TopicEditedListener {
private static final Logger log = LoggerFactory.getLogger(DownstreamCreator.class);
private static final String AUTOMERGER_TAG = "autogenerated:Automerger";
private static final String MERGE_CONFLICT_TAG = "autogenerated:MergeConflict";
private static final String SUBJECT_PREFIX = "[automerger]";
protected GerritApi gApi;
protected ConfigLoader config;
@Inject
public DownstreamCreator(GerritApi gApi, ConfigLoader config) {
this.gApi = gApi;
this.config = config;
}
/**
* Abandons downstream changes if a change is abandoned.
*
* @param event Event we are listening to.
*/
@Override
public void onChangeAbandoned(ChangeAbandonedListener.Event event) {
ChangeInfo change = event.getChange();
String revision = event.getRevision().commit.commit;
log.debug("Detected revision {} abandoned on {}.", revision, change.project);
try {
abandonDownstream(change, revision);
} catch (ConfigInvalidException e) {
log.error("Automerger plugin failed onChangeAbandoned for {}", change.id, e);
}
}
/**
* Updates downstream topics if a change has its topic modified.
*
* @param event Event we are listening to.
*/
@Override
public void onTopicEdited(TopicEditedListener.Event event) {
ChangeInfo eventChange = event.getChange();
// We have to re-query for this in order to include the current revision
ChangeInfo change;
try {
change =
gApi.changes()
.id(eventChange._number)
.get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
} catch (RestApiException e) {
log.error("Automerger could not get change with current revision for onTopicEdited: ", e);
return;
}
String oldTopic = event.getOldTopic();
String revision = change.currentRevision;
Set<String> downstreamBranches;
try {
downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
} catch (RestApiException | IOException | ConfigInvalidException e) {
log.error("Failed to edit downstream topics of {}", change.id, e);
return;
}
if (downstreamBranches.isEmpty()) {
log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
return;
}
// If change is empty, prevent someone breaking topic.
if (isNullOrEmpty(change.topic)) {
try {
gApi.changes().id(change._number).topic(oldTopic);
ReviewInput reviewInput = new ReviewInput();
reviewInput.message(
"Automerger prevented the topic from changing. Topic can only be modified on "
+ "non-automerger-created CLs to a non-empty value.");
reviewInput.notify = NotifyHandling.NONE;
gApi.changes().id(change._number).revision(change.currentRevision).review(reviewInput);
} catch (RestApiException e) {
log.error("Failed to prevent setting empty topic for automerger plugin.", e);
}
} else {
for (String downstreamBranch : downstreamBranches) {
try {
List<Integer> existingDownstream =
getExistingMergesOnBranch(revision, oldTopic, downstreamBranch);
for (Integer changeNumber : existingDownstream) {
log.debug("Setting topic {} on {}", change.topic, changeNumber);
gApi.changes().id(changeNumber).topic(change.topic);
}
} catch (RestApiException | InvalidQueryParameterException e) {
log.error("Failed to edit downstream topics of {}", change.id, e);
}
}
}
}
/**
* Updates downstream votes for a change each time a comment is made.
*
* @param event Event we are listening to.
*/
@Override
public void onCommentAdded(CommentAddedListener.Event event) {
RevisionInfo eventRevision = event.getRevision();
if (!eventRevision.isCurrent) {
log.info(
"Not updating downstream votes since revision {} is not current.", eventRevision._number);
return;
}
ChangeInfo change = event.getChange();
String revision = change.currentRevision;
Set<String> downstreamBranches;
try {
downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
} catch (RestApiException | IOException | ConfigInvalidException e) {
log.error("Failed to update downstream votes of {}", change.id, e);
return;
}
if (downstreamBranches.isEmpty()) {
log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
return;
}
Map<String, ApprovalInfo> approvals = event.getApprovals();
for (String downstreamBranch : downstreamBranches) {
try {
List<Integer> existingDownstream =
getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
for (Integer changeNumber : existingDownstream) {
ChangeInfo downstreamChange =
gApi.changes().id(changeNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
for (Map.Entry<String, ApprovalInfo> label : approvals.entrySet()) {
updateVote(downstreamChange, label.getKey(), label.getValue().value.shortValue());
}
}
} catch (RestApiException | InvalidQueryParameterException e) {
log.error("Exception when updating downstream votes of {}", change.id, e);
}
}
}
/**
* Automerges changes downstream if a change is restored.
*
* @param event Event we are listening to.
*/
@Override
public void onChangeRestored(ChangeRestoredListener.Event event) {
ChangeInfo change = event.getChange();
try {
automergeChanges(change, event.getRevision());
} catch (RestApiException
| IOException
| ConfigInvalidException
| InvalidQueryParameterException e) {
log.error("Automerger plugin failed onChangeRestored for {}", change.id, e);
}
}
/**
* Automerges changes downstream if a draft is published.
*
* @param event Event we are listening to.
*/
@Override
public void onDraftPublished(DraftPublishedListener.Event event) {
ChangeInfo change = event.getChange();
try {
automergeChanges(change, event.getRevision());
} catch (RestApiException
| IOException
| ConfigInvalidException
| InvalidQueryParameterException e) {
log.error("Automerger plugin failed onDraftPublished for {}", change.id, e);
}
}
/**
* Automerges changes downstream if a revision is created.
*
* @param event Event we are listening to.
*/
@Override
public void onRevisionCreated(RevisionCreatedListener.Event event) {
ChangeInfo change = event.getChange();
try {
automergeChanges(change, event.getRevision());
} catch (RestApiException
| IOException
| ConfigInvalidException
| InvalidQueryParameterException e) {
log.error("Automerger plugin failed onRevisionCreated for {}", change.id, e);
}
}
/**
* Creates merges 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.
* @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.
*/
public void createMergesAndHandleConflicts(MultipleDownstreamMergeInput mdsMergeInput)
throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
ReviewInput reviewInput = new ReviewInput();
Map<String, Short> labels = new HashMap<String, Short>();
try {
createDownstreamMerges(mdsMergeInput);
reviewInput.message =
"Automerging to "
+ Joiner.on(", ").join(mdsMergeInput.dsBranchMap.keySet())
+ " succeeded!";
reviewInput.notify = NotifyHandling.NONE;
} catch (FailedMergeException e) {
reviewInput.message = e.getDisplayString();
reviewInput.notify = NotifyHandling.ALL;
reviewInput.tag = MERGE_CONFLICT_TAG;
// Vote minAutomergeVote if we hit a conflict.
if (!config.minAutomergeVoteDisabled()) {
labels.put(config.getAutomergeLabel(), config.getMinAutomergeVote());
}
}
reviewInput.labels = labels;
// if this fails, i.e. -2 is restricted, catch it and still post message without a vote.
try {
gApi.changes()
.id(mdsMergeInput.changeNumber)
.revision(mdsMergeInput.currentRevision)
.review(reviewInput);
} catch (AuthException e) {
reviewInput.labels = null;
gApi.changes()
.id(mdsMergeInput.changeNumber)
.revision(mdsMergeInput.currentRevision)
.review(reviewInput);
}
}
/**
* Creates merge downstream.
*
* @param mdsMergeInput Input containing the downstream branch map and source change ID.
* @throws RestApiException Throws if we fail a REST API call.
* @throws FailedMergeException Throws if we get a merge conflict when merging downstream.
* @throws ConfigInvalidException Throws if we get a malformed config file
* @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
*/
public void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput)
throws RestApiException, FailedMergeException, ConfigInvalidException,
InvalidQueryParameterException {
// Map from branch to error message
Map<String, String> failedMergeBranchMap = new TreeMap<String, String>();
List<Integer> existingDownstream;
for (String downstreamBranch : mdsMergeInput.dsBranchMap.keySet()) {
// If there are existing downstream merges, update them
// Otherwise, create them.
boolean createDownstreams = true;
if (mdsMergeInput.obsoleteRevision != null) {
existingDownstream =
getExistingMergesOnBranch(
mdsMergeInput.obsoleteRevision, mdsMergeInput.topic, downstreamBranch);
if (!existingDownstream.isEmpty()) {
log.debug(
"Attempting to update downstream merge of {} on branch {}",
mdsMergeInput.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));
createDownstreams = false;
} catch (MergeConflictException e) {
failedMergeBranchMap.put(downstreamBranch, e.getMessage());
log.debug("Abandoning existing, obsolete {} due to merge conflict.", dsChangeNumber);
abandonChange(dsChangeNumber);
}
}
}
}
if (createDownstreams) {
log.debug(
"Attempting to create downstream merge of {} on branch {}",
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);
try {
createSingleDownstreamMerge(sdsMergeInput);
} catch (MergeConflictException e) {
failedMergeBranchMap.put(downstreamBranch, e.getMessage());
}
}
}
if (!failedMergeBranchMap.isEmpty()) {
throw new FailedMergeException(
failedMergeBranchMap,
mdsMergeInput.currentRevision,
config.getHostName(),
mdsMergeInput.project,
mdsMergeInput.changeNumber,
mdsMergeInput.patchsetNumber,
config.getConflictMessage(),
mdsMergeInput.topic);
}
}
/**
* Get change IDs 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.
* @return List of change numbers that are downstream of the given branch.
* @throws RestApiException Throws when we fail a REST API call.
* @throws InvalidQueryParameterException Throws when we try to add an invalid value to the query.
*/
public List<Integer> getExistingMergesOnBranch(
String upstreamRevision, String topic, String downstreamBranch)
throws RestApiException, InvalidQueryParameterException {
List<Integer> downstreamChangeNumbers = new ArrayList<Integer>();
List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch);
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);
}
}
}
return downstreamChangeNumbers;
}
/**
* Create a single downstream merge.
*
* @param sdsMergeInput Input containing metadata for the merge.
* @throws RestApiException
* @throws ConfigInvalidException
* @throws InvalidQueryParameterException
*/
public void createSingleDownstreamMerge(SingleDownstreamMergeInput sdsMergeInput)
throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
String currentTopic = getOrSetTopic(sdsMergeInput.changeNumber, sdsMergeInput.topic);
if (isAlreadyMerged(sdsMergeInput, currentTopic)) {
log.info(
"Commit {} already merged into {}, not automerging again.",
sdsMergeInput.currentRevision,
sdsMergeInput.downstreamBranch);
return;
}
MergeInput mergeInput = new MergeInput();
mergeInput.source = sdsMergeInput.currentRevision;
mergeInput.strategy = "recursive";
log.debug("Creating downstream merge for {}", 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);
log.debug(
"Skipping merge for {} to {}",
sdsMergeInput.currentRevision,
sdsMergeInput.downstreamBranch);
}
ChangeApi downstreamChange = gApi.changes().create(downstreamChangeInput);
// Vote maxAutomergeVote on the change so we know it was successful.
if (!config.maxAutomergeVoteDisabled()) {
updateVote(downstreamChange.get(), config.getAutomergeLabel(), config.getMaxAutomergeVote());
}
}
public String getOrSetTopic(int sourceId, String topic) throws RestApiException {
if (isNullOrEmpty(topic)) {
topic = "am-" + UUID.randomUUID();
log.debug("Setting original change {} topic to {}", sourceId, topic);
gApi.changes().id(sourceId).topic(topic);
}
return topic;
}
/**
* 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()) {
log.info("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);
}
}
return null;
}
private void automergeChanges(ChangeInfo change, RevisionInfo revisionInfo)
throws RestApiException, IOException, ConfigInvalidException, InvalidQueryParameterException {
if (revisionInfo.draft != null && revisionInfo.draft) {
log.debug("Patchset {} is draft change, ignoring.", revisionInfo.commit.commit);
return;
}
String currentRevision = revisionInfo.commit.commit;
log.debug(
"Handling patchsetevent with change id {} and revision {}", change.id, currentRevision);
Set<String> downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
if (downstreamBranches.isEmpty()) {
log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
return;
}
// Map whether or not we should merge it or skip it for each downstream
Map<String, Boolean> dsBranchMap = new HashMap<String, Boolean>();
for (String downstreamBranch : downstreamBranches) {
boolean isSkipMerge = config.isSkipMerge(change.branch, downstreamBranch, change.subject);
dsBranchMap.put(downstreamBranch, !isSkipMerge);
}
log.debug("Automerging change {} from branch {}", change.id, change.branch);
ChangeApi currentChange = gApi.changes().id(change._number);
String previousRevision = getPreviousRevision(currentChange, revisionInfo._number);
MultipleDownstreamMergeInput mdsMergeInput = new MultipleDownstreamMergeInput();
mdsMergeInput.dsBranchMap = dsBranchMap;
mdsMergeInput.changeNumber = change._number;
mdsMergeInput.patchsetNumber = revisionInfo._number;
mdsMergeInput.project = change.project;
mdsMergeInput.topic = getOrSetTopic(change._number, change.topic);
mdsMergeInput.subject = change.subject;
mdsMergeInput.obsoleteRevision = previousRevision;
mdsMergeInput.currentRevision = currentRevision;
createMergesAndHandleConflicts(mdsMergeInput);
}
private void abandonDownstream(ChangeInfo change, String revision) throws ConfigInvalidException {
try {
Set<String> downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
if (downstreamBranches.isEmpty()) {
log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
return;
}
for (String downstreamBranch : downstreamBranches) {
List<Integer> existingDownstream =
getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
log.debug("Abandoning existing downstreams: {}", existingDownstream);
for (Integer changeNumber : existingDownstream) {
abandonChange(changeNumber);
}
}
} catch (RestApiException | IOException | InvalidQueryParameterException e) {
log.error("Failed to abandon downstreams of {}", change.id, e);
}
}
private void updateVote(ChangeInfo change, String label, short vote) throws RestApiException {
log.debug("Giving {} for label {} to {}", vote, label, change.id);
// Vote on all downstream branches unless merge conflict.
ReviewInput reviewInput = new ReviewInput();
Map<String, Short> labels = new HashMap<String, Short>();
labels.put(label, vote);
reviewInput.labels = labels;
reviewInput.notify = NotifyHandling.NONE;
reviewInput.tag = AUTOMERGER_TAG;
try {
gApi.changes().id(change.id).revision(change.currentRevision).review(reviewInput);
} catch (AuthException e) {
log.error("Automerger could not set label, but still continuing.", e);
}
}
private void updateDownstreamMerge(
String newParentRevision, String upstreamSubject, Integer sourceNum, boolean doMerge)
throws RestApiException, ConfigInvalidException {
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);
log.debug("Skipping merge for {} on {}", newParentRevision, sourceNum);
}
mergePatchSetInput.merge = mergeInput;
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);
}
ChangeInfo downstreamChange = originalChange.createMergePatchSet(mergePatchSetInput);
if (!config.maxAutomergeVoteDisabled()) {
updateVote(downstreamChange, config.getAutomergeLabel(), config.getMaxAutomergeVote());
}
}
private String getPreviousRevision(ChangeApi change, int currentPatchSetNumber)
throws RestApiException {
String previousRevision = null;
int maxPatchSetNum = 0;
if (currentPatchSetNumber > 1) {
// Get sha of patch set with highest number we can see
Map<String, RevisionInfo> revisionMap =
change.get(EnumSet.of(ListChangesOption.ALL_REVISIONS)).revisions;
for (Map.Entry<String, RevisionInfo> revisionEntry : revisionMap.entrySet()) {
int revisionPatchNumber = revisionEntry.getValue()._number;
if (revisionPatchNumber > maxPatchSetNum && revisionPatchNumber < currentPatchSetNumber) {
previousRevision = revisionEntry.getKey();
maxPatchSetNum = revisionPatchNumber;
}
}
}
return previousRevision;
}
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);
}
return parents;
}
private void abandonChange(Integer changeNumber) throws RestApiException {
log.debug("Abandoning change: {}", changeNumber);
AbandonInput abandonInput = new AbandonInput();
abandonInput.notify = NotifyHandling.NONE;
abandonInput.message = "Merge parent 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 List<ChangeInfo> getChangesInTopicAndBranch(String topic, String downstreamBranch)
throws InvalidQueryParameterException, RestApiException {
QueryBuilder queryBuilder = new QueryBuilder();
queryBuilder.addParameter("topic", topic);
queryBuilder.addParameter("branch", downstreamBranch);
queryBuilder.addParameter("status", "open");
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". [automerger] will not
* be repeated for changes with multiple downstreams.
*
* @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)) {
upstreamSubject = Joiner.on(" ").join(SUBJECT_PREFIX, upstreamSubject);
}
String denotationString = skipped ? "skipped:" : "am:";
return Joiner.on(" ")
.join(upstreamSubject, denotationString, upstreamRevision.substring(0, 10));
}
}