diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java | 1317 |
1 files changed, 415 insertions, 902 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java index 2e8f183f0d..dace0ae90b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java @@ -14,30 +14,35 @@ package com.google.gerrit.server.git; +import static com.google.gerrit.server.git.MergeUtil.getSubmitter; +import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import com.google.common.base.Objects; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Sets; import com.google.gerrit.common.ChangeHooks; -import com.google.gerrit.common.data.ApprovalType; -import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.data.Capable; +import com.google.gerrit.common.data.LabelTypes; +import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.ApprovalCategory; 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.PatchSet; -import com.google.gerrit.reviewdb.client.PatchSetAncestor; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.Project.SubmitType; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeUtil; -import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.AllProjectsName; -import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.mail.MergeFailSender; import com.google.gerrit.server.mail.MergedSender; @@ -45,24 +50,20 @@ 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; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.util.RequestScopePropagator; -import com.google.gerrit.server.workflow.CategoryFunction; -import com.google.gerrit.server.workflow.FunctionState; import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmConcurrencyException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -70,11 +71,6 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.merge.MergeStrategy; -import org.eclipse.jgit.merge.Merger; -import org.eclipse.jgit.merge.ThreeWayMerger; -import org.eclipse.jgit.revwalk.FooterKey; -import org.eclipse.jgit.revwalk.FooterLine; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevSort; @@ -83,21 +79,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; -import java.util.TimeZone; - -import javax.annotation.Nullable; /** * Merges changes in submission order into a single branch. @@ -119,53 +109,47 @@ public class MergeOp { } private static final Logger log = LoggerFactory.getLogger(MergeOp.class); - private static final String R_HEADS_MASTER = - Constants.R_HEADS + Constants.MASTER; - private static final ApprovalCategory.Id CRVW = - new ApprovalCategory.Id("CRVW"); - private static final ApprovalCategory.Id VRIF = - new ApprovalCategory.Id("VRIF"); - private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on"); - private static final FooterKey CHANGE_ID = new FooterKey("Change-Id"); /** Amount of time to wait between submit and checking for missing deps. */ private static final long DEPENDENCY_DELAY = MILLISECONDS.convert(15, MINUTES); + private static final long LOCK_FAILURE_RETRY_DELAY = + MILLISECONDS.convert(15, SECONDS); + + private static final long DUPLICATE_MESSAGE_INTERVAL = + MILLISECONDS.convert(1, DAYS); + private final GitRepositoryManager repoManager; private final SchemaFactory<ReviewDb> schemaFactory; private final ProjectCache projectCache; - private final FunctionState.Factory functionState; - private final GitReferenceUpdated replication; + private final LabelNormalizer labelNormalizer; + private final GitReferenceUpdated gitRefUpdated; private final MergedSender.Factory mergedSenderFactory; private final MergeFailSender.Factory mergeFailSenderFactory; - private final Provider<String> urlProvider; - private final ApprovalTypes approvalTypes; private final PatchSetInfoFactory patchSetInfoFactory; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final ChangeControl.GenericFactory changeControlFactory; private final MergeQueue mergeQueue; - private final PersonIdent myIdent; private final Branch.NameKey destBranch; - private Project destProject; - private final List<CodeReviewCommit> toMerge; - private List<Change> submitted; + private ProjectState destProject; + private final ListMultimap<SubmitType, CodeReviewCommit> toMerge; + private final List<CodeReviewCommit> potentiallyStillSubmittable; private final Map<Change.Id, CodeReviewCommit> commits; private ReviewDb db; private Repository repo; private RevWalk rw; - private RevFlag CAN_MERGE; + private RevFlag canMergeFlag; private CodeReviewCommit branchTip; private CodeReviewCommit mergeTip; - private Set<RevCommit> alreadyAccepted; - private RefUpdate branchUpdate; private ObjectInserter inserter; + private PersonIdent refLogIdent; private final ChangeHooks hooks; private final AccountCache accountCache; private final TagCache tagCache; - private final CreateCodeReviewNotes.Factory codeReviewNotesFactory; + private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; private final WorkQueue workQueue; private final RequestScopePropagator requestScopePropagator; @@ -173,30 +157,27 @@ public class MergeOp { @Inject MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf, - final ProjectCache pc, final FunctionState.Factory fs, - final GitReferenceUpdated rq, final MergedSender.Factory msf, + final ProjectCache pc, final LabelNormalizer fs, + final GitReferenceUpdated gru, final MergedSender.Factory msf, final MergeFailSender.Factory mfsf, - @CanonicalWebUrl @Nullable final Provider<String> cwu, - final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif, + final LabelTypes labelTypes, final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf, final ChangeControl.GenericFactory changeControlFactory, - @GerritPersonIdent final PersonIdent myIdent, final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch, final ChangeHooks hooks, final AccountCache accountCache, - final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf, + final TagCache tagCache, + final SubmitStrategyFactory submitStrategyFactory, final SubmoduleOp.Factory subOpFactory, final WorkQueue workQueue, final RequestScopePropagator requestScopePropagator, final AllProjectsName allProjectsName) { repoManager = grm; schemaFactory = sf; - functionState = fs; + labelNormalizer = fs; projectCache = pc; - replication = rq; + gitRefUpdated = gru; mergedSenderFactory = msf; mergeFailSenderFactory = mfsf; - urlProvider = cwu; - this.approvalTypes = approvalTypes; patchSetInfoFactory = psif; identifiedUserFactory = iuf; this.changeControlFactory = changeControlFactory; @@ -204,24 +185,22 @@ public class MergeOp { this.hooks = hooks; this.accountCache = accountCache; this.tagCache = tagCache; - codeReviewNotesFactory = crnf; + this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; this.workQueue = workQueue; this.requestScopePropagator = requestScopePropagator; this.allProjectsName = allProjectsName; - this.myIdent = myIdent; destBranch = branch; - toMerge = new ArrayList<CodeReviewCommit>(); + toMerge = ArrayListMultimap.create(); + potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>(); commits = new HashMap<Change.Id, CodeReviewCommit>(); } - public void verifyMergeability(Change change) { + public void verifyMergeability(Change change) throws NoSuchProjectException { try { setDestProject(); openRepository(); final Ref destBranchRef = repo.getRef(destBranch.get()); - submitted = new ArrayList<Change>(); - submitted.add(change); // Test mergeability of the change if the last merged sha1 // in the branch is different from the last sha1 @@ -231,17 +210,27 @@ public class MergeOp { || (destBranchRef != null && !destBranchRef.getObjectId().getName() .equals(change.getLastSha1MergeTested().get()))) { openSchema(); - preMerge(); - - // update sha1 tested merge. - if (destBranchRef != null) { - change.setLastSha1MergeTested(new RevId(destBranchRef - .getObjectId().getName())); + openBranch(); + validateChangeList(Collections.singletonList(change)); + if (!toMerge.isEmpty()) { + final Entry<SubmitType, CodeReviewCommit> e = + toMerge.entries().iterator().next(); + final boolean isMergeable = + createStrategy(e.getKey()).dryRun(branchTip, e.getValue()); + + // update sha1 tested merge. + if (destBranchRef != null) { + change.setLastSha1MergeTested(new RevId(destBranchRef + .getObjectId().getName())); + } else { + change.setLastSha1MergeTested(new RevId("")); + } + change.setMergeable(isMergeable); + db.changes().update(Collections.singleton(change)); } else { - change.setLastSha1MergeTested(new RevId("")); + log.error("Test merge attempt for change: " + change.getId() + + " failed"); } - change.setMergeable(isMergeable(change)); - db.changes().update(Collections.singleton(change)); } } catch (MergeException e) { log.error("Test merge attempt for change: " + change.getId() @@ -263,11 +252,10 @@ public class MergeOp { } private void setDestProject() throws MergeException { - final ProjectState pe = projectCache.get(destBranch.getParentKey()); - if (pe == null) { + destProject = projectCache.get(destBranch.getParentKey()); + if (destProject == null) { throw new MergeException("No such project: " + destBranch.getParentKey()); } - destProject = pe.getProject(); } private void openSchema() throws OrmException { @@ -276,16 +264,59 @@ public class MergeOp { } } - public void merge() throws MergeException { + public void merge() throws MergeException, NoSuchProjectException { setDestProject(); try { openSchema(); openRepository(); - submitted = db.changes().submitted(destBranch).toList(); - preMerge(); - updateBranch(); - updateChangeStatus(); - updateSubscriptions(); + openBranch(); + final ListMultimap<SubmitType, Change> toSubmit = + validateChangeList(db.changes().submitted(destBranch).toList()); + + final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn = + ArrayListMultimap.create(); + final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun = + new ArrayList<CodeReviewCommit>(); + while (!toMerge.isEmpty()) { + toMergeNextTurn.clear(); + final Set<SubmitType> submitTypes = + new HashSet<Project.SubmitType>(toMerge.keySet()); + for (final SubmitType submitType : submitTypes) { + final RefUpdate branchUpdate = openBranch(); + final SubmitStrategy strategy = createStrategy(submitType); + preMerge(strategy, toMerge.get(submitType)); + updateBranch(strategy, branchUpdate); + updateChangeStatus(toSubmit.get(submitType)); + updateSubscriptions(toSubmit.get(submitType)); + + for (final Iterator<CodeReviewCommit> it = + potentiallyStillSubmittable.iterator(); it.hasNext();) { + final CodeReviewCommit commit = it.next(); + if (containsMissingCommits(toMerge, commit) + || containsMissingCommits(toMergeNextTurn, commit)) { + // change has missing dependencies, but all commits which are + // missing are still attempted to be merged with another submit + // strategy, retry to merge this commit in the next turn + it.remove(); + commit.statusCode = null; + commit.missing = null; + toMergeNextTurn.put(submitType, commit); + } + } + potentiallyStillSubmittableOnNextRun.addAll(potentiallyStillSubmittable); + potentiallyStillSubmittable.clear(); + } + toMerge.clear(); + toMerge.putAll(toMergeNextTurn); + } + + for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) { + final Capable capable = isSubmitStillPossible(commit); + if (capable != Capable.OK) { + sendMergeFail(commit.change, + message(commit.change, capable.getMessage()), false); + } + } } catch (OrmException e) { throw new MergeException("Cannot query the database", e); } finally { @@ -304,24 +335,59 @@ public class MergeOp { } } - private void preMerge() throws MergeException, OrmException { - openBranch(); - validateChangeList(); - mergeTip = branchTip; - switch (destProject.getSubmitType()) { - case CHERRY_PICK: - cherryPickChanges(); - break; - - case FAST_FORWARD_ONLY: - case MERGE_ALWAYS: - case MERGE_IF_NECESSARY: - default: - reduceToMinimalMerge(); - mergeTopics(); - markCleanMerges(); - break; + private boolean containsMissingCommits( + final ListMultimap<SubmitType, CodeReviewCommit> map, + final CodeReviewCommit commit) { + if (!isSubmitForMissingCommitsStillPossible(commit)) { + return false; + } + + for (final CodeReviewCommit missingCommit : commit.missing) { + if (!map.containsValue(missingCommit)) { + return false; + } } + return true; + } + + private boolean isSubmitForMissingCommitsStillPossible(final CodeReviewCommit commit) { + if (commit.missing == null || commit.missing.isEmpty()) { + return false; + } + + for (CodeReviewCommit missingCommit : commit.missing) { + loadChangeInfo(missingCommit); + + if (missingCommit.patchsetId == null) { + // The commit doesn't have a patch set, so it cannot be + // submitted to the branch. + // + return false; + } + + if (!missingCommit.change.currentPatchSetId().equals( + missingCommit.patchsetId)) { + // If the missing commit is not the current patch set, + // the change must be rebased to use the proper parent. + // + return false; + } + } + + return true; + } + + private void preMerge(final SubmitStrategy strategy, + final List<CodeReviewCommit> toMerge) throws MergeException { + mergeTip = strategy.run(branchTip, toMerge); + refLogIdent = strategy.getRefLogIdent(); + commits.putAll(strategy.getNewCommits()); + } + + private SubmitStrategy createStrategy(final SubmitType submitType) + throws MergeException, NoSuchProjectException { + return submitStrategyFactory.create(submitType, db, repo, rw, inserter, + canMergeFlag, getAlreadyAccepted(branchTip), destBranch); } private void openRepository() throws MergeException { @@ -344,20 +410,17 @@ public class MergeOp { }; rw.sort(RevSort.TOPO); rw.sort(RevSort.COMMIT_TIME_DESC, true); - CAN_MERGE = rw.newFlag("CAN_MERGE"); + canMergeFlag = rw.newFlag("CAN_MERGE"); inserter = repo.newObjectInserter(); } - private void openBranch() throws MergeException { - alreadyAccepted = new HashSet<RevCommit>(); - + private RefUpdate openBranch() throws MergeException, OrmException { try { - branchUpdate = repo.updateRef(destBranch.get()); + final RefUpdate branchUpdate = repo.updateRef(destBranch.get()); if (branchUpdate.getOldObjectId() != null) { branchTip = (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId()); - alreadyAccepted.add(branchTip); } else { branchTip = null; } @@ -369,14 +432,31 @@ public class MergeOp { } else if (repo.getFullBranch().equals(destBranch.get())) { branchUpdate.setExpectedOldObjectId(ObjectId.zeroId()); } else { - throw new MergeException("Destination branch \"" - + branchUpdate.getRef().getName() + "\" does not exist"); + for (final Change c : db.changes().submitted(destBranch).toList()) { + setNew(c, message(c, "Your change could not be merged, " + + "because the destination branch does not exist anymore.")); + } } } catch (IOException e) { throw new MergeException( "Failed to check existence of destination branch", e); } + return branchUpdate; + } catch (IOException e) { + throw new MergeException("Cannot open branch", e); + } + } + + private Set<RevCommit> getAlreadyAccepted(final CodeReviewCommit branchTip) + throws MergeException { + final Set<RevCommit> alreadyAccepted = new HashSet<RevCommit>(); + + if (branchTip != null) { + alreadyAccepted.add(branchTip); + } + + try { for (final Ref r : repo.getAllRefs().values()) { if (r.getName().startsWith(Constants.R_HEADS) || r.getName().startsWith(Constants.R_TAGS)) { @@ -388,11 +468,17 @@ public class MergeOp { } } } catch (IOException e) { - throw new MergeException("Cannot open branch", e); + throw new MergeException("Failed to determine already accepted commits.", e); } + + return alreadyAccepted; } - private void validateChangeList() throws MergeException { + private ListMultimap<SubmitType, Change> validateChangeList( + final List<Change> submitted) throws MergeException { + final ListMultimap<SubmitType, Change> toSubmit = + ArrayListMultimap.create(); + final Set<ObjectId> tips = new HashSet<ObjectId>(); for (final Ref r : repo.getAllRefs().values()) { tips.add(r.getObjectId()); @@ -455,10 +541,11 @@ public class MergeOp { continue; } - if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { + if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) { final Project.NameKey newParent; try { - ProjectConfig cfg = new ProjectConfig(destProject.getNameKey()); + ProjectConfig cfg = + new ProjectConfig(destProject.getProject().getNameKey()); cfg.load(repo, commit); newParent = cfg.getProject().getParent(allProjectsName); } catch (Exception e) { @@ -466,7 +553,8 @@ public class MergeOp { .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION)); continue; } - final Project.NameKey oldParent = destProject.getParent(allProjectsName); + final Project.NameKey oldParent = + destProject.getProject().getParent(allProjectsName); if (oldParent == null) { // update of the 'All-Projects' project if (newParent != null) { @@ -506,7 +594,7 @@ public class MergeOp { if (branchTip != null) { // If this commit is already merged its a bug in the queuing code - // that we got back here. Just mark it complete and move on. Its + // that we got back here. Just mark it complete and move on. It's // merged and that is all that mattered to the requestor. // try { @@ -519,593 +607,58 @@ public class MergeOp { } } - commit.add(CAN_MERGE); - toMerge.add(commit); - } - } - - private void reduceToMinimalMerge() throws MergeException { - final Collection<CodeReviewCommit> heads; - try { - heads = new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort(toMerge); - } catch (IOException e) { - throw new MergeException("Branch head sorting failed", e); - } - - toMerge.clear(); - toMerge.addAll(heads); - Collections.sort(toMerge, new Comparator<CodeReviewCommit>() { - public int compare(final CodeReviewCommit a, final CodeReviewCommit b) { - return a.originalOrder - b.originalOrder; - } - }); - } - - private void mergeTopics() throws MergeException { - // Take the first fast-forward available, if any is available in the set. - // - if (destProject.getSubmitType() != Project.SubmitType.MERGE_ALWAYS) { - for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) { - try { - final CodeReviewCommit n = i.next(); - if (mergeTip == null || rw.isMergedInto(mergeTip, n)) { - mergeTip = n; - i.remove(); - break; - } - } catch (IOException e) { - throw new MergeException("Cannot fast-forward test during merge", e); - } - } - } - - if (destProject.getSubmitType() == Project.SubmitType.FAST_FORWARD_ONLY) { - // If this project only permits fast-forwards, abort everything else. - // - while (!toMerge.isEmpty()) { - final CodeReviewCommit n = toMerge.remove(0); - n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD; + final SubmitType submitType = getSubmitType(chg, ps); + if (submitType == null) { + commits.put(changeId, + CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE)); + continue; } - } else { - // For every other commit do a pair-wise merge. - // - while (!toMerge.isEmpty()) { - mergeOneCommit(toMerge.remove(0)); - } + commit.add(canMergeFlag); + toMerge.put(submitType, commit); + toSubmit.put(submitType, chg); } + return toSubmit; } - private void mergeOneCommit(final CodeReviewCommit n) throws MergeException { - final ThreeWayMerger m = newThreeWayMerger(); + private SubmitType getSubmitType(final Change change, final PatchSet ps) { try { - if (m.merge(new AnyObjectId[] {mergeTip, n})) { - writeMergeCommit(m.getResultTreeId(), n); - - } else { - failed(n, CommitMergeStatus.PATH_CONFLICT); - } - } catch (IOException e) { - if (e.getMessage().startsWith("Multiple merge bases for")) { - try { - failed(n, CommitMergeStatus.CRISS_CROSS_MERGE); - } catch (IOException e2) { - throw new MergeException("Cannot merge " + n.name(), e); - } - } else { - throw new MergeException("Cannot merge " + n.name(), e); - } - } - } - - private ThreeWayMerger newThreeWayMerger() { - ThreeWayMerger m; - if (destProject.isUseContentMerge()) { - // Settings for this project allow us to try and - // automatically resolve conflicts within files if needed. - // Use ResolveMerge and instruct to operate in core. - m = MergeStrategy.RESOLVE.newMerger(repo, true); - } else { - // No auto conflict resolving allowed. If any of the - // affected files was modified, merge will fail. - m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo); - } - m.setObjectInserter(new ObjectInserter.Filter() { - @Override - protected ObjectInserter delegate() { - return inserter; - } - - @Override - public void flush() { - } - - @Override - public void release() { - } - }); - return m; - } - - private CodeReviewCommit failed(final CodeReviewCommit n, - final CommitMergeStatus failure) throws MissingObjectException, - IncorrectObjectTypeException, IOException { - rw.reset(); - rw.markStart(n); - rw.markUninteresting(mergeTip); - CodeReviewCommit failed; - while ((failed = (CodeReviewCommit) rw.next()) != null) { - failed.statusCode = failure; - } - return failed; - } - - private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n) - throws IOException, MissingObjectException, IncorrectObjectTypeException { - final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>(); - rw.reset(); - rw.markStart(n); - rw.markUninteresting(mergeTip); - for (final RevCommit c : rw) { - final CodeReviewCommit crc = (CodeReviewCommit) c; - if (crc.patchsetId != null) { - merged.add(crc); - } - } - - final StringBuilder msgbuf = new StringBuilder(); - if (merged.size() == 1) { - final CodeReviewCommit c = merged.get(0); - rw.parseBody(c); - msgbuf.append("Merge \""); - msgbuf.append(c.getShortMessage()); - msgbuf.append("\""); - - } else { - msgbuf.append("Merge changes "); - for (final Iterator<CodeReviewCommit> i = merged.iterator(); i.hasNext();) { - msgbuf.append(i.next().change.getKey().abbreviate()); - if (i.hasNext()) { - msgbuf.append(','); - } - } - } - - if (!R_HEADS_MASTER.equals(destBranch.get())) { - msgbuf.append(" into "); - msgbuf.append(destBranch.getShortName()); - } - - if (merged.size() > 1) { - msgbuf.append("\n\n* changes:\n"); - for (final CodeReviewCommit c : merged) { - rw.parseBody(c); - msgbuf.append(" "); - msgbuf.append(c.getShortMessage()); - msgbuf.append("\n"); - } - } - - PersonIdent authorIdent = computeAuthor(merged); - - final CommitBuilder mergeCommit = new CommitBuilder(); - mergeCommit.setTreeId(treeId); - mergeCommit.setParentIds(mergeTip, n); - mergeCommit.setAuthor(authorIdent); - mergeCommit.setCommitter(myIdent); - mergeCommit.setMessage(msgbuf.toString()); - - mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit)); - } - - private PersonIdent computeAuthor( - final List<CodeReviewCommit> codeReviewCommits) { - PatchSetApproval submitter = null; - for (final CodeReviewCommit c : codeReviewCommits) { - PatchSetApproval s = getSubmitter(db, c.patchsetId); - if (submitter == null - || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) { - submitter = s; - } - } - - // Try to use the submitter's identity for the merge commit author. - // If all of the commits being merged are created by the submitter, - // prefer the identity line they used in the commits rather than the - // preferred identity stored in the user account. This way the Git - // commit records are more consistent internally. - // - PersonIdent authorIdent; - if (submitter != null) { - IdentifiedUser who = - identifiedUserFactory.create(submitter.getAccountId()); - Set<String> emails = new HashSet<String>(); - for (RevCommit c : codeReviewCommits) { - try { - rw.parseBody(c); - } catch (IOException e) { - log.warn("Cannot parse commit " + c.name() + " in " + destBranch, e); - continue; - } - emails.add(c.getAuthorIdent().getEmailAddress()); - } - - final Timestamp dt = submitter.getGranted(); - final TimeZone tz = myIdent.getTimeZone(); - if (emails.size() == 1 - && who.getEmailAddresses().contains(emails.iterator().next())) { - authorIdent = - new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz); - } else { - authorIdent = who.newCommitterIdent(dt, tz); - } - } else { - authorIdent = myIdent; + final SubmitTypeRecord r = + changeControlFactory.controlFor(change, + identifiedUserFactory.create(change.getOwner())) + .getSubmitTypeRecord(db, ps); + if (r.status != SubmitTypeRecord.Status.OK) { + log.error("Failed to get submit type for " + change.getKey()); + return null; + } + return r.type; + } catch (NoSuchChangeException e) { + log.error("Failed to get submit type for " + change.getKey(), e); + return null; } - return authorIdent; } - private void markCleanMerges() throws MergeException { - if (mergeTip == null) { - // If mergeTip is null here, branchTip was null, indicating a new branch - // at the start of the merge process. We also elected to merge nothing, - // probably due to missing dependencies. Nothing was cleanly merged. - // + private void updateBranch(final SubmitStrategy strategy, + final RefUpdate branchUpdate) throws MergeException { + if ((branchTip == null && mergeTip == null) || branchTip == mergeTip) { + // nothing to do return; } - try { - rw.reset(); - rw.sort(RevSort.TOPO); - rw.sort(RevSort.REVERSE, true); - rw.markStart(mergeTip); - for (RevCommit c : alreadyAccepted) { - rw.markUninteresting(c); - } - - CodeReviewCommit c; - while ((c = (CodeReviewCommit) rw.next()) != null) { - if (c.patchsetId != null) { - c.statusCode = CommitMergeStatus.CLEAN_MERGE; - if (branchUpdate.getRefLogIdent() == null) { - setRefLogIdent(getSubmitter(db, c.patchsetId)); - } - } - } - } catch (IOException e) { - throw new MergeException("Cannot mark clean merges", e); - } - } - - private void setRefLogIdent(final PatchSetApproval submitAudit) { - if (submitAudit != null) { - branchUpdate.setRefLogIdent(identifiedUserFactory.create( - submitAudit.getAccountId()).newRefLogIdent()); - } - } - - private void cherryPickChanges() throws MergeException, OrmException { - while (!toMerge.isEmpty()) { - final CodeReviewCommit n = toMerge.remove(0); - final ThreeWayMerger m = newThreeWayMerger(); - try { - if (mergeTip == null) { - // The branch is unborn. Take a fast-forward resolution to - // create the branch. - // - mergeTip = n; - n.statusCode = CommitMergeStatus.CLEAN_MERGE; - - } else if (n.getParentCount() == 0) { - // Refuse to merge a root commit into an existing branch, - // we cannot obtain a delta for the cherry-pick to apply. - // - n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT; - - } else if (n.getParentCount() == 1) { - // 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. - // - m.setBase(n.getParent(0)); - if (m.merge(mergeTip, n)) { - writeCherryPickCommit(m, n); - - } else { - n.statusCode = CommitMergeStatus.PATH_CONFLICT; - } - - } else { - // There are multiple parents, so this is a merge commit. We - // don't want to cherry-pick 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. - // - if (hasDependenciesMet(n)) { - if (rw.isMergedInto(mergeTip, n)) { - mergeTip = n; - } else { - mergeOneCommit(n); - } - markCleanMerges(); - - } else { - // One or more dependencies were not met. The status was - // already marked on the commit so we have nothing further - // to perform at this time. - // - } - } - - } catch (IOException e) { - throw new MergeException("Cannot merge " + n.name(), e); - } - } - } - - private boolean hasDependenciesMet(final CodeReviewCommit n) - throws IOException { - // Oddly we can determine this by running the merge sorter and - // look for the one commit to come out as a result. This works - // as the merge sorter checks the dependency chain as part of - // its logic trying to find a minimal merge path. - // - return new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort( - Collections.singleton(n)).contains(n); - } - - private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n) - throws IOException, OrmException { - rw.parseBody(n); - - final List<FooterLine> footers = n.getFooterLines(); - final StringBuilder msgbuf = new StringBuilder(); - msgbuf.append(n.getFullMessage()); - - if (msgbuf.length() == 0) { - // WTF, an empty commit message? - msgbuf.append("<no commit message provided>"); - } - if (msgbuf.charAt(msgbuf.length() - 1) != '\n') { - // Missing a trailing LF? Correct it (perhaps the editor was broken). - msgbuf.append('\n'); - } - if (footers.isEmpty()) { - // Doesn't end in a "Signed-off-by: ..." style line? Add another line - // break to start a new paragraph for the reviewed-by tag lines. - // - msgbuf.append('\n'); - } - - if (!contains(footers, CHANGE_ID, n.change.getKey().get())) { - msgbuf.append(CHANGE_ID.getName()); - msgbuf.append(": "); - msgbuf.append(n.change.getKey().get()); - msgbuf.append('\n'); - } - - final String siteUrl = urlProvider.get(); - if (siteUrl != null) { - final String url = siteUrl + n.patchsetId.getParentKey().get(); - if (!contains(footers, REVIEWED_ON, url)) { - msgbuf.append(REVIEWED_ON.getName()); - msgbuf.append(": "); - msgbuf.append(url); - msgbuf.append('\n'); - } - } - - PatchSetApproval submitAudit = null; - List<PatchSetApproval> approvalList = null; - try { - approvalList = - db.patchSetApprovals().byPatchSet(n.patchsetId).toList(); - Collections.sort(approvalList, new Comparator<PatchSetApproval>() { - public int compare(final PatchSetApproval a, final PatchSetApproval b) { - return a.getGranted().compareTo(b.getGranted()); - } - }); - - for (final PatchSetApproval a : approvalList) { - if (a.getValue() <= 0) { - // Negative votes aren't counted. - continue; - } - - if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { - // Submit is treated specially, below (becomes committer) - // - if (submitAudit == null - || a.getGranted().compareTo(submitAudit.getGranted()) > 0) { - submitAudit = a; - } - continue; - } - - final Account acc = - identifiedUserFactory.create(a.getAccountId()).getAccount(); - final StringBuilder identbuf = new StringBuilder(); - if (acc.getFullName() != null && acc.getFullName().length() > 0) { - if (identbuf.length() > 0) { - identbuf.append(' '); - } - identbuf.append(acc.getFullName()); - } - if (acc.getPreferredEmail() != null - && acc.getPreferredEmail().length() > 0) { - if (isSignedOffBy(footers, acc.getPreferredEmail())) { - continue; - } - if (identbuf.length() > 0) { - identbuf.append(' '); - } - identbuf.append('<'); - identbuf.append(acc.getPreferredEmail()); - identbuf.append('>'); - } - if (identbuf.length() == 0) { - // Nothing reasonable to describe them by? Ignore them. - continue; - } - - final String tag; - if (CRVW.equals(a.getCategoryId())) { - tag = "Reviewed-by"; - } else if (VRIF.equals(a.getCategoryId())) { - tag = "Tested-by"; - } else { - final ApprovalType at = - approvalTypes.byId(a.getCategoryId()); - if (at == null) { - // A deprecated/deleted approval type, ignore it. - continue; - } - tag = at.getCategory().getName().replace(' ', '-'); - } - - if (!contains(footers, new FooterKey(tag), identbuf.toString())) { - msgbuf.append(tag); - msgbuf.append(": "); - msgbuf.append(identbuf); - msgbuf.append('\n'); - } - } - } catch (OrmException e) { - log.error("Can't read approval records for " + n.patchsetId, e); - } - - final CommitBuilder mergeCommit = new CommitBuilder(); - mergeCommit.setTreeId(m.getResultTreeId()); - mergeCommit.setParentId(mergeTip); - mergeCommit.setAuthor(n.getAuthorIdent()); - mergeCommit.setCommitter(toCommitterIdent(submitAudit)); - mergeCommit.setMessage(msgbuf.toString()); - - final ObjectId id = commit(mergeCommit); - final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id); - - if (submitAudit != null) { - final Change oldChange = n.change; - - n.change = - db.changes().atomicUpdate(n.change.getId(), - new AtomicUpdate<Change>() { - @Override - public Change update(Change change) { - change.nextPatchSetId(); - return change; - } - }); - - final PatchSet ps = new PatchSet(n.change.currPatchSetId()); - ps.setCreatedOn(new Timestamp(System.currentTimeMillis())); - ps.setUploader(submitAudit.getAccountId()); - ps.setRevision(new RevId(id.getName())); - insertAncestors(ps.getId(), newCommit); - db.patchSets().insert(Collections.singleton(ps)); - - n.change = - db.changes().atomicUpdate(n.change.getId(), - new AtomicUpdate<Change>() { - @Override - public Change update(Change change) { - change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, - ps.getId())); - return change; - } - }); - - this.submitted.remove(oldChange); - this.submitted.add(n.change); - - if (approvalList != null) { - for (PatchSetApproval a : approvalList) { - db.patchSetApprovals().insert( - Collections.singleton(new PatchSetApproval(ps.getId(), a))); - } - } - - final RefUpdate ru = repo.updateRef(ps.getRefName()); - ru.setExpectedOldObjectId(ObjectId.zeroId()); - ru.setNewObjectId(newCommit); - ru.disableRefLog(); - if (ru.update(rw) != RefUpdate.Result.NEW) { - throw new IOException(String.format( - "Failed to create ref %s in %s: %s", ps.getRefName(), n.change - .getDest().getParentKey().get(), ru.getResult())); - } - replication.fire(n.change.getProject(), ru.getName()); - } - - newCommit.copyFrom(n); - newCommit.statusCode = CommitMergeStatus.CLEAN_PICK; - commits.put(newCommit.patchsetId.getParentKey(), newCommit); - mergeTip = newCommit; - setRefLogIdent(submitAudit); - } - - private void insertAncestors(PatchSet.Id id, RevCommit src) - throws OrmException { - final int cnt = src.getParentCount(); - List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt); - for (int p = 0; p < cnt; p++) { - PatchSetAncestor a; - - a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1)); - a.setAncestorRevision(new RevId(src.getParent(p).getId().name())); - toInsert.add(a); - } - db.patchSetAncestors().insert(toInsert); - } - - private ObjectId commit(CommitBuilder mergeCommit) - throws IOException, UnsupportedEncodingException { - ObjectId id = inserter.insert(mergeCommit); - inserter.flush(); - return id; - } - - private boolean contains(List<FooterLine> footers, FooterKey key, String val) { - for (final FooterLine line : footers) { - if (line.matches(key) && val.equals(line.getValue())) { - return true; - } - } - return false; - } - - private boolean isSignedOffBy(List<FooterLine> footers, String email) { - for (final FooterLine line : footers) { - if (line.matches(FooterKey.SIGNED_OFF_BY) - && email.equals(line.getEmailAddress())) { - return true; - } - } - return false; - } - - private PersonIdent toCommitterIdent(final PatchSetApproval audit) { - if (audit != null) { - return identifiedUserFactory.create(audit.getAccountId()) - .newCommitterIdent(audit.getGranted(), myIdent.getTimeZone()); - } - return myIdent; - } - - private void updateBranch() throws MergeException { if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { try { - ProjectConfig cfg = new ProjectConfig(destProject.getNameKey()); + ProjectConfig cfg = + new ProjectConfig(destProject.getProject().getNameKey()); cfg.load(repo, mergeTip); } catch (Exception e) { throw new MergeException("Submit would store invalid" + " project configuration " + mergeTip.name() + " for " - + destProject.getName(), e); + + destProject.getProject().getName(), e); } } + branchUpdate.setRefLogIdent(refLogIdent); branchUpdate.setForceUpdate(false); branchUpdate.setNewObjectId(mergeTip); branchUpdate.setRefLogMessage("merged", true); @@ -1121,13 +674,14 @@ public class MergeOp { } if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { - projectCache.evict(destProject); - ProjectState ps = projectCache.get(destProject.getNameKey()); - repoManager.setProjectDescription(destProject.getNameKey(), // - ps.getProject().getDescription()); + projectCache.evict(destProject.getProject()); + destProject = projectCache.get(destProject.getProject().getNameKey()); + repoManager.setProjectDescription( + destProject.getProject().getNameKey(), + destProject.getProject().getDescription()); } - replication.fire(destBranch.getParentKey(), branchUpdate.getName()); + gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate); Account account = null; final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId); @@ -1137,6 +691,16 @@ public class MergeOp { hooks.doRefUpdatedHook(destBranch, branchUpdate, account); break; + case LOCK_FAILURE: + String msg; + if (strategy.retryOnLockFailure()) { + mergeQueue.recheckAfter(destBranch, LOCK_FAILURE_RETRY_DELAY, + MILLISECONDS); + msg = "will retry"; + } else { + msg = "will not retry"; + } + throw new IOException(branchUpdate.getResult().name() + ", " + msg); default: throw new IOException(branchUpdate.getResult().name()); } @@ -1146,23 +710,7 @@ public class MergeOp { } } - private boolean isMergeable(Change c) { - final CodeReviewCommit commit = commits.get(c.getId()); - final CommitMergeStatus s = commit != null ? commit.statusCode : null; - boolean isMergeable = false; - if (s != null - && (s.equals(CommitMergeStatus.CLEAN_MERGE) - || s.equals(CommitMergeStatus.CLEAN_PICK) || s - .equals(CommitMergeStatus.ALREADY_MERGED))) { - isMergeable = true; - } - - return isMergeable; - } - - private void updateChangeStatus() { - List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>(); - + private void updateChangeStatus(final List<Change> submitted) { for (final Change c : submitted) { final CodeReviewCommit commit = commits.get(c.getId()); final CommitMergeStatus s = commit != null ? commit.statusCode : null; @@ -1175,66 +723,51 @@ public class MergeOp { final String txt = s.getMessage(); - switch (s) { - case CLEAN_MERGE: { - setMerged(c, message(c, txt)); - merged.add(commit); - break; - } + try { + switch (s) { + case CLEAN_MERGE: + setMerged(c, message(c, txt)); + break; - case CLEAN_PICK: { - setMerged(c, message(c, txt + " as " + commit.name())); - merged.add(commit); - break; - } + case CLEAN_REBASE: + case CLEAN_PICK: + setMerged(c, message(c, txt + " as " + commit.name())); + break; - case ALREADY_MERGED: - setMerged(c, null); - merged.add(commit); - break; - - case PATH_CONFLICT: - case CRISS_CROSS_MERGE: - case CANNOT_CHERRY_PICK_ROOT: - case NOT_FAST_FORWARD: - case INVALID_PROJECT_CONFIGURATION: - case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND: - case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT: - case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: { - setNew(c, message(c, txt)); - break; - } + case ALREADY_MERGED: + setMerged(c, null); + break; - case MISSING_DEPENDENCY: { - final Capable capable = isSubmitStillPossible(commit); - if (capable != Capable.OK) { - sendMergeFail(c, message(c, capable.getMessage()), false); - } - break; - } + case PATH_CONFLICT: + case MANUAL_RECURSIVE_MERGE: + case CANNOT_CHERRY_PICK_ROOT: + case NOT_FAST_FORWARD: + case INVALID_PROJECT_CONFIGURATION: + case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND: + case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT: + case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: + setNew(c, message(c, txt)); + break; - default: - setNew(c, message(c, "Unspecified merge failure: " + s.name())); - break; - } - } + case MISSING_DEPENDENCY: + potentiallyStillSubmittable.add(commit); + break; - CreateCodeReviewNotes codeReviewNotes = - codeReviewNotesFactory.create(db, repo); - try { - codeReviewNotes.create(merged, computeAuthor(merged)); - } catch (CodeReviewNoteCreationException e) { - log.error(e.getMessage()); + default: + setNew(c, message(c, "Unspecified merge failure: " + s.name())); + break; + } + } catch (OrmException err) { + log.warn("Error updating change status for " + c.getId(), err); + } } - replication.fire(destBranch.getParentKey(), - GitRepositoryManager.REFS_NOTES_REVIEW); } - private void updateSubscriptions() { + private void updateSubscriptions(final List<Change> submitted) { if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { SubmoduleOp subOp = - subOpFactory.create(destBranch, mergeTip, rw, repo, destProject, - submitted, commits); + subOpFactory.create(destBranch, mergeTip, rw, repo, + destProject.getProject(), submitted, commits); try { subOp.update(); } catch (SubmoduleException e) { @@ -1248,32 +781,7 @@ public class MergeOp { private Capable isSubmitStillPossible(final CodeReviewCommit commit) { final Capable capable; final Change c = commit.change; - if (commit.missing == null) { - commit.missing = new ArrayList<CodeReviewCommit>(); - } - - boolean submitStillPossible = commit.missing.size() > 0; - for (CodeReviewCommit missingCommit : commit.missing) { - loadChangeInfo(missingCommit); - - if (missingCommit.patchsetId == null) { - // The commit doesn't have a patch set, so it cannot be - // submitted to the branch. - // - submitStillPossible = false; - break; - } - - if (!missingCommit.change.currentPatchSetId().equals( - missingCommit.patchsetId)) { - // If the missing commit is not the current patch set, - // the change must be rebased to use the proper parent. - // - submitStillPossible = false; - break; - } - } - + final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit); final long now = System.currentTimeMillis(); final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY; if (submitStillPossible && now < waitUntil) { @@ -1287,25 +795,20 @@ public class MergeOp { // dependencies are also submitted. Perhaps the user just // forgot to submit those. // - String txt = - "Change could not be merged because of a missing dependency."; - if (!isAlreadySent(c, txt)) { - StringBuilder m = new StringBuilder(); - m.append(txt); - m.append("\n"); + StringBuilder m = new StringBuilder(); + m.append("Change could not be merged because of a missing dependency."); + m.append("\n"); - m.append("\n"); + m.append("\n"); - m.append("The following changes must also be submitted:\n"); + m.append("The following changes must also be submitted:\n"); + m.append("\n"); + for (CodeReviewCommit missingCommit : commit.missing) { + m.append("* "); + m.append(missingCommit.change.getKey().get()); m.append("\n"); - for (CodeReviewCommit missingCommit : commit.missing) { - m.append("* "); - m.append(missingCommit.change.getKey().get()); - m.append("\n"); - } - txt = m.toString(); } - capable = new Capable(txt); + capable = new Capable(m.toString()); } else { // It is impossible to submit this change as-is. The author // needs to rebase it in order to work around the missing @@ -1323,8 +826,10 @@ public class MergeOp { m.append(missingCommit.patchsetId.get()); m.append(" of "); m.append(missingCommit.change.getKey().abbreviate()); - m.append(", however the current patch set is "); - m.append(missingCommit.change.currentPatchSetId().get()); + if (missingCommit.patchsetId.get() != missingCommit.change.currentPatchSetId().get()) { + m.append(", however the current patch set is "); + m.append(missingCommit.change.currentPatchSetId().get()); + } m.append(".\n"); } else { @@ -1356,30 +861,6 @@ public class MergeOp { } } - private boolean isAlreadySent(final Change c, final String prefix) { - try { - final List<ChangeMessage> msgList = - db.changeMessages().byChange(c.getId()).toList(); - if (msgList.size() > 0) { - final ChangeMessage last = msgList.get(msgList.size() - 1); - if (last.getAuthor() == null && last.getMessage().startsWith(prefix)) { - // The last message was written by us, and it said this - // same message already. Its unlikely anything has changed - // that would cause us to need to repeat ourselves. - // - return true; - } - } - - // The last message was not sent by us, or doesn't match the text - // we are about to send. - // - return false; - } catch (OrmException e) { - return true; - } - } - private ChangeMessage message(final Change c, final String body) { final String uuid; try { @@ -1394,87 +875,87 @@ public class MergeOp { return m; } - private static PatchSetApproval getSubmitter(ReviewDb reviewDb, - PatchSet.Id c) { - if (c == null) { - return null; - } - PatchSetApproval submitter = null; + private void setMerged(final Change c, final ChangeMessage msg) + throws OrmException { try { - final List<PatchSetApproval> approvals = - reviewDb.patchSetApprovals().byPatchSet(c).toList(); - for (PatchSetApproval a : approvals) { - if (a.getValue() > 0 - && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { - if (submitter == null - || a.getGranted().compareTo(submitter.getGranted()) > 0) { - submitter = a; - } + 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 merged = commit.change.currentPatchSetId(); + setMergedPatchSet(c.getId(), merged); + PatchSetApproval submitter = saveApprovals(c, merged); + addMergedMessage(submitter, msg); + + db.commit(); + + sendMergedEmail(c, submitter); + if (submitter != null) { + try { + hooks.doChangeMergedHook(c, + accountCache.get(submitter.getAccountId()).getAccount(), + db.patchSets().get(c.currentPatchSetId()), db); + } catch (OrmException ex) { + log.error("Cannot run hook for submitted patch set " + c.getId(), ex); } } - } catch (OrmException e) { + } finally { + db.rollback(); } - return submitter; } - private void setMerged(final Change c, final ChangeMessage msg) { - final Change.Id changeId = c.getId(); - // We must pull the patchset out of commits, because the patchset ID is - // modified when using the cherry-pick merge strategy. - final CodeReviewCommit commit = commits.get(c.getId()); - final PatchSet.Id merged = commit.change.currentPatchSetId(); - - try { - db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() { - @Override - public Change update(Change c) { - c.setStatus(Change.Status.MERGED); - // It could be possible that the change being merged - // has never had its mergeability tested. So we insure - // merged changes has mergeable field true. - c.setMergeable(true); - 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, merged)); - } catch (PatchSetInfoNotAvailableException e1) { - log.error("Cannot read merged patch set " + merged, e1); - } + private void setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged) + throws OrmException { + db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() { + @Override + public Change update(Change c) { + c.setStatus(Change.Status.MERGED); + // It could be possible that the change being merged + // has never had its mergeability tested. So we insure + // merged changes has mergeable field true. + c.setMergeable(true); + 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, merged)); + } catch (PatchSetInfoNotAvailableException e1) { + log.error("Cannot read merged patch set " + merged, e1); } - ChangeUtil.updated(c); - return c; } - }); - } catch (OrmConcurrencyException err) { - } catch (OrmException err) { - log.warn("Cannot update change status", err); - } + ChangeUtil.updated(c); + return c; + } + }); + } - // Flatten out all existing approvals based upon the current - // permissions. Once the change is closed the approvals are - // not updated at presentation view time, so we need to make. - // sure they are accurate now. This way if permissions get - // modified in the future, historical records stay accurate. - // + private PatchSetApproval saveApprovals(Change c, PatchSet.Id merged) + throws OrmException { + // 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. PatchSetApproval submitter = null; try { c.setStatus(Change.Status.MERGED); - final List<PatchSetApproval> approvals = - db.patchSetApprovals().byChange(changeId).toList(); - final FunctionState fs = functionState.create( - changeControlFactory.controlFor( - c, - identifiedUserFactory.create(c.getOwner())), - merged, approvals); - for (ApprovalType at : approvalTypes.getApprovalTypes()) { - CategoryFunction.forCategory(at.getCategory()).run(at, fs); + + List<PatchSetApproval> approvals = + db.patchSetApprovals().byPatchSet(merged).toList(); + Set<PatchSetApproval.Key> toDelete = + Sets.newHashSetWithExpectedSize(approvals.size()); + for (PatchSetApproval a : approvals) { + if (a.getValue() != 0) { + toDelete.add(a.getKey()); + } } + + approvals = labelNormalizer.normalize(c, approvals); for (PatchSetApproval a : approvals) { - if (a.getValue() > 0 - && ApprovalCategory.SUBMIT.equals(a.getCategoryId()) - && a.getPatchSetId().equals(merged)) { + toDelete.remove(a.getKey()); + if (a.getValue() > 0 && a.isSubmit()) { if (submitter == null || a.getGranted().compareTo(submitter.getGranted()) > 0) { submitter = a; @@ -1483,24 +964,24 @@ public class MergeOp { a.cache(c); } db.patchSetApprovals().update(approvals); + db.patchSetApprovals().deleteKeys(toDelete); } catch (NoSuchChangeException err) { - log.warn("Cannot normalize approvals for change " + changeId, err); - } catch (OrmException err) { - log.warn("Cannot normalize approvals for change " + changeId, err); + throw new OrmException(err); } + return submitter; + } + private void addMergedMessage(PatchSetApproval submitter, ChangeMessage msg) + throws OrmException { if (msg != null) { if (submitter != null && msg.getAuthor() == null) { msg.setAuthor(submitter.getAccountId()); } - try { - db.changeMessages().insert(Collections.singleton(msg)); - } catch (OrmException err) { - log.warn("Cannot store message on change", err); - } + db.changeMessages().insert(Collections.singleton(msg)); } + } - final PatchSetApproval from = submitter; + private void sendMergedEmail(final Change c, final PatchSetApproval from) { workQueue.getDefaultQueue() .submit(requestScopePropagator.wrap(new Runnable() { @Override @@ -1519,7 +1000,9 @@ public class MergeOp { } try { - final MergedSender cm = mergedSenderFactory.create(c); + final ChangeControl control = changeControlFactory.controlFor(c, + identifiedUserFactory.create(c.getOwner())); + final MergedSender cm = mergedSenderFactory.create(control); if (from != null) { cm.setFrom(from.getAccountId()); } @@ -1535,23 +1018,37 @@ public class MergeOp { return "send-email merged"; } })); - - - try { - hooks.doChangeMergedHook(c, // - accountCache.get(submitter.getAccountId()).getAccount(), // - db.patchSets().get(c.currentPatchSetId()), db); - } catch (OrmException ex) { - log.error("Cannot run hook for submitted patch set " + c.getId(), ex); - } } private void setNew(Change c, ChangeMessage msg) { sendMergeFail(c, msg, true); } + private boolean isDuplicate(ChangeMessage msg) { + try { + ChangeMessage last = Iterables.getLast(db.changeMessages().byChange( + msg.getPatchSetId().getParentKey()), null); + if (last != null) { + long lastMs = last.getWrittenOn().getTime(); + long msgMs = msg.getWrittenOn().getTime(); + if (Objects.equal(last.getAuthor(), msg.getAuthor()) + && Objects.equal(last.getMessage(), msg.getMessage()) + && msgMs - lastMs < DUPLICATE_MESSAGE_INTERVAL) { + return true; + } + } + } catch (OrmException err) { + log.warn("Cannot check previous merge failure message", err); + } + return false; + } + private void sendMergeFail(final Change c, final ChangeMessage msg, final boolean makeNew) { + if (isDuplicate(msg)) { + return; + } + try { db.changeMessages().insert(Collections.singleton(msg)); } catch (OrmException err) { @@ -1582,17 +1079,23 @@ public class MergeOp { } } + PatchSetApproval submitter = null; + try { + submitter = getSubmitter(db, c.currentPatchSetId()); + } catch (Exception e) { + log.error("Cannot get submitter", e); + } + + final PatchSetApproval from = submitter; workQueue.getDefaultQueue() .submit(requestScopePropagator.wrap(new Runnable() { @Override public void run() { PatchSet patchSet; - PatchSetApproval submitter; try { ReviewDb reviewDb = schemaFactory.open(); try { patchSet = reviewDb.patchSets().get(c.currentPatchSetId()); - submitter = getSubmitter(reviewDb, c.currentPatchSetId()); } finally { reviewDb.close(); } @@ -1603,8 +1106,8 @@ public class MergeOp { try { final MergeFailSender cm = mergeFailSenderFactory.create(c); - if (submitter != null) { - cm.setFrom(submitter.getAccountId()); + if (from != null) { + cm.setFrom(from.getAccountId()); } cm.setPatchSet(patchSet); cm.setChangeMessage(msg); @@ -1619,5 +1122,15 @@ public class MergeOp { return "send-email merge-failed"; } })); + + if (submitter != null) { + try { + hooks.doMergeFailedHook(c, + accountCache.get(submitter.getAccountId()).getAccount(), + db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db); + } catch (OrmException ex) { + log.error("Cannot run hook for merge failed " + c.getId(), ex); + } + } } } |