Java tutorial
// Copyright (C) 2008 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; 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.Capable; import com.google.gerrit.common.data.SubmitTypeRecord; 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.PatchSet; 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.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.index.ChangeIndexer; import com.google.gerrit.server.mail.MergeFailSender; import com.google.gerrit.server.mail.MergedSender; 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.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.assistedinject.Assisted; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; 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.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; 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; /** * Merges changes in submission order into a single branch. * <p> * Branches are reduced to the minimum number of heads needed to merge * everything. This allows commits to be entered into the queue in any order * (such as ancestors before descendants) and only the most recent commit on any * line of development will be merged. All unmerged commits along a line of * development must be in the submission queue in order to merge the tip of that * line. * <p> * Conflicts are handled by discarding the entire line of development and * marking it as conflicting, even if an earlier commit along that same line can * be merged cleanly. */ public class MergeOp { public interface Factory { MergeOp create(Branch.NameKey branch); } private static final Logger log = LoggerFactory.getLogger(MergeOp.class); /** 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 LabelNormalizer labelNormalizer; private final GitReferenceUpdated gitRefUpdated; private final MergedSender.Factory mergedSenderFactory; private final MergeFailSender.Factory mergeFailSenderFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final ChangeControl.GenericFactory changeControlFactory; private final MergeQueue mergeQueue; private final Branch.NameKey destBranch; 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 canMergeFlag; private CodeReviewCommit branchTip; private CodeReviewCommit mergeTip; private ObjectInserter inserter; private PersonIdent refLogIdent; private final ChangeHooks hooks; private final AccountCache accountCache; private final TagCache tagCache; private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; private final WorkQueue workQueue; private final RequestScopePropagator requestScopePropagator; private final AllProjectsName allProjectsName; private final ChangeIndexer indexer; @Inject MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf, final ProjectCache pc, final LabelNormalizer fs, final GitReferenceUpdated gru, final MergedSender.Factory msf, final MergeFailSender.Factory mfsf, final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf, final ChangeControl.GenericFactory changeControlFactory, final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch, final ChangeHooks hooks, final AccountCache accountCache, final TagCache tagCache, final SubmitStrategyFactory submitStrategyFactory, final SubmoduleOp.Factory subOpFactory, final WorkQueue workQueue, final RequestScopePropagator requestScopePropagator, final AllProjectsName allProjectsName, final ChangeIndexer indexer) { repoManager = grm; schemaFactory = sf; labelNormalizer = fs; projectCache = pc; gitRefUpdated = gru; mergedSenderFactory = msf; mergeFailSenderFactory = mfsf; patchSetInfoFactory = psif; identifiedUserFactory = iuf; this.changeControlFactory = changeControlFactory; this.mergeQueue = mergeQueue; this.hooks = hooks; this.accountCache = accountCache; this.tagCache = tagCache; this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; this.workQueue = workQueue; this.requestScopePropagator = requestScopePropagator; this.allProjectsName = allProjectsName; this.indexer = indexer; destBranch = branch; toMerge = ArrayListMultimap.create(); potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>(); commits = new HashMap<Change.Id, CodeReviewCommit>(); } public void verifyMergeability(Change change) throws NoSuchProjectException { try { setDestProject(); openRepository(); final Ref destBranchRef = repo.getRef(destBranch.get()); // Test mergeability of the change if the last merged sha1 // in the branch is different from the last sha1 // the change was tested against. if ((destBranchRef == null && change.getLastSha1MergeTested() == null) || change.getLastSha1MergeTested() == null || (destBranchRef != null && !destBranchRef .getObjectId().getName().equals(change.getLastSha1MergeTested().get()))) { openSchema(); 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 { log.error("Test merge attempt for change: " + change.getId() + " failed"); } } } catch (MergeException e) { log.error("Test merge attempt for change: " + change.getId() + " failed", e); } catch (OrmException e) { log.error( "Test merge attempt for change: " + change.getId() + " failed: Not able to query the database", e); } catch (IOException e) { log.error("Test merge attempt for change: " + change.getId() + " failed", e); } finally { if (repo != null) { repo.close(); } if (db != null) { db.close(); } } } private void setDestProject() throws MergeException { destProject = projectCache.get(destBranch.getParentKey()); if (destProject == null) { throw new MergeException("No such project: " + destBranch.getParentKey()); } } private void openSchema() throws OrmException { if (db == null) { db = schemaFactory.open(); } } public void merge() throws MergeException, NoSuchProjectException { setDestProject(); try { openSchema(); openRepository(); 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 { if (inserter != null) { inserter.release(); } if (rw != null) { rw.release(); } if (repo != null) { repo.close(); } if (db != null) { db.close(); } } } 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 { final Project.NameKey name = destBranch.getParentKey(); try { repo = repoManager.openRepository(name); } catch (RepositoryNotFoundException notGit) { final String m = "Repository \"" + name.get() + "\" unknown."; throw new MergeException(m, notGit); } catch (IOException err) { final String m = "Error opening repository \"" + name.get() + '"'; throw new MergeException(m, err); } rw = new RevWalk(repo) { @Override protected RevCommit createCommit(final AnyObjectId id) { return new CodeReviewCommit(id); } }; rw.sort(RevSort.TOPO); rw.sort(RevSort.COMMIT_TIME_DESC, true); canMergeFlag = rw.newFlag("CAN_MERGE"); inserter = repo.newObjectInserter(); } private RefUpdate openBranch() throws MergeException, OrmException { try { final RefUpdate branchUpdate = repo.updateRef(destBranch.get()); if (branchUpdate.getOldObjectId() != null) { branchTip = (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId()); } else { branchTip = null; } try { final Ref destRef = repo.getRef(destBranch.get()); if (destRef != null) { branchUpdate.setExpectedOldObjectId(destRef.getObjectId()); } else if (repo.getFullBranch().equals(destBranch.get())) { branchUpdate.setExpectedOldObjectId(ObjectId.zeroId()); } else { 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)) { try { alreadyAccepted.add(rw.parseCommit(r.getObjectId())); } catch (IncorrectObjectTypeException iote) { // Not a commit? Skip over it. } } } } catch (IOException e) { throw new MergeException("Failed to determine already accepted commits.", e); } return alreadyAccepted; } 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()); } int commitOrder = 0; for (final Change chg : submitted) { final Change.Id changeId = chg.getId(); if (chg.currentPatchSetId() == null) { commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.NO_PATCH_SET)); continue; } final PatchSet ps; try { ps = db.patchSets().get(chg.currentPatchSetId()); } catch (OrmException e) { throw new MergeException("Cannot query the database", e); } if (ps == null || ps.getRevision() == null || ps.getRevision().get() == null) { commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.NO_PATCH_SET)); continue; } final String idstr = ps.getRevision().get(); final ObjectId id; try { id = ObjectId.fromString(idstr); } catch (IllegalArgumentException iae) { commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.NO_PATCH_SET)); continue; } if (!tips.contains(id)) { // TODO Technically the proper way to do this test is to use a // RevWalk on "$id --not --all" and test for an empty set. But // that is way slower than looking for a ref directly pointing // at the desired tip. We should always have a ref available. // // TODO this is actually an error, the branch is gone but we // want to merge the issue. We can't safely do that if the // tip is not reachable. // commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.REVISION_GONE)); continue; } final CodeReviewCommit commit; try { commit = (CodeReviewCommit) rw.parseCommit(id); } catch (IOException e) { log.error("Invalid commit " + id.name() + " on " + chg.getKey(), e); commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.REVISION_GONE)); continue; } if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) { final Project.NameKey newParent; try { ProjectConfig cfg = new ProjectConfig(destProject.getProject().getNameKey()); cfg.load(repo, commit); newParent = cfg.getProject().getParent(allProjectsName); } catch (Exception e) { commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION)); continue; } final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName); if (oldParent == null) { // update of the 'All-Projects' project if (newParent != null) { commits.put(changeId, CodeReviewCommit.error( CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT)); continue; } } else { if (!oldParent.equals(newParent)) { final PatchSetApproval psa = getSubmitter(db, ps.getId()); if (psa == null) { commits.put(changeId, CodeReviewCommit .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN)); continue; } final IdentifiedUser submitter = identifiedUserFactory.create(psa.getAccountId()); if (!submitter.getCapabilities().canAdministrateServer()) { commits.put(changeId, CodeReviewCommit .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN)); continue; } if (projectCache.get(newParent) == null) { commits.put(changeId, CodeReviewCommit.error( CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND)); continue; } } } } commit.change = chg; commit.patchsetId = ps.getId(); commit.originalOrder = commitOrder++; commits.put(changeId, commit); 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. It's // merged and that is all that mattered to the requestor. // try { if (rw.isMergedInto(commit, branchTip)) { commit.statusCode = CommitMergeStatus.ALREADY_MERGED; continue; } } catch (IOException err) { throw new MergeException("Cannot perform merge base test", err); } } final SubmitType submitType = getSubmitType(chg, ps); if (submitType == null) { commits.put(changeId, CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE)); continue; } commit.add(canMergeFlag); toMerge.put(submitType, commit); toSubmit.put(submitType, chg); } return toSubmit; } private SubmitType getSubmitType(final Change change, final PatchSet ps) { try { 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; } } private void updateBranch(final SubmitStrategy strategy, final RefUpdate branchUpdate) throws MergeException { if ((branchTip == null && mergeTip == null) || branchTip == mergeTip) { // nothing to do return; } if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { try { 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.getProject().getName(), e); } } branchUpdate.setRefLogIdent(refLogIdent); branchUpdate.setForceUpdate(false); branchUpdate.setNewObjectId(mergeTip); branchUpdate.setRefLogMessage("merged", true); try { switch (branchUpdate.update(rw)) { case NEW: case FAST_FORWARD: if (branchUpdate.getResult() == RefUpdate.Result.FAST_FORWARD) { tagCache.updateFastForward(destBranch.getParentKey(), branchUpdate.getName(), branchUpdate.getOldObjectId(), mergeTip); } if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { projectCache.evict(destProject.getProject()); destProject = projectCache.get(destProject.getProject().getNameKey()); repoManager.setProjectDescription(destProject.getProject().getNameKey(), destProject.getProject().getDescription()); } gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate); Account account = null; final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId); if (submitter != null) { account = accountCache.get(submitter.getAccountId()).getAccount(); } 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()); } } catch (IOException e) { throw new MergeException("Cannot update " + branchUpdate.getName(), e); } } } 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; if (s == null) { // Shouldn't ever happen, but leave the change alone. We'll pick // it up on the next pass. // continue; } final String txt = s.getMessage(); try { switch (s) { case CLEAN_MERGE: setMerged(c, message(c, txt)); break; case CLEAN_REBASE: case CLEAN_PICK: setMerged(c, message(c, txt + " as " + commit.name())); break; case ALREADY_MERGED: setMerged(c, null); 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; case MISSING_DEPENDENCY: potentiallyStillSubmittable.add(commit); break; default: setNew(c, message(c, "Unspecified merge failure: " + s.name())); break; } } catch (OrmException err) { log.warn("Error updating change status for " + c.getId(), err); } indexer.index(c); } } private void updateSubscriptions(final List<Change> submitted) { if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { SubmoduleOp subOp = subOpFactory.create(destBranch, mergeTip, rw, repo, destProject.getProject(), submitted, commits); try { subOp.update(); } catch (SubmoduleException e) { log.error("The gitLinks were not updated according to the subscriptions " + e.getMessage()); } } } private Capable isSubmitStillPossible(final CodeReviewCommit commit) { final Capable capable; final Change c = commit.change; final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit); final long now = System.currentTimeMillis(); final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY; if (submitStillPossible && now < waitUntil) { // If we waited a short while we might still be able to get // this change submitted. Reschedule an attempt in a bit. // mergeQueue.recheckAfter(destBranch, waitUntil - now, MILLISECONDS); capable = Capable.OK; } else if (submitStillPossible) { // It would be possible to submit the change if the missing // dependencies are also submitted. Perhaps the user just // forgot to submit those. // StringBuilder m = new StringBuilder(); m.append("Change could not be merged because of a missing dependency."); m.append("\n"); m.append("\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"); } 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 // dependencies. // StringBuilder m = new StringBuilder(); m.append("Change cannot be merged due" + " to unsatisfiable dependencies.\n"); m.append("\n"); m.append("The following dependency errors were found:\n"); m.append("\n"); for (CodeReviewCommit missingCommit : commit.missing) { if (missingCommit.patchsetId != null) { m.append("* Depends on patch set "); m.append(missingCommit.patchsetId.get()); m.append(" of "); m.append(missingCommit.change.getKey().abbreviate()); 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 { m.append("* Depends on commit "); m.append(missingCommit.name()); m.append(" which has no change associated with it.\n"); } } m.append("\n"); m.append("Please rebase the change and upload a replacement commit."); capable = new Capable(m.toString()); } return capable; } private void loadChangeInfo(final CodeReviewCommit commit) { if (commit.patchsetId == null) { try { List<PatchSet> matches = db.patchSets().byRevision(new RevId(commit.name())).toList(); if (matches.size() == 1) { final PatchSet ps = matches.get(0); commit.patchsetId = ps.getId(); commit.change = db.changes().get(ps.getId().getParentKey()); } } catch (OrmException e) { } } } private ChangeMessage message(final Change c, final String body) { final String uuid; try { uuid = ChangeUtil.messageUUID(db); } catch (OrmException e) { return null; } final ChangeMessage m = new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid), null, c.currentPatchSetId()); m.setMessage(body); return m; } private void setMerged(final Change c, final ChangeMessage msg) throws OrmException { 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 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); } } } finally { db.rollback(); } } 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; } }); } 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); 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) { toDelete.remove(a.getKey()); if (a.getValue() > 0 && a.isSubmit()) { if (submitter == null || a.getGranted().compareTo(submitter.getGranted()) > 0) { submitter = a; } } a.cache(c); } db.patchSetApprovals().update(approvals); db.patchSetApprovals().deleteKeys(toDelete); } catch (NoSuchChangeException 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()); } db.changeMessages().insert(Collections.singleton(msg)); } } private void sendMergedEmail(final Change c, final PatchSetApproval from) { workQueue.getDefaultQueue().submit(requestScopePropagator.wrap(new Runnable() { @Override public void run() { PatchSet patchSet; try { ReviewDb reviewDb = schemaFactory.open(); try { patchSet = reviewDb.patchSets().get(c.currentPatchSetId()); } finally { reviewDb.close(); } } catch (Exception e) { log.error("Cannot send email for submitted patch set " + c.getId(), e); return; } try { final ChangeControl control = changeControlFactory.controlFor(c, identifiedUserFactory.create(c.getOwner())); final MergedSender cm = mergedSenderFactory.create(control); if (from != null) { cm.setFrom(from.getAccountId()); } cm.setPatchSet(patchSet); cm.send(); } catch (Exception e) { log.error("Cannot send email for submitted patch set " + c.getId(), e); } } @Override public String toString() { return "send-email merged"; } })); } 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) { log.warn("Cannot record merge failure message", err); } if (makeNew) { try { db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change c) { if (c.getStatus().isOpen()) { c.setStatus(Change.Status.NEW); ChangeUtil.updated(c); } return c; } }); } catch (OrmConcurrencyException err) { } catch (OrmException err) { log.warn("Cannot update change status", err); } } else { try { ChangeUtil.touch(c, db); } catch (OrmException err) { log.warn("Cannot update change timestamp", err); } } 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; try { ReviewDb reviewDb = schemaFactory.open(); try { patchSet = reviewDb.patchSets().get(c.currentPatchSetId()); } finally { reviewDb.close(); } } catch (Exception e) { log.error("Cannot send email notifications about merge failure", e); return; } try { final MergeFailSender cm = mergeFailSenderFactory.create(c); if (from != null) { cm.setFrom(from.getAccountId()); } cm.setPatchSet(patchSet); cm.setChangeMessage(msg); cm.send(); } catch (Exception e) { log.error("Cannot send email notifications about merge failure", e); } } @Override public String toString() { 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); } } } }