Rewrite MergeOp to use BatchUpdate
There are a number of major architectural changes here. At a high level, instances of SubmitStrategy no longer actually touch the repo or changes directly; the only public method is now: public void addOps(BatchUpdate bu, Set<CodeReviewCommit> toMerge) throws IntegrationException; Thus the main loop in MergeOp#integrateIntoHistory creates a submit strategy for each branch, adds its ops to a BatchUpdate, and then executes the update. Some of the validation steps (which are read-only) as well as submodule operations still happen outside the main BatchUpdate. The next major change is moving all of the post-submit-strategy validation and change updating logic out of MergeOp. This has been split across two other basic locations. The first location is SubmitStrategyOp, which is a common abstract base class of ops created by SubmitStrategy implementations. This contains the logic to update the branch and change status based on the actual meat of the submit strategy implementation. It handles things like updating refs, writing out ChangeMessages, firing hooks, etc. SubmitStrategy guarantees that there is exactly one SubmitStrategyOp per input CodeReviewCommit that gets merged. Implementations may create a smaller set of ops, for example a single FastForwardOp to merge the first few commits in a series. SubmitStrategy "fills in the gaps" and adds an ImplicitIntegrateOp to the update for each change that needs to be validated even if it was not set up initially by the SubmitStrategy. The second location for validation uses the newly added BatchUpdate.Listener interface. The SubmitStrategyListener implementation uses the problem map stored in the new CommitStatus class to abort the process after updating the repo or updating changes. It also does some checks that are more appropriate after running _all_ submit strategies in the batch, like checking the CommitMergeStatus bit on all the input CodeReviewCommits. One goal of this rewrite is to make fully implementing the notedb part of MergeOp more feasible, but apart from that, there are some behavioral and organizational differences resulting from this change that I believe are overall beneficial. First, the main feature of BatchUpdate is that it follows a well-defined order of operations: all ObjectInserters are flushed before any ref updates are written; all ref updates are written as a (per-repo) batch; and each change is updated in a transaction. Since the merge process does all of these things, it will receive these performance and consistency improvements as a result. Second, this change deletes several hundred lines from MergeOp, making it somewhat less of a kitchen sink. Instead most of the behavior is decomposed into batch update operations. There is some cognitive cost to this in terms of communicating between phases using shared state, but I hope that overall the decomposition is worth it. Change-Id: I1d9883a515729f9085709dd0ec85f9ba3c5a6152
This commit is contained in:
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.api.changes.SubmitInput;
|
||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||||
import com.google.gerrit.extensions.webui.UiAction;
|
||||
@@ -157,9 +158,8 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
|
||||
@Override
|
||||
public Output apply(RevisionResource rsrc, SubmitInput input)
|
||||
throws AuthException, ResourceConflictException,
|
||||
RepositoryNotFoundException, IOException, OrmException,
|
||||
UnprocessableEntityException {
|
||||
throws RestApiException, RepositoryNotFoundException, IOException,
|
||||
OrmException {
|
||||
input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
|
||||
if (input.onBehalfOf != null) {
|
||||
rsrc = onBehalfOf(rsrc, input);
|
||||
@@ -430,9 +430,8 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
|
||||
@Override
|
||||
public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
|
||||
throws AuthException, ResourceConflictException,
|
||||
RepositoryNotFoundException, IOException, OrmException,
|
||||
UnprocessableEntityException {
|
||||
throws RestApiException, RepositoryNotFoundException, IOException,
|
||||
OrmException {
|
||||
PatchSet ps = dbProvider.get().patchSets()
|
||||
.get(rsrc.getChange().currentPatchSetId());
|
||||
if (ps == null) {
|
||||
|
@@ -66,7 +66,7 @@ public class EmailMerge implements Runnable, RequestContext {
|
||||
this.submitter = submitter;
|
||||
}
|
||||
|
||||
void sendAsync() {
|
||||
public void sendAsync() {
|
||||
sendEmailsExecutor.submit(this);
|
||||
}
|
||||
|
||||
|
@@ -14,19 +14,19 @@
|
||||
|
||||
package com.google.gerrit.server.git;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
@@ -35,40 +35,29 @@ import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.hash.Hasher;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.gerrit.common.ChangeHooks;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.common.data.SubmitRecord;
|
||||
import com.google.gerrit.common.data.SubmitTypeRecord;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||
import com.google.gerrit.reviewdb.client.LabelId;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.ApprovalsUtil;
|
||||
import com.google.gerrit.server.ChangeMessagesUtil;
|
||||
import com.google.gerrit.server.ChangeUtil;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
|
||||
import com.google.gerrit.server.git.strategy.CommitMergeStatus;
|
||||
import com.google.gerrit.server.git.strategy.SubmitStrategy;
|
||||
import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
|
||||
import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
|
||||
import com.google.gerrit.server.git.validators.MergeValidationException;
|
||||
import com.google.gerrit.server.git.validators.MergeValidators;
|
||||
import com.google.gerrit.server.index.ChangeIndexer;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||
import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
||||
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||
@@ -84,7 +73,6 @@ import com.google.inject.Provider;
|
||||
|
||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.CommitBuilder;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectInserter;
|
||||
@@ -105,6 +93,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -127,12 +116,14 @@ import java.util.Set;
|
||||
public class MergeOp implements AutoCloseable {
|
||||
private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
|
||||
|
||||
private static class OpenRepo {
|
||||
private class OpenRepo {
|
||||
final Repository repo;
|
||||
final CodeReviewRevWalk rw;
|
||||
final RevFlag canMergeFlag;
|
||||
final ObjectInserter ins;
|
||||
|
||||
ProjectState project;
|
||||
BatchUpdate update;
|
||||
|
||||
private final Map<Branch.NameKey, OpenBranch> branches;
|
||||
|
||||
@@ -162,7 +153,18 @@ public class MergeOp implements AutoCloseable {
|
||||
return project.getProject().getNameKey();
|
||||
}
|
||||
|
||||
BatchUpdate getUpdate() {
|
||||
if (update == null) {
|
||||
update = batchUpdateFactory.create(db, getProjectName(), caller, ts);
|
||||
update.setRepository(repo, rw, ins);
|
||||
}
|
||||
return update;
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (update != null) {
|
||||
update.close();
|
||||
}
|
||||
ins.close();
|
||||
rw.close();
|
||||
repo.close();
|
||||
@@ -192,22 +194,39 @@ public class MergeOp implements AutoCloseable {
|
||||
throw new IntegrationException("Cannot open branch " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
CodeReviewCommit getCurrentTip() {
|
||||
return mergeTip != null ? mergeTip.getCurrentTip() : oldTip;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommitStatus {
|
||||
private final Map<Change.Id, CodeReviewCommit> commits = new HashMap<>();
|
||||
private final Multimap<Change.Id, String> problems =
|
||||
MultimapBuilder.treeKeys(
|
||||
private final ImmutableMap<Change.Id, ChangeData> changes;
|
||||
private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
|
||||
private final Map<Change.Id, CodeReviewCommit> commits;
|
||||
private final Multimap<Change.Id, String> problems;
|
||||
|
||||
private CommitStatus(ChangeSet cs) throws OrmException {
|
||||
changes = cs.changesById();
|
||||
ImmutableSetMultimap.Builder<Branch.NameKey, Change.Id> bb =
|
||||
ImmutableSetMultimap.builder();
|
||||
for (ChangeData cd : cs.changes()) {
|
||||
bb.put(cd.change().getDest(), cd.getId());
|
||||
}
|
||||
byBranch = bb.build();
|
||||
commits = new HashMap<>();
|
||||
problems = MultimapBuilder.treeKeys(
|
||||
Ordering.natural().onResultOf(new Function<Change.Id, Integer>() {
|
||||
@Override
|
||||
public Integer apply(Change.Id in) {
|
||||
return in.get();
|
||||
}
|
||||
})).arrayListValues(1).build();
|
||||
}
|
||||
|
||||
public ImmutableSet<Change.Id> getChangeIds() {
|
||||
return changes.keySet();
|
||||
}
|
||||
|
||||
public ImmutableSet<Change.Id> getChangeIds(Branch.NameKey branch) {
|
||||
return byBranch.get(branch);
|
||||
}
|
||||
|
||||
public CodeReviewCommit get(Change.Id changeId) {
|
||||
return commits.get(changeId);
|
||||
@@ -232,36 +251,70 @@ public class MergeOp implements AutoCloseable {
|
||||
problems.put(id, msg);
|
||||
}
|
||||
|
||||
boolean isOk() {
|
||||
public boolean isOk() {
|
||||
return problems.isEmpty();
|
||||
}
|
||||
|
||||
ImmutableMultimap<Change.Id, String> getProblems() {
|
||||
public ImmutableMultimap<Change.Id, String> getProblems() {
|
||||
return ImmutableMultimap.copyOf(problems);
|
||||
}
|
||||
|
||||
public List<SubmitRecord> getSubmitRecords(Change.Id id) {
|
||||
// Use the cached submit records from the original ChangeData in the input
|
||||
// ChangeSet, which were checked earlier in the integrate process. Even in
|
||||
// the case of a race where the submit records may have changed, it makes
|
||||
// more sense to store the original results of the submit rule evaluator
|
||||
// than to fail at this point.
|
||||
//
|
||||
// However, do NOT expose that ChangeData directly, as it is way out of
|
||||
// date by this point.
|
||||
ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id);
|
||||
return checkNotNull(cd.getSubmitRecords(),
|
||||
"getSubmitRecord only valid after submit rules are evalutated");
|
||||
}
|
||||
|
||||
public void maybeFailVerbose() throws ResourceConflictException {
|
||||
if (isOk()) {
|
||||
return;
|
||||
}
|
||||
String msg = "Failed to submit " + changes.size() + " change"
|
||||
+ (changes.size() > 1 ? "s" : "")
|
||||
+ " due to the following problems:\n";
|
||||
List<String> ps = new ArrayList<>(problems.keySet().size());
|
||||
for (Change.Id id : problems.keySet()) {
|
||||
ps.add("Change " + id + ": " + Joiner.on("; ").join(problems.get(id)));
|
||||
}
|
||||
throw new ResourceConflictException(msg + Joiner.on('\n').join(ps));
|
||||
}
|
||||
|
||||
public void maybeFail(String msgPrefix) throws ResourceConflictException {
|
||||
if (isOk()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder msg = new StringBuilder(msgPrefix).append(" of change");
|
||||
Set<Change.Id> ids = problems.keySet();
|
||||
if (ids.size() == 1) {
|
||||
msg.append(" ").append(ids.iterator().next());
|
||||
} else {
|
||||
msg.append("s ").append(Joiner.on(", ").join(ids));
|
||||
}
|
||||
throw new ResourceConflictException(msg.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private final AccountCache accountCache;
|
||||
private final ApprovalsUtil approvalsUtil;
|
||||
private final ChangeControl.GenericFactory changeControlFactory;
|
||||
private final ChangeHooks hooks;
|
||||
private final ChangeIndexer indexer;
|
||||
private final ChangeMessagesUtil cmUtil;
|
||||
private final ChangeUpdate.Factory updateFactory;
|
||||
private final GitReferenceUpdated gitRefUpdated;
|
||||
private final ChangeUpdate.Factory changeUpdateFactory;
|
||||
private final BatchUpdate.Factory batchUpdateFactory;
|
||||
private final GitRepositoryManager repoManager;
|
||||
private final IdentifiedUser.GenericFactory identifiedUserFactory;
|
||||
private final LabelNormalizer labelNormalizer;
|
||||
private final EmailMerge.Factory mergedSenderFactory;
|
||||
private final MergeSuperSet mergeSuperSet;
|
||||
private final MergeValidators.Factory mergeValidatorsFactory;
|
||||
private final PatchSetInfoFactory patchSetInfoFactory;
|
||||
private final ProjectCache projectCache;
|
||||
private final InternalChangeQuery internalChangeQuery;
|
||||
private final SubmitStrategyFactory submitStrategyFactory;
|
||||
private final Provider<SubmoduleOp> subOpProvider;
|
||||
private final TagCache tagCache;
|
||||
private final CommitStatus commits;
|
||||
|
||||
private final Map<Project.NameKey, OpenRepo> openRepos;
|
||||
|
||||
@@ -279,52 +332,38 @@ public class MergeOp implements AutoCloseable {
|
||||
private String submissionId;
|
||||
private IdentifiedUser caller;
|
||||
|
||||
private CommitStatus commits;
|
||||
private ReviewDb db;
|
||||
|
||||
@Inject
|
||||
MergeOp(AccountCache accountCache,
|
||||
ApprovalsUtil approvalsUtil,
|
||||
ChangeControl.GenericFactory changeControlFactory,
|
||||
ChangeHooks hooks,
|
||||
MergeOp(ChangeControl.GenericFactory changeControlFactory,
|
||||
ChangeIndexer indexer,
|
||||
ChangeMessagesUtil cmUtil,
|
||||
ChangeUpdate.Factory updateFactory,
|
||||
GitReferenceUpdated gitRefUpdated,
|
||||
ChangeUpdate.Factory changeUpdateFactory,
|
||||
BatchUpdate.Factory batchUpdateFactory,
|
||||
GitRepositoryManager repoManager,
|
||||
IdentifiedUser.GenericFactory identifiedUserFactory,
|
||||
LabelNormalizer labelNormalizer,
|
||||
EmailMerge.Factory mergedSenderFactory,
|
||||
MergeSuperSet mergeSuperSet,
|
||||
MergeValidators.Factory mergeValidatorsFactory,
|
||||
PatchSetInfoFactory patchSetInfoFactory,
|
||||
ProjectCache projectCache,
|
||||
InternalChangeQuery internalChangeQuery,
|
||||
SubmitStrategyFactory submitStrategyFactory,
|
||||
Provider<SubmoduleOp> subOpProvider,
|
||||
TagCache tagCache) {
|
||||
this.accountCache = accountCache;
|
||||
this.approvalsUtil = approvalsUtil;
|
||||
Provider<SubmoduleOp> subOpProvider) {
|
||||
this.changeControlFactory = changeControlFactory;
|
||||
this.hooks = hooks;
|
||||
this.indexer = indexer;
|
||||
this.cmUtil = cmUtil;
|
||||
this.updateFactory = updateFactory;
|
||||
this.gitRefUpdated = gitRefUpdated;
|
||||
this.changeUpdateFactory = changeUpdateFactory;
|
||||
this.batchUpdateFactory = batchUpdateFactory;
|
||||
this.repoManager = repoManager;
|
||||
this.identifiedUserFactory = identifiedUserFactory;
|
||||
this.labelNormalizer = labelNormalizer;
|
||||
this.mergedSenderFactory = mergedSenderFactory;
|
||||
this.mergeSuperSet = mergeSuperSet;
|
||||
this.mergeValidatorsFactory = mergeValidatorsFactory;
|
||||
this.patchSetInfoFactory = patchSetInfoFactory;
|
||||
this.projectCache = projectCache;
|
||||
this.internalChangeQuery = internalChangeQuery;
|
||||
this.submitStrategyFactory = submitStrategyFactory;
|
||||
this.subOpProvider = subOpProvider;
|
||||
this.tagCache = tagCache;
|
||||
|
||||
openRepos = new HashMap<>();
|
||||
commits = new CommitStatus();
|
||||
}
|
||||
|
||||
private OpenRepo getRepo(Project.NameKey project) {
|
||||
@@ -502,13 +541,14 @@ public class MergeOp implements AutoCloseable {
|
||||
}
|
||||
|
||||
public void merge(ReviewDb db, Change change, IdentifiedUser caller,
|
||||
boolean checkSubmitRules) throws OrmException, ResourceConflictException {
|
||||
boolean checkSubmitRules) throws OrmException, RestApiException {
|
||||
this.caller = caller;
|
||||
updateSubmissionId(change);
|
||||
this.db = db;
|
||||
logDebug("Beginning integration of {}", change);
|
||||
try {
|
||||
ChangeSet cs = mergeSuperSet.completeChangeSet(db, change);
|
||||
this.commits = new CommitStatus(cs);
|
||||
reloadChanges(cs);
|
||||
logDebug("Calculated to merge {}", cs);
|
||||
if (checkSubmitRules) {
|
||||
@@ -553,7 +593,7 @@ public class MergeOp implements AutoCloseable {
|
||||
}
|
||||
|
||||
private void integrateIntoHistory(ChangeSet cs)
|
||||
throws IntegrationException, ResourceConflictException {
|
||||
throws IntegrationException, RestApiException {
|
||||
logDebug("Beginning merge attempt on {}", cs);
|
||||
Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
|
||||
logDebug("Perform the merges");
|
||||
@@ -569,74 +609,60 @@ public class MergeOp implements AutoCloseable {
|
||||
}
|
||||
failFast(cs); // Done checks that don't involve running submit strategies.
|
||||
|
||||
Map<Branch.NameKey, SubmitStrategy> strategies =
|
||||
Maps.newHashMapWithExpectedSize(branches.size());
|
||||
List<SubmitStrategy> strategies = new ArrayList<>(branches.size());
|
||||
for (Branch.NameKey branch : branches) {
|
||||
OpenRepo or = getRepo(branch.getParentKey());
|
||||
OpenBranch ob = or.getBranch(branch);
|
||||
BranchBatch submitting = toSubmit.get(branch);
|
||||
SubmitStrategy strategy = createStrategy(or, branch,
|
||||
Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
|
||||
ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
|
||||
SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
|
||||
submitting.submitType(), ob.oldTip);
|
||||
strategies.put(branch, strategy);
|
||||
ob.mergeTip = preMerge(strategy, submitting.changes(), ob.oldTip);
|
||||
}
|
||||
checkMergeStrategyResults(cs, strategies, toSubmit);
|
||||
for (Project.NameKey project : br.keySet()) {
|
||||
getRepo(project).ins.flush();
|
||||
strategies.add(strategy);
|
||||
strategy.addOps(or.getUpdate(), commitsToSubmit);
|
||||
}
|
||||
|
||||
Set<Branch.NameKey> done =
|
||||
Sets.newHashSetWithExpectedSize(cbb.keySet().size());
|
||||
logDebug("Write out the new branch tips");
|
||||
BatchUpdate.execute(
|
||||
batchUpdates(projects),
|
||||
new SubmitStrategyListener(strategies, commits));
|
||||
|
||||
SubmoduleOp subOp = subOpProvider.get();
|
||||
for (Project.NameKey project : br.keySet()) {
|
||||
OpenRepo or = getRepo(project);
|
||||
for (Branch.NameKey branch : br.get(project)) {
|
||||
OpenBranch ob = or.getBranch(branch);
|
||||
boolean updated = updateBranch(or, branch);
|
||||
|
||||
BranchBatch submitting = toSubmit.get(branch);
|
||||
updateChangeStatus(ob, submitting.changes());
|
||||
updateSubmoduleSubscriptions(ob, subOp);
|
||||
if (updated) {
|
||||
fireRefUpdated(ob);
|
||||
}
|
||||
done.add(branch);
|
||||
}
|
||||
for (Branch.NameKey branch : branches) {
|
||||
OpenBranch ob = getRepo(branch.getParentKey()).getBranch(branch);
|
||||
updateSubmoduleSubscriptions(ob, subOp);
|
||||
}
|
||||
|
||||
updateSuperProjects(subOp, br.values());
|
||||
checkState(done.equals(cbb.keySet()), "programmer error: did not process"
|
||||
+ " all branches in input set.\nExpected: %s\nActual: %s",
|
||||
done, cbb.keySet());
|
||||
} catch (OrmException e) {
|
||||
throw new IntegrationException("Cannot query the database", e);
|
||||
} catch (IOException e) {
|
||||
throw new IntegrationException("Cannot query the database", e);
|
||||
} catch (UpdateException | OrmException e) {
|
||||
throw new IntegrationException("Error submitting changes", e);
|
||||
}
|
||||
}
|
||||
|
||||
private MergeTip preMerge(SubmitStrategy strategy,
|
||||
List<ChangeData> submitted, CodeReviewCommit branchTip)
|
||||
throws IntegrationException, OrmException {
|
||||
logDebug("Running submit strategy {} for {} commits {}",
|
||||
strategy.getClass().getSimpleName(), submitted.size(), submitted);
|
||||
List<CodeReviewCommit> toMerge = new ArrayList<>(submitted.size());
|
||||
for (ChangeData cd : submitted) {
|
||||
private List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
|
||||
List<BatchUpdate> updates = new ArrayList<>(projects.size());
|
||||
for (Project.NameKey project : projects) {
|
||||
updates.add(getRepo(project).getUpdate());
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
private Set<CodeReviewCommit> commits(List<ChangeData> cds) throws OrmException {
|
||||
LinkedHashSet<CodeReviewCommit> result =
|
||||
Sets.newLinkedHashSetWithExpectedSize(cds.size());
|
||||
for (ChangeData cd : cds) {
|
||||
CodeReviewCommit commit = commits.get(cd.getId());
|
||||
checkState(commit != null,
|
||||
"commit for %s not found by validateChangeList", cd.change().getId());
|
||||
toMerge.add(commit);
|
||||
result.add(commit);
|
||||
}
|
||||
return strategy.run(branchTip, toMerge);
|
||||
return result;
|
||||
}
|
||||
|
||||
private SubmitStrategy createStrategy(OpenRepo or,
|
||||
Branch.NameKey destBranch, SubmitType submitType,
|
||||
MergeTip mergeTip, Branch.NameKey destBranch, SubmitType submitType,
|
||||
CodeReviewCommit branchTip) throws IntegrationException {
|
||||
return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
|
||||
or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller,
|
||||
commits);
|
||||
mergeTip, commits, submissionId);
|
||||
}
|
||||
|
||||
private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
|
||||
@@ -808,239 +834,6 @@ public class MergeOp implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateBranch(OpenRepo or, Branch.NameKey destBranch)
|
||||
throws IntegrationException {
|
||||
OpenBranch ob = or.getBranch(destBranch);
|
||||
CodeReviewCommit currentTip = ob.getCurrentTip();
|
||||
if (Objects.equals(ob.oldTip, currentTip)) {
|
||||
if (currentTip != null) {
|
||||
logDebug("Branch already at merge tip {}, no update to perform",
|
||||
currentTip.name());
|
||||
} else {
|
||||
logDebug("Both branch and merge tip are nonexistent, no update");
|
||||
}
|
||||
return false;
|
||||
} else if (currentTip == null) {
|
||||
logDebug("No merge tip, no update to perform");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RefNames.REFS_CONFIG.equals(ob.update.getName())) {
|
||||
logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
|
||||
try {
|
||||
ProjectConfig cfg = new ProjectConfig(or.getProjectName());
|
||||
cfg.load(or.repo, currentTip);
|
||||
} catch (Exception e) {
|
||||
throw new IntegrationException("Submit would store invalid"
|
||||
+ " project configuration " + currentTip.name() + " for "
|
||||
+ or.getProjectName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
ob.update.setRefLogIdent(
|
||||
identifiedUserFactory.create(caller.getAccountId()).newRefLogIdent());
|
||||
ob.update.setForceUpdate(false);
|
||||
ob.update.setNewObjectId(currentTip);
|
||||
ob.update.setRefLogMessage("merged", true);
|
||||
try {
|
||||
RefUpdate.Result result = ob.update.update(or.rw);
|
||||
logDebug("Update of {}: {}..{} returned status {}",
|
||||
ob.update.getName(), ob.update.getOldObjectId(),
|
||||
ob.update.getNewObjectId(), result);
|
||||
switch (result) {
|
||||
case NEW:
|
||||
case FAST_FORWARD:
|
||||
if (ob.update.getResult() == RefUpdate.Result.FAST_FORWARD) {
|
||||
tagCache.updateFastForward(destBranch.getParentKey(),
|
||||
ob.update.getName(),
|
||||
ob.update.getOldObjectId(),
|
||||
currentTip);
|
||||
}
|
||||
|
||||
if (RefNames.REFS_CONFIG.equals(ob.update.getName())) {
|
||||
Project p = or.project.getProject();
|
||||
projectCache.evict(p);
|
||||
or.project = projectCache.get(p.getNameKey());
|
||||
repoManager.setProjectDescription(
|
||||
p.getNameKey(), p.getDescription());
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case LOCK_FAILURE:
|
||||
throw new IntegrationException(
|
||||
"Failed to lock " + ob.update.getName());
|
||||
default:
|
||||
throw new IOException(
|
||||
ob.update.getResult().name() + '\n' + ob.update);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IntegrationException("Cannot update " + ob.update.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void fireRefUpdated(OpenBranch ob) {
|
||||
logDebug("Firing ref updated hooks for {}", ob.name);
|
||||
gitRefUpdated.fire(ob.name.getParentKey(), ob.update);
|
||||
hooks.doRefUpdatedHook(ob.name, ob.update,
|
||||
getAccount(ob.mergeTip.getCurrentTip()));
|
||||
}
|
||||
|
||||
private Account getAccount(CodeReviewCommit codeReviewCommit) {
|
||||
Account account = null;
|
||||
PatchSetApproval submitter = approvalsUtil.getSubmitter(
|
||||
db, codeReviewCommit.notes(), codeReviewCommit.getPatchsetId());
|
||||
if (submitter != null) {
|
||||
account = accountCache.get(submitter.getAccountId()).getAccount();
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private String getByAccountName(CodeReviewCommit codeReviewCommit) {
|
||||
Account account = getAccount(codeReviewCommit);
|
||||
if (account != null && account.getFullName() != null) {
|
||||
return " by " + account.getFullName();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private Iterable<ChangeData> flattenBatches(Collection<BranchBatch> batches) {
|
||||
return FluentIterable.from(batches)
|
||||
.transformAndConcat(new Function<BranchBatch, List<ChangeData>>() {
|
||||
@Override
|
||||
public List<ChangeData> apply(BranchBatch batch) {
|
||||
return batch.changes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void checkMergeStrategyResults(ChangeSet cs,
|
||||
Map<Branch.NameKey, SubmitStrategy> strategies,
|
||||
Map<Branch.NameKey, BranchBatch> batches)
|
||||
throws ResourceConflictException, IntegrationException {
|
||||
checkCommitStatus(batches);
|
||||
failFast(cs);
|
||||
findUnmergedChanges(strategies, batches);
|
||||
failFast(cs);
|
||||
}
|
||||
|
||||
private void checkCommitStatus(Map<Branch.NameKey, BranchBatch> batches) {
|
||||
for (ChangeData cd : flattenBatches(batches.values())) {
|
||||
Change.Id id = cd.getId();
|
||||
CodeReviewCommit commit = commits.get(id);
|
||||
CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
|
||||
if (s == null) {
|
||||
commits.problem(id,
|
||||
"internal error: change not processed by merge strategy");
|
||||
continue;
|
||||
}
|
||||
switch (s) {
|
||||
case CLEAN_MERGE:
|
||||
case CLEAN_REBASE:
|
||||
case CLEAN_PICK:
|
||||
case ALREADY_MERGED:
|
||||
break; // Merge strategy accepted this change.
|
||||
|
||||
case PATH_CONFLICT:
|
||||
case REBASE_MERGE_CONFLICT:
|
||||
case MANUAL_RECURSIVE_MERGE:
|
||||
case CANNOT_CHERRY_PICK_ROOT:
|
||||
case NOT_FAST_FORWARD:
|
||||
// TODO(dborowitz): Reformat these messages to be more appropriate for
|
||||
// short problem descriptions.
|
||||
commits.problem(id,
|
||||
CharMatcher.is('\n').collapseFrom(s.getMessage(), ' '));
|
||||
break;
|
||||
|
||||
case MISSING_DEPENDENCY:
|
||||
commits.problem(id, "depends on change that was not submitted");
|
||||
break;
|
||||
|
||||
default:
|
||||
commits.problem(id, "unspecified merge failure: " + s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void findUnmergedChanges(
|
||||
Map<Branch.NameKey, SubmitStrategy> strategies,
|
||||
Map<Branch.NameKey, BranchBatch> batches) throws IntegrationException {
|
||||
checkState(strategies.keySet().equals(batches.keySet()));
|
||||
for (Branch.NameKey branch : strategies.keySet()) {
|
||||
SubmitStrategy strategy = strategies.get(branch);
|
||||
BranchBatch batch = batches.get(branch);
|
||||
if (batch.submitType() == SubmitType.CHERRY_PICK) {
|
||||
// Might have picked a subset of changes, can't do this sanity check.
|
||||
continue;
|
||||
}
|
||||
OpenBranch ob = getRepo(branch.getParentKey()).branches.get(branch);
|
||||
Set<Change.Id> unmerged = strategy.args.mergeUtil.findUnmergedChanges(
|
||||
batch.changes(), strategy.args.rw, strategy.args.canMergeFlag,
|
||||
ob.oldTip, ob.mergeTip.getCurrentTip());
|
||||
for (Change.Id id : unmerged) {
|
||||
commits.problem(id,
|
||||
"internal error: change not reachable from new branch tip");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChangeStatus(OpenBranch ob, List<ChangeData> submitted)
|
||||
throws ResourceConflictException {
|
||||
List<Change.Id> problemChanges = new ArrayList<>(submitted.size());
|
||||
logDebug("Updating change status for {} changes", submitted.size());
|
||||
|
||||
for (ChangeData cd : submitted) {
|
||||
Change.Id id = cd.getId();
|
||||
try {
|
||||
Change c = cd.change();
|
||||
CodeReviewCommit commit = commits.get(id);
|
||||
CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
|
||||
logDebug("Status of change {} ({}) on {}: {}", id, commit.name(),
|
||||
c.getDest(), s);
|
||||
checkState(s != null,
|
||||
"status not set for change %s; expected to previously fail fast",
|
||||
id);
|
||||
setApproval(cd);
|
||||
|
||||
ObjectId mergeResultRev = ob.mergeTip != null
|
||||
? ob.mergeTip.getMergeResults().get(commit) : null;
|
||||
String txt = s.getMessage();
|
||||
|
||||
// The change notes must be forcefully reloaded so that the SUBMIT
|
||||
// approval that we added earlier is visible
|
||||
commit.notes().reload();
|
||||
if (s == CommitMergeStatus.CLEAN_MERGE) {
|
||||
setMerged(c, message(c, txt + getByAccountName(commit)),
|
||||
mergeResultRev);
|
||||
} else if (s == CommitMergeStatus.CLEAN_REBASE
|
||||
|| s == CommitMergeStatus.CLEAN_PICK) {
|
||||
setMerged(c, message(c, txt + " as " + commit.name()
|
||||
+ getByAccountName(commit)), mergeResultRev);
|
||||
} else if (s == CommitMergeStatus.ALREADY_MERGED) {
|
||||
setMerged(c, null, mergeResultRev);
|
||||
} else {
|
||||
throw new IllegalStateException("unexpected status " + s +
|
||||
" for change " + c.getId() + "; expected to previously fail fast");
|
||||
}
|
||||
} catch (OrmException | IOException err) {
|
||||
logWarn("Error updating change status for " + id, err);
|
||||
problemChanges.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (problemChanges.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder msg = new StringBuilder("Error updating status of change");
|
||||
if (problemChanges.size() == 1) {
|
||||
msg.append(' ').append(problemChanges.iterator().next());
|
||||
} else {
|
||||
msg.append('s').append(Joiner.on(", ").join(problemChanges));
|
||||
}
|
||||
throw new ResourceConflictException(msg.toString());
|
||||
}
|
||||
|
||||
private void updateSubmoduleSubscriptions(OpenBranch ob, SubmoduleOp subOp) {
|
||||
CodeReviewCommit branchTip = ob.oldTip;
|
||||
MergeTip mergeTip = ob.mergeTip;
|
||||
@@ -1067,213 +860,6 @@ public class MergeOp implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private ChangeMessage message(Change c, String body) {
|
||||
String uuid;
|
||||
try {
|
||||
uuid = ChangeUtil.messageUUID(db);
|
||||
} catch (OrmException e) {
|
||||
return null;
|
||||
}
|
||||
ChangeMessage m = new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid),
|
||||
null, TimeUtil.nowTs(), c.currentPatchSetId());
|
||||
m.setMessage(body);
|
||||
return m;
|
||||
}
|
||||
|
||||
private void setMerged(Change c, ChangeMessage msg, ObjectId mergeResultRev)
|
||||
throws OrmException, IOException {
|
||||
logDebug("Setting change {} merged", c.getId());
|
||||
ChangeUpdate update = null;
|
||||
final PatchSetApproval submitter;
|
||||
PatchSet merged;
|
||||
try {
|
||||
db.changes().beginTransaction(c.getId());
|
||||
|
||||
// We must pull the patchset out of commits, because the patchset ID is
|
||||
// modified when using the cherry-pick merge strategy.
|
||||
CodeReviewCommit commit = commits.get(c.getId());
|
||||
PatchSet.Id mergedId = commit.change().currentPatchSetId();
|
||||
// TODO(dborowitz): Use PatchSetUtil after BatchUpdate migration.
|
||||
merged = db.patchSets().get(mergedId);
|
||||
c = setMergedPatchSet(commit.notes(), mergedId);
|
||||
submitter = approvalsUtil.getSubmitter(db, commit.notes(), mergedId);
|
||||
ChangeControl control = commit.getControl();
|
||||
update = updateFactory.create(control, c.getLastUpdatedOn());
|
||||
|
||||
// TODO(yyonas): we need to be able to change the author of the message
|
||||
// is not the person for whom the change was made. addMergedMessage
|
||||
// did this in the past.
|
||||
if (msg != null) {
|
||||
cmUtil.addChangeMessage(db, update, msg);
|
||||
}
|
||||
db.commit();
|
||||
|
||||
} finally {
|
||||
db.rollback();
|
||||
}
|
||||
update.commit();
|
||||
indexer.index(db, c);
|
||||
|
||||
try {
|
||||
mergedSenderFactory.create(
|
||||
c.getId(),
|
||||
submitter != null ? submitter.getAccountId() : null).sendAsync();
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot email merged notification for " + c.getId(), e);
|
||||
}
|
||||
if (submitter != null && mergeResultRev != null) {
|
||||
try {
|
||||
hooks.doChangeMergedHook(c,
|
||||
accountCache.get(submitter.getAccountId()).getAccount(),
|
||||
merged, db, mergeResultRev.name());
|
||||
} catch (OrmException ex) {
|
||||
logError("Cannot run hook for submitted patch set " + c.getId(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Change setMergedPatchSet(final ChangeNotes notes,
|
||||
final PatchSet.Id merged) throws OrmException {
|
||||
return db.changes().atomicUpdate(notes.getChangeId(),
|
||||
new AtomicUpdate<Change>() {
|
||||
@Override
|
||||
public Change update(Change c) {
|
||||
c.setStatus(Change.Status.MERGED);
|
||||
c.setSubmissionId(submissionId);
|
||||
if (!merged.equals(c.currentPatchSetId())) {
|
||||
// Uncool; the patch set changed after we merged it.
|
||||
// Go back to the patch set that was actually merged.
|
||||
//
|
||||
try {
|
||||
c.setCurrentPatchSet(
|
||||
patchSetInfoFactory.get(db, notes, merged));
|
||||
} catch (PatchSetInfoNotAvailableException e1) {
|
||||
logError("Cannot read merged patch set " + merged, e1);
|
||||
}
|
||||
}
|
||||
ChangeUtil.updated(c);
|
||||
return c;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setApproval(ChangeData cd) throws OrmException, IOException {
|
||||
Timestamp timestamp = TimeUtil.nowTs();
|
||||
ChangeControl control = cd.changeControl();
|
||||
PatchSet.Id psId = cd.currentPatchSet().getId();
|
||||
PatchSet.Id psIdNewRev = commits.get(cd.change().getId())
|
||||
.change().currentPatchSetId();
|
||||
|
||||
logDebug("Add approval for " + cd);
|
||||
ChangeUpdate update = updateFactory.create(control, timestamp);
|
||||
update.setPatchSetId(psId);
|
||||
update.putReviewer(caller.getAccountId(), REVIEWER);
|
||||
Optional<SubmitRecord> okRecord = findOkRecord(cd.getSubmitRecords());
|
||||
if (okRecord.isPresent()) {
|
||||
update.merge(ImmutableList.of(okRecord.get()));
|
||||
} else {
|
||||
update.merge(cd.getSubmitRecords());
|
||||
}
|
||||
db.changes().beginTransaction(cd.change().getId());
|
||||
try {
|
||||
BatchMetaDataUpdate batch = update.openUpdate();
|
||||
LabelNormalizer.Result normalized =
|
||||
approve(control, psId, caller, update, timestamp);
|
||||
batch.write(update, new CommitBuilder());
|
||||
|
||||
// If the submit strategy created a new revision (rebase, cherry-pick)
|
||||
// approve that as well
|
||||
if (!psIdNewRev.equals(psId)) {
|
||||
update.commit();
|
||||
// Create a new ChangeUpdate instance because we need to store meta data
|
||||
// on another patch set (psIdNewRev).
|
||||
update = updateFactory.create(control, timestamp);
|
||||
update.setPatchSetId(psIdNewRev);
|
||||
saveApprovals(normalized, update);
|
||||
batch = update.openUpdate();
|
||||
batch.write(update, new CommitBuilder());
|
||||
}
|
||||
db.commit();
|
||||
} finally {
|
||||
db.rollback();
|
||||
}
|
||||
update.commit();
|
||||
indexer.index(db, cd.change());
|
||||
}
|
||||
|
||||
private LabelNormalizer.Result approve(ChangeControl control,
|
||||
PatchSet.Id psId, IdentifiedUser user, ChangeUpdate update,
|
||||
Timestamp timestamp) throws OrmException {
|
||||
Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
|
||||
for (PatchSetApproval psa :
|
||||
approvalsUtil.byPatchSet(db, control, psId)) {
|
||||
if (!byKey.containsKey(psa.getKey())) {
|
||||
byKey.put(psa.getKey(), psa);
|
||||
}
|
||||
}
|
||||
|
||||
PatchSetApproval submit = new PatchSetApproval(
|
||||
new PatchSetApproval.Key(
|
||||
psId,
|
||||
user.getAccountId(),
|
||||
LabelId.SUBMIT),
|
||||
(short) 1, TimeUtil.nowTs());
|
||||
byKey.put(submit.getKey(), submit);
|
||||
submit.setValue((short) 1);
|
||||
submit.setGranted(timestamp);
|
||||
|
||||
// Flatten out existing approvals for this patch set based upon the current
|
||||
// permissions. Once the change is closed the approvals are not updated at
|
||||
// presentation view time, except for zero votes used to indicate a reviewer
|
||||
// was added. So we need to make sure votes are accurate now. This way if
|
||||
// permissions get modified in the future, historical records stay accurate.
|
||||
LabelNormalizer.Result normalized =
|
||||
labelNormalizer.normalize(control, byKey.values());
|
||||
update.putApproval(submit.getLabel(), submit.getValue());
|
||||
saveApprovals(normalized, update);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void saveApprovals(LabelNormalizer.Result normalized,
|
||||
ChangeUpdate update) throws OrmException {
|
||||
PatchSet.Id psId = update.getPatchSetId();
|
||||
db.patchSetApprovals().upsert(
|
||||
convertPatchSet(normalized.getNormalized(), psId));
|
||||
db.patchSetApprovals().delete(convertPatchSet(normalized.deleted(), psId));
|
||||
for (PatchSetApproval psa : normalized.updated()) {
|
||||
update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
|
||||
}
|
||||
for (PatchSetApproval psa : normalized.deleted()) {
|
||||
update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Don't use a label in notedb; just check when status
|
||||
// change happened.
|
||||
for (PatchSetApproval psa : normalized.unchanged()) {
|
||||
if (psa.isSubmit()) {
|
||||
logDebug("Adding submit label " + psa);
|
||||
update.putApprovalFor(
|
||||
psa.getAccountId(), psa.getLabel(), psa.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Iterable<PatchSetApproval> convertPatchSet(
|
||||
Iterable<PatchSetApproval> approvals, final PatchSet.Id psId) {
|
||||
return Iterables.transform(approvals,
|
||||
new Function<PatchSetApproval, PatchSetApproval>() {
|
||||
@Override
|
||||
public PatchSetApproval apply(PatchSetApproval in) {
|
||||
if (in.getPatchSetId().equals(psId)) {
|
||||
return in;
|
||||
} else {
|
||||
return new PatchSetApproval(psId, in);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openRepos(Collection<Project.NameKey> projects)
|
||||
throws IntegrationException {
|
||||
for (Project.NameKey project : projects) {
|
||||
@@ -1306,7 +892,8 @@ public class MergeOp implements AutoCloseable {
|
||||
//TODO(dborowitz): support InternalUser in ChangeUpdate
|
||||
ChangeControl control = changeControlFactory.controlFor(change,
|
||||
identifiedUserFactory.create(change.getOwner()));
|
||||
ChangeUpdate update = updateFactory.create(control);
|
||||
// TODO(dborowitz): Convert to BatchUpdate.
|
||||
ChangeUpdate update = changeUpdateFactory.create(control);
|
||||
try {
|
||||
change = db.changes().atomicUpdate(
|
||||
change.getId(),
|
||||
|
@@ -40,7 +40,6 @@ import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.strategy.CommitMergeStatus;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.project.ProjectState;
|
||||
import com.google.gerrit.server.query.change.ChangeData;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
@@ -674,19 +673,15 @@ public class MergeUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Change.Id> findUnmergedChanges(List<ChangeData> cds,
|
||||
public Set<Change.Id> findUnmergedChanges(Set<Change.Id> expected,
|
||||
CodeReviewRevWalk rw, RevFlag canMergeFlag, CodeReviewCommit oldTip,
|
||||
CodeReviewCommit mergeTip) throws IntegrationException {
|
||||
Set<Change.Id> expected = Sets.newHashSetWithExpectedSize(cds.size());
|
||||
for (ChangeData cd : cds) {
|
||||
expected.add(cd.getId());
|
||||
}
|
||||
if (mergeTip == null) {
|
||||
return expected;
|
||||
}
|
||||
|
||||
try {
|
||||
Set<Change.Id> found = Sets.newHashSetWithExpectedSize(cds.size());
|
||||
Set<Change.Id> found = Sets.newHashSetWithExpectedSize(expected.size());
|
||||
rw.resetRetain(canMergeFlag);
|
||||
rw.sort(RevSort.TOPO);
|
||||
rw.markStart(mergeTip);
|
||||
|
@@ -117,7 +117,6 @@ import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||
import com.google.gerrit.server.notedb.NotesMigration;
|
||||
import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gerrit.server.project.ProjectCache;
|
||||
import com.google.gerrit.server.project.ProjectControl;
|
||||
import com.google.gerrit.server.project.ProjectState;
|
||||
@@ -1828,7 +1827,7 @@ public class ReceiveCommits {
|
||||
}
|
||||
|
||||
private void submit(ChangeControl changeCtl, PatchSet ps)
|
||||
throws OrmException, ResourceConflictException {
|
||||
throws OrmException, RestApiException {
|
||||
Submit submit = submitProvider.get();
|
||||
RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
|
||||
try (MergeOp op = mergeOpProvider.get()) {
|
||||
@@ -2159,7 +2158,7 @@ public class ReceiveCommits {
|
||||
requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
|
||||
@Override
|
||||
public PatchSet.Id call() throws OrmException, IOException,
|
||||
NoSuchChangeException, ResourceConflictException {
|
||||
RestApiException {
|
||||
try {
|
||||
if (magicBranch != null && magicBranch.edit) {
|
||||
return upsertEdit();
|
||||
@@ -2233,7 +2232,7 @@ public class ReceiveCommits {
|
||||
}
|
||||
|
||||
PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException,
|
||||
ResourceConflictException {
|
||||
RestApiException {
|
||||
final Account.Id me = user.getAccountId();
|
||||
final List<FooterLine> footerLines = newCommit.getFooterLines();
|
||||
final MailRecipients recipients = new MailRecipients();
|
||||
|
@@ -14,23 +14,18 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.gerrit.extensions.restapi.MergeConflictException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetInfo;
|
||||
import com.google.gerrit.server.ChangeUtil;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeIdenticalTreeException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
|
||||
@@ -39,6 +34,7 @@ import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -49,86 +45,65 @@ public class CherryPick extends SubmitStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeTip run(CodeReviewCommit branchTip,
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
|
||||
public List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) {
|
||||
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
|
||||
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
|
||||
boolean first = true;
|
||||
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
Change.Id cid = n.change().getId();
|
||||
if (first && branchTip == null) {
|
||||
u.addOp(cid, new CherryPickUnbornRootOp(mergeTip, n));
|
||||
} else if (n.getParentCount() == 0) {
|
||||
u.addOp(cid, new CherryPickRootOp(n));
|
||||
} else if (n.getParentCount() == 1) {
|
||||
u.addOp(cid, new CherryPickOneOp(mergeTip, n));
|
||||
} else {
|
||||
u.addOp(cid, new CherryPickMultipleParentsOp(mergeTip, n));
|
||||
}
|
||||
first = false;
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
if (first && args.mergeTip.getInitialTip() == null) {
|
||||
ops.add(new CherryPickUnbornRootOp(n));
|
||||
} else if (n.getParentCount() == 0) {
|
||||
ops.add(new CherryPickRootOp(n));
|
||||
} else if (n.getParentCount() == 1) {
|
||||
ops.add(new CherryPickOneOp(n));
|
||||
} else {
|
||||
ops.add(new CherryPickMultipleParentsOp(n));
|
||||
}
|
||||
u.execute();
|
||||
} catch (UpdateException | RestApiException e) {
|
||||
throw new IntegrationException(
|
||||
"Cannot cherry-pick onto " + args.destBranch, e);
|
||||
first = false;
|
||||
}
|
||||
// TODO(dborowitz): When BatchUpdate is hoisted out of CherryPick,
|
||||
// SubmitStrategy should probably no longer return MergeTip, instead just
|
||||
// mutating a single shared MergeTip passed in from the caller.
|
||||
return mergeTip;
|
||||
return ops;
|
||||
}
|
||||
|
||||
private static class CherryPickUnbornRootOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private CherryPickUnbornRootOp(MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
private class CherryPickUnbornRootOp extends SubmitStrategyOp {
|
||||
private CherryPickUnbornRootOp(CodeReviewCommit toMerge) {
|
||||
super(CherryPick.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) {
|
||||
protected void updateRepoImpl(RepoContext ctx) {
|
||||
// The branch is unborn. Take fast-forward resolution to create the
|
||||
// branch.
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
args.mergeTip.moveTipTo(toMerge, toMerge);
|
||||
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CherryPickRootOp extends BatchUpdate.Op {
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private class CherryPickRootOp extends SubmitStrategyOp {
|
||||
private CherryPickRootOp(CodeReviewCommit toMerge) {
|
||||
this.toMerge = toMerge;
|
||||
super(CherryPick.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) {
|
||||
public void updateRepoImpl(RepoContext ctx) {
|
||||
// Refuse to merge a root commit into an existing branch, we cannot obtain
|
||||
// a delta for the cherry-pick to apply.
|
||||
toMerge.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
private class CherryPickOneOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private class CherryPickOneOp extends SubmitStrategyOp {
|
||||
private PatchSet.Id psId;
|
||||
private CodeReviewCommit newCommit;
|
||||
private PatchSetInfo patchSetInfo;
|
||||
|
||||
private CherryPickOneOp(MergeTip mergeTip, CodeReviewCommit n) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = n;
|
||||
private CherryPickOneOp(CodeReviewCommit toMerge) {
|
||||
super(CherryPick.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) throws IOException {
|
||||
protected void updateRepoImpl(RepoContext ctx) throws IOException {
|
||||
// If there is only one parent, a cherry-pick can be done by taking the
|
||||
// delta relative to that one parent and redoing that on the current merge
|
||||
// tip.
|
||||
@@ -142,65 +117,57 @@ public class CherryPick extends SubmitStrategy {
|
||||
ctx.getWhen(), args.serverIdent.getTimeZone());
|
||||
try {
|
||||
newCommit = args.mergeUtil.createCherryPickFromCommit(
|
||||
args.repo, args.inserter, mergeTip.getCurrentTip(), toMerge,
|
||||
args.repo, args.inserter, args.mergeTip.getCurrentTip(), toMerge,
|
||||
committer, cherryPickCmtMsg, args.rw);
|
||||
newCommit.copyFrom(toMerge);
|
||||
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
|
||||
mergeTip.moveTipTo(newCommit, newCommit);
|
||||
args.commits.put(newCommit);
|
||||
ctx.addRefUpdate(
|
||||
new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
|
||||
patchSetInfo =
|
||||
args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
|
||||
} catch (MergeConflictException mce) {
|
||||
// Keep going in the case of a single merge failure; the goal is to
|
||||
// cherry-pick as many commits as possible.
|
||||
toMerge.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
|
||||
return;
|
||||
} catch (MergeIdenticalTreeException mie) {
|
||||
toMerge.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
|
||||
return;
|
||||
}
|
||||
// Initial copy doesn't have new patch set ID since change hasn't been
|
||||
// updated yet.
|
||||
newCommit.copyFrom(toMerge);
|
||||
newCommit.setPatchsetId(psId);
|
||||
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
|
||||
args.mergeTip.moveTipTo(newCommit, newCommit);
|
||||
args.commits.put(newCommit);
|
||||
|
||||
ctx.addRefUpdate(
|
||||
new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
|
||||
patchSetInfo =
|
||||
args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateChange(ChangeContext ctx) throws OrmException,
|
||||
public void updateChangeImpl(ChangeContext ctx) throws OrmException,
|
||||
NoSuchChangeException {
|
||||
if (newCommit == null) {
|
||||
// Merge conflict; don't update change.
|
||||
return false;
|
||||
}
|
||||
checkState(newCommit != null,
|
||||
"no new commit produced by CherryPick of %s, expected to fail fast",
|
||||
toMerge.change().getId());
|
||||
PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
|
||||
|
||||
args.psUtil.insert(ctx.getDb(), ctx.getUpdate(psId), psId, newCommit,
|
||||
false, prevPs != null ? prevPs.getGroups() : null, null);
|
||||
toMerge.change().setCurrentPatchSet(patchSetInfo);
|
||||
ctx.getChange().setCurrentPatchSet(patchSetInfo);
|
||||
ctx.saveChange();
|
||||
|
||||
List<PatchSetApproval> approvals = Lists.newArrayList();
|
||||
for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
|
||||
args.db, toMerge.getControl(), toMerge.getPatchsetId())) {
|
||||
approvals.add(new PatchSetApproval(psId, a));
|
||||
ctx.getUpdate(psId).putApproval(a.getLabel(), a.getValue());
|
||||
}
|
||||
args.db.patchSetApprovals().insert(approvals);
|
||||
// Don't copy approvals, as this is already taken care of by
|
||||
// SubmitStrategyOp.
|
||||
|
||||
newCommit.setControl(
|
||||
args.changeControlFactory.controlFor(toMerge.change(), args.caller));
|
||||
return true;
|
||||
newCommit.setControl(ctx.getControl());
|
||||
}
|
||||
}
|
||||
|
||||
private class CherryPickMultipleParentsOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private CherryPickMultipleParentsOp(MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
private class CherryPickMultipleParentsOp extends SubmitStrategyOp {
|
||||
private CherryPickMultipleParentsOp(CodeReviewCommit toMerge) {
|
||||
super(CherryPick.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx)
|
||||
public void updateRepoImpl(RepoContext ctx)
|
||||
throws IntegrationException, IOException {
|
||||
if (args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)) {
|
||||
// One or more dependencies were not met. The status was already marked
|
||||
@@ -212,6 +179,7 @@ public class CherryPick extends SubmitStrategy {
|
||||
// with that merge present and replaced by an equivalent merge with a
|
||||
// different first parent. So instead behave as though MERGE_IF_NECESSARY
|
||||
// was configured.
|
||||
MergeTip mergeTip = args.mergeTip;
|
||||
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
} else {
|
||||
|
@@ -14,17 +14,11 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import static com.google.gerrit.server.git.strategy.MarkCleanMergesOp.anyChangeId;
|
||||
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -34,44 +28,29 @@ public class FastForwardOnly extends SubmitStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeTip run(CodeReviewCommit branchTip,
|
||||
public List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
List<CodeReviewCommit> sorted =
|
||||
args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
|
||||
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
|
||||
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
|
||||
CodeReviewCommit newTipCommit =
|
||||
args.mergeUtil.getFirstFastForward(branchTip, args.rw, sorted);
|
||||
if (!newTipCommit.equals(branchTip)) {
|
||||
u.addOp(newTipCommit.change().getId(),
|
||||
new FastForwardOp(mergeTip, newTipCommit));
|
||||
}
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
u.addOp(n.change().getId(), new NotFastForwardOp(n));
|
||||
}
|
||||
u.addOp(anyChangeId(toMerge), new MarkCleanMergesOp(args, mergeTip));
|
||||
|
||||
u.execute();
|
||||
} catch (RestApiException | UpdateException e) {
|
||||
if (e.getCause() instanceof IntegrationException) {
|
||||
throw new IntegrationException(e.getCause().getMessage(), e);
|
||||
}
|
||||
throw new IntegrationException(
|
||||
"Cannot fast-forward into " + args.destBranch);
|
||||
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
|
||||
CodeReviewCommit newTipCommit = args.mergeUtil.getFirstFastForward(
|
||||
args.mergeTip.getInitialTip(), args.rw, sorted);
|
||||
if (!newTipCommit.equals(args.mergeTip.getInitialTip())) {
|
||||
ops.add(new FastForwardOp(args, newTipCommit));
|
||||
}
|
||||
return mergeTip;
|
||||
while (!sorted.isEmpty()) {
|
||||
ops.add(new NotFastForwardOp(sorted.remove(0)));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
private static class NotFastForwardOp extends BatchUpdate.Op {
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private class NotFastForwardOp extends SubmitStrategyOp {
|
||||
private NotFastForwardOp(CodeReviewCommit toMerge) {
|
||||
this.toMerge = toMerge;
|
||||
super(FastForwardOnly.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) {
|
||||
public void updateRepoImpl(RepoContext ctx) {
|
||||
toMerge.setStatusCode(CommitMergeStatus.NOT_FAST_FORWARD);
|
||||
}
|
||||
}
|
||||
|
@@ -14,27 +14,20 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class FastForwardOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
FastForwardOp(MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
class FastForwardOp extends SubmitStrategyOp {
|
||||
FastForwardOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
|
||||
super(args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx)
|
||||
public void updateRepoImpl(RepoContext ctx)
|
||||
throws IntegrationException, IOException {
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
args.mergeTip.moveTipTo(toMerge, toMerge);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
// 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.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
|
||||
/**
|
||||
* Operation for a change that is implicitly integrated by integrating another
|
||||
* commit.
|
||||
* <p>
|
||||
* Updates the change status and message based on {@link
|
||||
* CodeReviewCommit#getStatusCode()}, but does not touch the repository.
|
||||
*/
|
||||
class ImplicitIntegrateOp extends SubmitStrategyOp {
|
||||
ImplicitIntegrateOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
|
||||
super(args, toMerge);
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
// 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.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.Context;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
|
||||
class MarkCleanMergesOp extends BatchUpdate.Op {
|
||||
static Change.Id anyChangeId(Iterable<CodeReviewCommit> commits) {
|
||||
for (CodeReviewCommit c : commits) {
|
||||
if (c.change() != null) {
|
||||
return c.change().getId();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"no CodeReviewCommits have changes: " + commits);
|
||||
}
|
||||
private final SubmitStrategy.Arguments args;
|
||||
private final MergeTip mergeTip;
|
||||
|
||||
MarkCleanMergesOp(SubmitStrategy.Arguments args,
|
||||
MergeTip mergeTip) {
|
||||
this.args = args;
|
||||
this.mergeTip = mergeTip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postUpdate(Context ctx) throws IntegrationException {
|
||||
// TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
|
||||
// When hoisting BatchUpdate into MergeOp, we will need to teach
|
||||
// BatchUpdate how to produce CodeReviewRevWalks.
|
||||
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
|
||||
mergeTip.getCurrentTip(), args.alreadyAccepted);
|
||||
}
|
||||
}
|
@@ -14,16 +14,10 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import static com.google.gerrit.server.git.strategy.MarkCleanMergesOp.anyChangeId;
|
||||
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,33 +27,22 @@ public class MergeAlways extends SubmitStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeTip run(CodeReviewCommit branchTip,
|
||||
public List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
List<CodeReviewCommit> sorted =
|
||||
args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
|
||||
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
|
||||
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
|
||||
if (branchTip == null) {
|
||||
// The branch is unborn. Take a fast-forward resolution to
|
||||
// create the branch.
|
||||
CodeReviewCommit first = sorted.remove(0);
|
||||
u.addOp(first.change().getId(), new FastForwardOp(mergeTip, first));
|
||||
}
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
u.addOp(n.change().getId(), new MergeOneOp(args, mergeTip, n));
|
||||
}
|
||||
u.addOp(anyChangeId(toMerge), new MarkCleanMergesOp(args, mergeTip));
|
||||
|
||||
u.execute();
|
||||
} catch (RestApiException | UpdateException e) {
|
||||
if (e.getCause() instanceof IntegrationException) {
|
||||
throw new IntegrationException(e.getCause().getMessage(), e);
|
||||
}
|
||||
throw new IntegrationException(
|
||||
"Cannot merge into " + args.destBranch);
|
||||
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
|
||||
if (args.mergeTip.getInitialTip() == null) {
|
||||
// The branch is unborn. Take a fast-forward resolution to
|
||||
// create the branch.
|
||||
CodeReviewCommit first = sorted.remove(0);
|
||||
ops.add(new FastForwardOp(args, first));
|
||||
}
|
||||
return mergeTip;
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
ops.add(new MergeOneOp(args, n));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static boolean dryRun(SubmitDryRun.Arguments args,
|
||||
|
@@ -14,16 +14,10 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import static com.google.gerrit.server.git.strategy.MarkCleanMergesOp.anyChangeId;
|
||||
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,42 +27,28 @@ public class MergeIfNecessary extends SubmitStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeTip run(CodeReviewCommit branchTip,
|
||||
public List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
List<CodeReviewCommit> sorted =
|
||||
args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
|
||||
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
|
||||
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
|
||||
// Start with the first fast-forward. This may create the branch if it did
|
||||
// not exist.
|
||||
CodeReviewCommit firstFastForward;
|
||||
if (branchTip == null) {
|
||||
firstFastForward = sorted.remove(0);
|
||||
} else {
|
||||
firstFastForward =
|
||||
args.mergeUtil.getFirstFastForward(branchTip, args.rw, sorted);
|
||||
}
|
||||
if (!firstFastForward.equals(branchTip)) {
|
||||
u.addOp(firstFastForward.change().getId(),
|
||||
new FastForwardOp(mergeTip, firstFastForward));
|
||||
}
|
||||
|
||||
// For every other commit do a pair-wise merge.
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
u.addOp(n.change().getId(), new MergeOneOp(args, mergeTip, n));
|
||||
}
|
||||
u.addOp(anyChangeId(toMerge), new MarkCleanMergesOp(args, mergeTip));
|
||||
|
||||
u.execute();
|
||||
} catch (RestApiException | UpdateException e) {
|
||||
if (e.getCause() instanceof IntegrationException) {
|
||||
throw new IntegrationException(e.getCause().getMessage(), e);
|
||||
}
|
||||
throw new IntegrationException(
|
||||
"Cannot merge into " + args.destBranch);
|
||||
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
|
||||
CodeReviewCommit firstFastForward;
|
||||
if (args.mergeTip.getInitialTip() == null) {
|
||||
firstFastForward = sorted.remove(0);
|
||||
} else {
|
||||
firstFastForward = args.mergeUtil.getFirstFastForward(
|
||||
args.mergeTip.getInitialTip(), args.rw, sorted);
|
||||
}
|
||||
return mergeTip;
|
||||
if (!firstFastForward.equals(args.mergeTip.getInitialTip())) {
|
||||
ops.add(new FastForwardOp(args, firstFastForward));
|
||||
}
|
||||
|
||||
// For every other commit do a pair-wise merge.
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
ops.add(new MergeOneOp(args, n));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static boolean dryRun(SubmitDryRun.Arguments args,
|
||||
|
@@ -14,34 +14,25 @@
|
||||
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class MergeOneOp extends BatchUpdate.Op {
|
||||
private final SubmitStrategy.Arguments args;
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
MergeOneOp(SubmitStrategy.Arguments args, MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.args = args;
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
class MergeOneOp extends SubmitStrategyOp {
|
||||
MergeOneOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
|
||||
super(args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx)
|
||||
public void updateRepoImpl(RepoContext ctx)
|
||||
throws IntegrationException, IOException {
|
||||
PersonIdent caller = ctx.getUser().asIdentifiedUser().newCommitterIdent(
|
||||
ctx.getWhen(), ctx.getTimeZone());
|
||||
if (mergeTip.getCurrentTip() == null) {
|
||||
if (args.mergeTip.getCurrentTip() == null) {
|
||||
throw new IllegalStateException("cannot merge commit " + toMerge.name()
|
||||
+ " onto a null tip; expected at least one fast-forward prior to"
|
||||
+ " this operation");
|
||||
@@ -52,7 +43,7 @@ class MergeOneOp extends BatchUpdate.Op {
|
||||
CodeReviewCommit merged =
|
||||
args.mergeUtil.mergeOneCommit(caller, args.serverIdent,
|
||||
ctx.getRepository(), args.rw, ctx.getInserter(), args.canMergeFlag,
|
||||
args.destBranch, mergeTip.getCurrentTip(), toMerge);
|
||||
mergeTip.moveTipTo(merged, toMerge);
|
||||
args.destBranch, args.mergeTip.getCurrentTip(), toMerge);
|
||||
args.mergeTip.moveTipTo(merged, toMerge);
|
||||
}
|
||||
}
|
||||
|
@@ -15,14 +15,11 @@
|
||||
package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.restapi.MergeConflictException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.server.change.RebaseChangeOp;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
||||
import com.google.gerrit.server.git.BatchUpdate.Context;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
@@ -30,13 +27,13 @@ import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.RebaseSorter;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||
import com.google.gerrit.server.project.InvalidChangeOperationException;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -48,98 +45,76 @@ public class RebaseIfNecessary extends SubmitStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeTip run(final CodeReviewCommit branchTip,
|
||||
final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
|
||||
public List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
|
||||
List<CodeReviewCommit> sorted = sort(toMerge);
|
||||
|
||||
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
|
||||
boolean first = true;
|
||||
|
||||
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
Change.Id cid = n.change().getId();
|
||||
if (first && branchTip == null) {
|
||||
u.addOp(cid, new RebaseUnbornRootOp(mergeTip, n));
|
||||
} else if (n.getParentCount() == 0) {
|
||||
u.addOp(cid, new RebaseRootOp(n));
|
||||
} else if (n.getParentCount() == 1) {
|
||||
u.addOp(cid, new RebaseOneOp(mergeTip, n));
|
||||
} else {
|
||||
u.addOp(cid, new RebaseMultipleParentsOp(mergeTip, n));
|
||||
}
|
||||
first = false;
|
||||
while (!sorted.isEmpty()) {
|
||||
CodeReviewCommit n = sorted.remove(0);
|
||||
if (first && args.mergeTip.getInitialTip() == null) {
|
||||
ops.add(new RebaseUnbornRootOp(n));
|
||||
} else if (n.getParentCount() == 0) {
|
||||
ops.add(new RebaseRootOp(n));
|
||||
} else if (n.getParentCount() == 1) {
|
||||
ops.add(new RebaseOneOp(n));
|
||||
} else {
|
||||
ops.add(new RebaseMultipleParentsOp(n));
|
||||
}
|
||||
u.execute();
|
||||
} catch (UpdateException | RestApiException e) {
|
||||
if (e.getCause() instanceof IntegrationException) {
|
||||
throw new IntegrationException(e.getCause().getMessage(), e);
|
||||
}
|
||||
throw new IntegrationException(
|
||||
"Cannot rebase onto " + args.destBranch, e);
|
||||
first = false;
|
||||
}
|
||||
return mergeTip;
|
||||
return ops;
|
||||
}
|
||||
|
||||
private class RebaseUnbornRootOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private RebaseUnbornRootOp(MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
private class RebaseUnbornRootOp extends SubmitStrategyOp {
|
||||
private RebaseUnbornRootOp(CodeReviewCommit toMerge) {
|
||||
super(RebaseIfNecessary.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) {
|
||||
public void updateRepoImpl(RepoContext ctx) {
|
||||
// The branch is unborn. Take fast-forward resolution to create the
|
||||
// branch.
|
||||
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
acceptMergeTip(mergeTip);
|
||||
args.mergeTip.moveTipTo(toMerge, toMerge);
|
||||
acceptMergeTip(args.mergeTip);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RebaseRootOp extends BatchUpdate.Op {
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private class RebaseRootOp extends SubmitStrategyOp {
|
||||
private RebaseRootOp(CodeReviewCommit toMerge) {
|
||||
this.toMerge = toMerge;
|
||||
super(RebaseIfNecessary.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx) {
|
||||
public void updateRepoImpl(RepoContext ctx) {
|
||||
// Refuse to merge a root commit into an existing branch, we cannot obtain
|
||||
// a delta for the cherry-pick to apply.
|
||||
toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
private class RebaseOneOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private class RebaseOneOp extends SubmitStrategyOp {
|
||||
private RebaseChangeOp rebaseOp;
|
||||
private CodeReviewCommit newCommit;
|
||||
|
||||
private RebaseOneOp(MergeTip mergeTip, CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
private RebaseOneOp(CodeReviewCommit toMerge) {
|
||||
super(RebaseIfNecessary.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx)
|
||||
public void updateRepoImpl(RepoContext ctx)
|
||||
throws IntegrationException, InvalidChangeOperationException,
|
||||
RestApiException, IOException, OrmException {
|
||||
// TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
|
||||
// When hoisting BatchUpdate into MergeOp, we will need to teach
|
||||
// BatchUpdate how to produce CodeReviewRevWalks.
|
||||
if (args.mergeUtil.canFastForward(args.mergeSorter,
|
||||
mergeTip.getCurrentTip(), args.rw, toMerge)) {
|
||||
args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
|
||||
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
acceptMergeTip(mergeTip);
|
||||
args.mergeTip.moveTipTo(toMerge, toMerge);
|
||||
acceptMergeTip(args.mergeTip);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,7 +122,7 @@ public class RebaseIfNecessary extends SubmitStrategy {
|
||||
PatchSet origPs = args.psUtil.get(
|
||||
ctx.getDb(), toMerge.getControl().getNotes(), toMerge.getPatchsetId());
|
||||
rebaseOp = args.rebaseFactory.create(
|
||||
toMerge.getControl(), origPs, mergeTip.getCurrentTip().name())
|
||||
toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name())
|
||||
.setRunHooks(false)
|
||||
// Bypass approval copier since we're going to copy all approvals
|
||||
// later anyway.
|
||||
@@ -164,62 +139,58 @@ public class RebaseIfNecessary extends SubmitStrategy {
|
||||
newCommit.copyFrom(toMerge);
|
||||
newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
|
||||
newCommit.setPatchsetId(rebaseOp.getPatchSetId());
|
||||
mergeTip.moveTipTo(newCommit, newCommit);
|
||||
args.commits.put(mergeTip.getCurrentTip());
|
||||
acceptMergeTip(mergeTip);
|
||||
args.mergeTip.moveTipTo(newCommit, newCommit);
|
||||
args.commits.put(args.mergeTip.getCurrentTip());
|
||||
acceptMergeTip(args.mergeTip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateChange(ChangeContext ctx) throws NoSuchChangeException,
|
||||
InvalidChangeOperationException, OrmException, IOException {
|
||||
public void updateChangeImpl(ChangeContext ctx)
|
||||
throws NoSuchChangeException, InvalidChangeOperationException,
|
||||
OrmException, IOException {
|
||||
if (rebaseOp == null) {
|
||||
// Took the fast-forward option, nothing to do.
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
rebaseOp.updateChange(ctx);
|
||||
PatchSet.Id newPatchSetId = rebaseOp.getPatchSetId();
|
||||
List<PatchSetApproval> approvals = Lists.newArrayList();
|
||||
// Copy approvals from original patch set.
|
||||
for (PatchSetApproval a : args.approvalsUtil.byPatchSet(ctx.getDb(),
|
||||
toMerge.getControl(), toMerge.getPatchsetId())) {
|
||||
ctx.getControl(), toMerge.getPatchsetId())) {
|
||||
approvals.add(new PatchSetApproval(newPatchSetId, a));
|
||||
}
|
||||
args.db.patchSetApprovals().insert(approvals);
|
||||
|
||||
toMerge.change().setCurrentPatchSet(
|
||||
args.patchSetInfoFactory.get(args.rw, mergeTip.getCurrentTip(),
|
||||
newPatchSetId));
|
||||
newCommit.setControl(
|
||||
args.changeControlFactory.controlFor(toMerge.change(), args.caller));
|
||||
return true;
|
||||
ctx.getChange().setCurrentPatchSet(
|
||||
args.patchSetInfoFactory.get(
|
||||
args.rw, newCommit, newPatchSetId));
|
||||
newCommit.setControl(ctx.getControl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postUpdate(Context ctx) throws OrmException {
|
||||
public void postUpdateImpl(Context ctx) throws OrmException {
|
||||
if (rebaseOp != null) {
|
||||
rebaseOp.postUpdate(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RebaseMultipleParentsOp extends BatchUpdate.Op {
|
||||
private final MergeTip mergeTip;
|
||||
private final CodeReviewCommit toMerge;
|
||||
|
||||
private RebaseMultipleParentsOp(MergeTip mergeTip,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.mergeTip = mergeTip;
|
||||
this.toMerge = toMerge;
|
||||
private class RebaseMultipleParentsOp extends SubmitStrategyOp {
|
||||
private RebaseMultipleParentsOp(CodeReviewCommit toMerge) {
|
||||
super(RebaseIfNecessary.this.args, toMerge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRepo(RepoContext ctx)
|
||||
public void updateRepoImpl(RepoContext ctx)
|
||||
throws IntegrationException, IOException {
|
||||
// There are multiple parents, so this is a merge commit. We don't want
|
||||
// to rebase the merge as clients can't easily rebase their history with
|
||||
// that merge present and replaced by an equivalent merge with a different
|
||||
// first parent. So instead behave as though MERGE_IF_NECESSARY was
|
||||
// configured.
|
||||
MergeTip mergeTip = args.mergeTip;
|
||||
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
|
||||
mergeTip.moveTipTo(toMerge, toMerge);
|
||||
acceptMergeTip(mergeTip);
|
||||
|
@@ -16,23 +16,31 @@ package com.google.gerrit.server.git.strategy;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gerrit.common.ChangeHooks;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.config.FactoryModule;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.ApprovalsUtil;
|
||||
import com.google.gerrit.server.ChangeMessagesUtil;
|
||||
import com.google.gerrit.server.GerritPersonIdent;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.PatchSetUtil;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.change.RebaseChangeOp;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.EmailMerge;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.LabelNormalizer;
|
||||
import com.google.gerrit.server.git.MergeOp.CommitStatus;
|
||||
import com.google.gerrit.server.git.MergeSorter;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.git.TagCache;
|
||||
import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.project.ProjectCache;
|
||||
@@ -47,8 +55,8 @@ import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevFlag;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -67,118 +75,153 @@ public abstract class SubmitStrategy {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(dborowitz): make non-public when converting to BatchUpdate.
|
||||
public static class Arguments {
|
||||
static class Arguments {
|
||||
interface Factory {
|
||||
Arguments create(
|
||||
Branch.NameKey destBranch,
|
||||
CommitStatus commits,
|
||||
CodeReviewRevWalk rw,
|
||||
IdentifiedUser caller,
|
||||
MergeTip mergeTip,
|
||||
ObjectInserter inserter,
|
||||
Repository repo,
|
||||
RevFlag canMergeFlag,
|
||||
ReviewDb db,
|
||||
Set<RevCommit> alreadyAccepted);
|
||||
Set<RevCommit> alreadyAccepted,
|
||||
String submissionId);
|
||||
}
|
||||
|
||||
final AccountCache accountCache;
|
||||
final ApprovalsUtil approvalsUtil;
|
||||
final BatchUpdate.Factory batchUpdateFactory;
|
||||
final ChangeControl.GenericFactory changeControlFactory;
|
||||
final ChangeHooks hooks;
|
||||
final ChangeMessagesUtil cmUtil;
|
||||
final EmailMerge.Factory mergedSenderFactory;
|
||||
final GitRepositoryManager repoManager;
|
||||
final LabelNormalizer labelNormalizer;
|
||||
final PatchSetInfoFactory patchSetInfoFactory;
|
||||
final PatchSetUtil psUtil;
|
||||
final ProjectCache projectCache;
|
||||
final PersonIdent serverIdent;
|
||||
final RebaseChangeOp.Factory rebaseFactory;
|
||||
final TagCache tagCache;
|
||||
|
||||
final Branch.NameKey destBranch;
|
||||
// TODO(dborowitz): make non-public when converting to BatchUpdate.
|
||||
public final CodeReviewRevWalk rw;
|
||||
final CodeReviewRevWalk rw;
|
||||
final CommitStatus commits;
|
||||
final IdentifiedUser caller;
|
||||
final MergeTip mergeTip;
|
||||
final ObjectInserter inserter;
|
||||
final Repository repo;
|
||||
// TODO(dborowitz): make non-public when converting to BatchUpdate.
|
||||
public final RevFlag canMergeFlag;
|
||||
final RevFlag canMergeFlag;
|
||||
final ReviewDb db;
|
||||
final Set<RevCommit> alreadyAccepted;
|
||||
final String submissionId;
|
||||
|
||||
final ProjectState project;
|
||||
final MergeSorter mergeSorter;
|
||||
// TODO(dborowitz): make non-public when converting to BatchUpdate.
|
||||
public final MergeUtil mergeUtil;
|
||||
final MergeUtil mergeUtil;
|
||||
|
||||
@AssistedInject
|
||||
Arguments(
|
||||
AccountCache accountCache,
|
||||
ApprovalsUtil approvalsUtil,
|
||||
BatchUpdate.Factory batchUpdateFactory,
|
||||
ChangeControl.GenericFactory changeControlFactory,
|
||||
ChangeHooks hooks,
|
||||
ChangeMessagesUtil cmUtil,
|
||||
EmailMerge.Factory mergedSenderFactory,
|
||||
GitRepositoryManager repoManager,
|
||||
LabelNormalizer labelNormalizer,
|
||||
MergeUtil.Factory mergeUtilFactory,
|
||||
PatchSetInfoFactory patchSetInfoFactory,
|
||||
PatchSetUtil psUtil,
|
||||
@GerritPersonIdent PersonIdent serverIdent,
|
||||
ProjectCache projectCache,
|
||||
RebaseChangeOp.Factory rebaseFactory,
|
||||
TagCache tagCache,
|
||||
@Assisted Branch.NameKey destBranch,
|
||||
@Assisted CommitStatus commits,
|
||||
@Assisted CodeReviewRevWalk rw,
|
||||
@Assisted IdentifiedUser caller,
|
||||
@Assisted MergeTip mergeTip,
|
||||
@Assisted ObjectInserter inserter,
|
||||
@Assisted Repository repo,
|
||||
@Assisted RevFlag canMergeFlag,
|
||||
@Assisted ReviewDb db,
|
||||
@Assisted Set<RevCommit> alreadyAccepted) {
|
||||
@Assisted Set<RevCommit> alreadyAccepted,
|
||||
@Assisted String submissionId) {
|
||||
this.accountCache = accountCache;
|
||||
this.approvalsUtil = approvalsUtil;
|
||||
this.batchUpdateFactory = batchUpdateFactory;
|
||||
this.changeControlFactory = changeControlFactory;
|
||||
this.hooks = hooks;
|
||||
this.mergedSenderFactory = mergedSenderFactory;
|
||||
this.repoManager = repoManager;
|
||||
this.cmUtil = cmUtil;
|
||||
this.labelNormalizer = labelNormalizer;
|
||||
this.patchSetInfoFactory = patchSetInfoFactory;
|
||||
this.psUtil = psUtil;
|
||||
this.projectCache = projectCache;
|
||||
this.rebaseFactory = rebaseFactory;
|
||||
this.tagCache = tagCache;
|
||||
|
||||
this.serverIdent = serverIdent;
|
||||
this.destBranch = destBranch;
|
||||
this.commits = commits;
|
||||
this.rw = rw;
|
||||
this.caller = caller;
|
||||
this.mergeTip = mergeTip;
|
||||
this.inserter = inserter;
|
||||
this.repo = repo;
|
||||
this.canMergeFlag = canMergeFlag;
|
||||
this.db = db;
|
||||
this.alreadyAccepted = alreadyAccepted;
|
||||
this.submissionId = submissionId;
|
||||
|
||||
this.project = checkNotNull(projectCache.get(destBranch.getParentKey()),
|
||||
"project not found: %s", destBranch.getParentKey());
|
||||
this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
|
||||
this.mergeUtil = mergeUtilFactory.create(project);
|
||||
}
|
||||
|
||||
BatchUpdate newBatchUpdate(Timestamp when) {
|
||||
return batchUpdateFactory
|
||||
.create(db, destBranch.getParentKey(), caller, when)
|
||||
.setRepository(repo, rw, inserter);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dborowitz): make non-public when converting to BatchUpdate.
|
||||
public final Arguments args;
|
||||
final Arguments args;
|
||||
|
||||
SubmitStrategy(Arguments args) {
|
||||
this.args = checkNotNull(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this submit strategy.
|
||||
* Add operations to a batch update that execute this submit strategy.
|
||||
* <p>
|
||||
* If possible, the provided commits will be merged with this submit strategy.
|
||||
* Guarantees exactly one op is added to the update for each change in the
|
||||
* input set.
|
||||
*
|
||||
* @param currentTip the mergeTip
|
||||
* @param toMerge the list of submitted commits that should be merged using
|
||||
* this submit strategy. Implementations are responsible for ordering
|
||||
* of commits, and should not modify the input in place.
|
||||
* @return the new merge tip.
|
||||
* @throws IntegrationException
|
||||
* @param bu batch update to add operations to.
|
||||
* @param toMerge the set of submitted commits that should be merged using
|
||||
* this submit strategy. Implementations are responsible for ordering of
|
||||
* commits, and will not modify the input in place.
|
||||
* @throws IntegrationException if an error occurred initializing the
|
||||
* operations (as opposed to an error during execution, which will be
|
||||
* reported only when the batch update executes the operations).
|
||||
*/
|
||||
public abstract MergeTip run(CodeReviewCommit currentTip,
|
||||
public final void addOps(BatchUpdate bu, Set<CodeReviewCommit> toMerge)
|
||||
throws IntegrationException {
|
||||
List<SubmitStrategyOp> ops = buildOps(toMerge);
|
||||
Set<CodeReviewCommit> added = Sets.newHashSetWithExpectedSize(ops.size());
|
||||
for (SubmitStrategyOp op : ops) {
|
||||
bu.addOp(op.getId(), op);
|
||||
added.add(op.getCommit());
|
||||
}
|
||||
|
||||
// Fill in ops for any implicitly merged changes.
|
||||
for (CodeReviewCommit c : Sets.difference(toMerge, added)) {
|
||||
bu.addOp(c.change().getId(), new ImplicitIntegrateOp(args, c));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract List<SubmitStrategyOp> buildOps(
|
||||
Collection<CodeReviewCommit> toMerge) throws IntegrationException;
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeOp.CommitStatus;
|
||||
import com.google.gerrit.server.git.MergeTip;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@@ -49,10 +50,12 @@ public class SubmitStrategyFactory {
|
||||
public SubmitStrategy create(SubmitType submitType, ReviewDb db,
|
||||
Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
|
||||
RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
|
||||
Branch.NameKey destBranch, IdentifiedUser caller, CommitStatus commits)
|
||||
Branch.NameKey destBranch, IdentifiedUser caller, MergeTip mergeTip,
|
||||
CommitStatus commits, String submissionId)
|
||||
throws IntegrationException {
|
||||
SubmitStrategy.Arguments args = argsFactory.create(destBranch, commits, rw,
|
||||
caller, inserter, repo, canMergeFlag, db, alreadyAccepted);
|
||||
caller, mergeTip, inserter, repo, canMergeFlag, db, alreadyAccepted,
|
||||
submissionId);
|
||||
switch (submitType) {
|
||||
case CHERRY_PICK:
|
||||
return new CherryPick(args);
|
||||
|
@@ -0,0 +1,120 @@
|
||||
// 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.google.gerrit.server.git.strategy;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.MergeOp.CommitStatus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
public class SubmitStrategyListener extends BatchUpdate.Listener {
|
||||
private final Collection<SubmitStrategy> strategies;
|
||||
private final CommitStatus commits;
|
||||
|
||||
public SubmitStrategyListener(Collection<SubmitStrategy> strategies,
|
||||
CommitStatus commits) {
|
||||
this.strategies = strategies;
|
||||
this.commits = commits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterUpdateRepos() throws ResourceConflictException {
|
||||
try {
|
||||
markCleanMerges();
|
||||
checkCommitStatus();
|
||||
findUnmergedChanges();
|
||||
} catch (IntegrationException e) {
|
||||
throw new ResourceConflictException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void findUnmergedChanges()
|
||||
throws ResourceConflictException, IntegrationException {
|
||||
for (SubmitStrategy strategy : strategies) {
|
||||
if (strategy instanceof CherryPick) {
|
||||
// Might have picked a subset of changes, can't do this sanity check.
|
||||
continue;
|
||||
}
|
||||
SubmitStrategy.Arguments args = strategy.args;
|
||||
Set<Change.Id> unmerged = args.mergeUtil.findUnmergedChanges(
|
||||
args.commits.getChangeIds(args.destBranch), args.rw,
|
||||
args.canMergeFlag, args.mergeTip.getInitialTip(),
|
||||
args.mergeTip.getCurrentTip());
|
||||
for (Change.Id id : unmerged) {
|
||||
commits.problem(id,
|
||||
"internal error: change not reachable from new branch tip");
|
||||
}
|
||||
}
|
||||
commits.maybeFailVerbose();
|
||||
}
|
||||
|
||||
private void markCleanMerges() throws IntegrationException {
|
||||
for (SubmitStrategy strategy : strategies) {
|
||||
SubmitStrategy.Arguments args = strategy.args;
|
||||
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
|
||||
args.mergeTip.getCurrentTip(), args.alreadyAccepted);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCommitStatus() throws ResourceConflictException {
|
||||
for (Change.Id id : commits.getChangeIds()) {
|
||||
CodeReviewCommit commit = commits.get(id);
|
||||
CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
|
||||
if (s == null) {
|
||||
commits.problem(id,
|
||||
"internal error: change not processed by merge strategy");
|
||||
return;
|
||||
}
|
||||
switch (s) {
|
||||
case CLEAN_MERGE:
|
||||
case CLEAN_REBASE:
|
||||
case CLEAN_PICK:
|
||||
case ALREADY_MERGED:
|
||||
break; // Merge strategy accepted this change.
|
||||
|
||||
case PATH_CONFLICT:
|
||||
case REBASE_MERGE_CONFLICT:
|
||||
case MANUAL_RECURSIVE_MERGE:
|
||||
case CANNOT_CHERRY_PICK_ROOT:
|
||||
case NOT_FAST_FORWARD:
|
||||
// TODO(dborowitz): Reformat these messages to be more appropriate for
|
||||
// short problem descriptions.
|
||||
commits.problem(id,
|
||||
CharMatcher.is('\n').collapseFrom(s.getMessage(), ' '));
|
||||
break;
|
||||
|
||||
case MISSING_DEPENDENCY:
|
||||
commits.problem(id, "depends on change that was not submitted");
|
||||
break;
|
||||
|
||||
default:
|
||||
commits.problem(id, "unspecified merge failure: " + s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
commits.maybeFailVerbose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterUpdateChanges() throws ResourceConflictException {
|
||||
commits.maybeFail("Error updating status");
|
||||
}
|
||||
}
|
@@ -0,0 +1,411 @@
|
||||
// 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.google.gerrit.server.git.strategy;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gerrit.common.data.SubmitRecord;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||
import com.google.gerrit.reviewdb.client.LabelId;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.ChangeUtil;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.git.BatchUpdate;
|
||||
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
|
||||
import com.google.gerrit.server.git.BatchUpdate.Context;
|
||||
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.IntegrationException;
|
||||
import com.google.gerrit.server.git.LabelNormalizer;
|
||||
import com.google.gerrit.server.git.ProjectConfig;
|
||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||
import com.google.gerrit.server.project.ProjectState;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
abstract class SubmitStrategyOp extends BatchUpdate.Op {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SubmitStrategyOp.class);
|
||||
|
||||
protected final SubmitStrategy.Arguments args;
|
||||
protected final CodeReviewCommit toMerge;
|
||||
|
||||
private ReceiveCommand command;
|
||||
private PatchSetApproval submitter;
|
||||
private ObjectId mergeResultRev;
|
||||
private PatchSet mergedPatchSet;
|
||||
private Change updatedChange;
|
||||
|
||||
protected SubmitStrategyOp(SubmitStrategy.Arguments args,
|
||||
CodeReviewCommit toMerge) {
|
||||
this.args = args;
|
||||
this.toMerge = toMerge;
|
||||
}
|
||||
|
||||
final Change.Id getId() {
|
||||
return toMerge.change().getId();
|
||||
}
|
||||
|
||||
final CodeReviewCommit getCommit() {
|
||||
return toMerge;
|
||||
}
|
||||
|
||||
protected final Branch.NameKey getDest() {
|
||||
return toMerge.change().getDest();
|
||||
}
|
||||
|
||||
protected final Project.NameKey getProject() {
|
||||
return getDest().getParentKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void updateRepo(RepoContext ctx) throws Exception {
|
||||
// Run the submit strategy implementation and record the merge tip state so
|
||||
// we can create the ref update.
|
||||
CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip();
|
||||
updateRepoImpl(ctx);
|
||||
CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
|
||||
|
||||
if (Objects.equals(tipBefore, tipAfter)) {
|
||||
return;
|
||||
} else if (tipAfter == null) {
|
||||
logDebug("No merge tip, no update to perform");
|
||||
return;
|
||||
}
|
||||
|
||||
checkProjectConfig(ctx, tipAfter);
|
||||
|
||||
// Needed by postUpdate, at which point mergeTip will have advanced further,
|
||||
// so it's easier to just snapshot the command.
|
||||
command = new ReceiveCommand(
|
||||
firstNonNull(tipBefore, ObjectId.zeroId()),
|
||||
tipAfter,
|
||||
getDest().get());
|
||||
ctx.addRefUpdate(command);
|
||||
}
|
||||
|
||||
private void checkProjectConfig(RepoContext ctx, CodeReviewCommit commit)
|
||||
throws IntegrationException {
|
||||
String refName = getDest().get();
|
||||
if (RefNames.REFS_CONFIG.equals(refName)) {
|
||||
logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
|
||||
try {
|
||||
ProjectConfig cfg = new ProjectConfig(getProject());
|
||||
cfg.load(ctx.getRepository(), commit);
|
||||
} catch (Exception e) {
|
||||
throw new IntegrationException("Submit would store invalid"
|
||||
+ " project configuration " + commit.name() + " for "
|
||||
+ getProject(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean updateChange(ChangeContext ctx) throws Exception {
|
||||
toMerge.setControl(ctx.getControl()); // Update change and notes from ctx.
|
||||
updateChangeImpl(ctx);
|
||||
|
||||
Change c = ctx.getChange();
|
||||
Change.Id id = c.getId();
|
||||
try {
|
||||
CodeReviewCommit commit = args.commits.get(id);
|
||||
CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
|
||||
logDebug("Status of change {} ({}) on {}: {}", id, commit.name(),
|
||||
c.getDest(), s);
|
||||
checkState(s != null,
|
||||
"status not set for change %s; expected to previously fail fast",
|
||||
id);
|
||||
setApproval(ctx, args.caller);
|
||||
|
||||
mergeResultRev = args.mergeTip != null
|
||||
? args.mergeTip.getMergeResults().get(commit) : null;
|
||||
String txt = s.getMessage();
|
||||
|
||||
ChangeMessage msg;
|
||||
if (s == CommitMergeStatus.CLEAN_MERGE) {
|
||||
msg = message(ctx, txt + getByAccountName());
|
||||
} else if (s == CommitMergeStatus.CLEAN_REBASE
|
||||
|| s == CommitMergeStatus.CLEAN_PICK) {
|
||||
msg = message(ctx, txt + " as " + commit.name() + getByAccountName());
|
||||
} else if (s == CommitMergeStatus.ALREADY_MERGED) {
|
||||
msg = null;
|
||||
} else {
|
||||
throw new IllegalStateException("unexpected status " + s +
|
||||
" for change " + c.getId() + "; expected to previously fail fast");
|
||||
}
|
||||
setMerged(ctx, msg);
|
||||
} catch (OrmException err) {
|
||||
String msg = "Error updating change status for " + id;
|
||||
log.error(msg, err);
|
||||
args.commits.logProblem(id, msg);
|
||||
// It's possible this happened before updating anything in the db, but
|
||||
// it's hard to know for sure, so just return true below to be safe.
|
||||
}
|
||||
updatedChange = c;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setApproval(ChangeContext ctx, IdentifiedUser user)
|
||||
throws OrmException {
|
||||
Change.Id id = ctx.getChange().getId();
|
||||
List<SubmitRecord> records = args.commits.getSubmitRecords(id);
|
||||
PatchSet.Id oldPsId = toMerge.getPatchsetId();
|
||||
PatchSet.Id newPsId = ctx.getChange().currentPatchSetId();
|
||||
|
||||
logDebug("Add approval for " + id);
|
||||
ChangeUpdate origPsUpdate = ctx.getUpdate(oldPsId);
|
||||
origPsUpdate.putReviewer(user.getAccountId(), REVIEWER);
|
||||
LabelNormalizer.Result normalized = approve(ctx, origPsUpdate);
|
||||
|
||||
ChangeUpdate newPsUpdate = ctx.getUpdate(newPsId);
|
||||
newPsUpdate.merge(records);
|
||||
// If the submit strategy created a new revision (rebase, cherry-pick), copy
|
||||
// approvals as well.
|
||||
if (!newPsId.equals(oldPsId)) {
|
||||
saveApprovals(normalized, ctx, newPsUpdate, true);
|
||||
submitter = convertPatchSet(newPsId).apply(submitter);
|
||||
}
|
||||
}
|
||||
|
||||
private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
|
||||
throws OrmException {
|
||||
PatchSet.Id psId = update.getPatchSetId();
|
||||
Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
|
||||
for (PatchSetApproval psa : args.approvalsUtil.byPatchSet(
|
||||
ctx.getDb(), ctx.getControl(), psId)) {
|
||||
byKey.put(psa.getKey(), psa);
|
||||
}
|
||||
|
||||
submitter = new PatchSetApproval(
|
||||
new PatchSetApproval.Key(
|
||||
psId,
|
||||
ctx.getUser().getAccountId(),
|
||||
LabelId.SUBMIT),
|
||||
(short) 1, ctx.getWhen());
|
||||
byKey.put(submitter.getKey(), submitter);
|
||||
submitter.setValue((short) 1);
|
||||
submitter.setGranted(ctx.getWhen());
|
||||
|
||||
// Flatten out existing approvals for this patch set based upon the current
|
||||
// permissions. Once the change is closed the approvals are not updated at
|
||||
// presentation view time, except for zero votes used to indicate a reviewer
|
||||
// was added. So we need to make sure votes are accurate now. This way if
|
||||
// permissions get modified in the future, historical records stay accurate.
|
||||
LabelNormalizer.Result normalized =
|
||||
args.labelNormalizer.normalize(ctx.getControl(), byKey.values());
|
||||
update.putApproval(submitter.getLabel(), submitter.getValue());
|
||||
saveApprovals(normalized, ctx, update, false);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void saveApprovals(LabelNormalizer.Result normalized,
|
||||
ChangeContext ctx, ChangeUpdate update, boolean includeUnchanged)
|
||||
throws OrmException {
|
||||
PatchSet.Id psId = update.getPatchSetId();
|
||||
ctx.getDb().patchSetApprovals().upsert(
|
||||
convertPatchSet(normalized.getNormalized(), psId));
|
||||
ctx.getDb().patchSetApprovals().delete(
|
||||
convertPatchSet(normalized.deleted(), psId));
|
||||
for (PatchSetApproval psa : normalized.updated()) {
|
||||
update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
|
||||
}
|
||||
for (PatchSetApproval psa : normalized.deleted()) {
|
||||
update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Don't use a label in notedb; just check when status
|
||||
// change happened.
|
||||
for (PatchSetApproval psa : normalized.unchanged()) {
|
||||
if (includeUnchanged || psa.isSubmit()) {
|
||||
logDebug("Adding submit label " + psa);
|
||||
update.putApprovalFor(
|
||||
psa.getAccountId(), psa.getLabel(), psa.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Function<PatchSetApproval, PatchSetApproval>
|
||||
convertPatchSet(final PatchSet.Id psId) {
|
||||
return new Function<PatchSetApproval, PatchSetApproval>() {
|
||||
@Override
|
||||
public PatchSetApproval apply(PatchSetApproval in) {
|
||||
if (in.getPatchSetId().equals(psId)) {
|
||||
return in;
|
||||
} else {
|
||||
return new PatchSetApproval(psId, in);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Iterable<PatchSetApproval> convertPatchSet(
|
||||
Iterable<PatchSetApproval> approvals, PatchSet.Id psId) {
|
||||
return Iterables.transform(approvals, convertPatchSet(psId));
|
||||
}
|
||||
|
||||
private String getByAccountName() {
|
||||
checkNotNull(submitter,
|
||||
"getByAccountName called before submitter populated");
|
||||
Account account =
|
||||
args.accountCache.get(submitter.getAccountId()).getAccount();
|
||||
if (account != null && account.getFullName() != null) {
|
||||
return " by " + account.getFullName();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private ChangeMessage message(ChangeContext ctx, String body) {
|
||||
String uuid;
|
||||
try {
|
||||
uuid = ChangeUtil.messageUUID(ctx.getDb());
|
||||
} catch (OrmException e) {
|
||||
return null;
|
||||
}
|
||||
ChangeMessage m = new ChangeMessage(
|
||||
new ChangeMessage.Key(ctx.getChange().getId(), uuid),
|
||||
// TODO(dborowitz): Pre-BatchUpdate behavior wrote the merged message on
|
||||
// the old patch set ID, so that's what we do here. I don't think this
|
||||
// was intentional, and it should be changed.
|
||||
null, ctx.getWhen(), toMerge.change().currentPatchSetId());
|
||||
m.setMessage(body);
|
||||
return m;
|
||||
}
|
||||
|
||||
private void setMerged(ChangeContext ctx, ChangeMessage msg)
|
||||
throws OrmException {
|
||||
Change c = ctx.getChange();
|
||||
ReviewDb db = ctx.getDb();
|
||||
logDebug("Setting change {} merged", c.getId());
|
||||
// TODO(dborowitz): Use PatchSetUtil? But we don't have a recent notes.
|
||||
mergedPatchSet = db.patchSets().get(c.currentPatchSetId());
|
||||
c.setStatus(Change.Status.MERGED);
|
||||
c.setSubmissionId(args.submissionId);
|
||||
ctx.saveChange();
|
||||
|
||||
// TODO(dborowitz): We need to be able to change the author of the message,
|
||||
// which is not the user from the update context. addMergedMessage was able
|
||||
// to do this in the past.
|
||||
if (msg != null) {
|
||||
args.cmUtil.addChangeMessage(db, ctx.getUpdate(msg.getPatchSetId()), msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void postUpdate(Context ctx) throws Exception {
|
||||
postUpdateImpl(ctx);
|
||||
|
||||
if (command != null) {
|
||||
args.tagCache.updateFastForward(
|
||||
getProject(),
|
||||
command.getRefName(),
|
||||
command.getOldId(),
|
||||
command.getNewId());
|
||||
// TODO(dborowitz): Move to BatchUpdate? Would also allow us to run once
|
||||
// per project even if multiple changes to refs/meta/config are submitted.
|
||||
if (RefNames.REFS_CONFIG.equals(getDest().get())) {
|
||||
args.projectCache.evict(getProject());
|
||||
ProjectState p = args.projectCache.get(getProject());
|
||||
args.repoManager.setProjectDescription(
|
||||
p.getProject().getNameKey(), p.getProject().getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
// Assume the change must have been merged at this point, otherwise we would
|
||||
// have failed fast in one of the other steps.
|
||||
try {
|
||||
args.mergedSenderFactory.create(getId(), submitter.getAccountId())
|
||||
.sendAsync();
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot email merged notification for " + getId(), e);
|
||||
}
|
||||
if (mergeResultRev != null) {
|
||||
try {
|
||||
args.hooks.doChangeMergedHook(updatedChange,
|
||||
args.accountCache.get(submitter.getAccountId()).getAccount(),
|
||||
mergedPatchSet, ctx.getDb(), mergeResultRev.name());
|
||||
} catch (OrmException ex) {
|
||||
logError("Cannot run hook for submitted patch set " + getId(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #updateRepo(RepoContext)
|
||||
* @param ctx
|
||||
*/
|
||||
protected void updateRepoImpl(RepoContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #updateChange(ChangeContext)
|
||||
* @param ctx
|
||||
*/
|
||||
protected void updateChangeImpl(ChangeContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #postUpdate(Context)
|
||||
* @param ctx
|
||||
*/
|
||||
protected void postUpdateImpl(Context ctx) throws Exception {
|
||||
}
|
||||
|
||||
protected final void logDebug(String msg, Object... args) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[" + this.args.submissionId + "]" + msg, args);
|
||||
}
|
||||
}
|
||||
|
||||
protected final void logWarn(String msg, Throwable t) {
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn("[" + args.submissionId + "]" + msg, t);
|
||||
}
|
||||
}
|
||||
|
||||
protected void logError(String msg, Throwable t) {
|
||||
if (log.isErrorEnabled()) {
|
||||
if (t != null) {
|
||||
log.error("[" + args.submissionId + "]" + msg, t);
|
||||
} else {
|
||||
log.error("[" + args.submissionId + "]" + msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void logError(String msg) {
|
||||
logError(msg, null);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user