diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java | 626 |
1 files changed, 0 insertions, 626 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java deleted file mode 100644 index 9a362d4325..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java +++ /dev/null @@ -1,626 +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 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.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.reviewdb.server.ReviewDbUtil; -import com.google.gerrit.server.ApprovalsUtil; -import com.google.gerrit.server.ChangeMessagesUtil; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.git.CodeReviewCommit; -import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; -import com.google.gerrit.server.git.GroupCollector; -import com.google.gerrit.server.git.IntegrationException; -import com.google.gerrit.server.git.LabelNormalizer; -import com.google.gerrit.server.git.MergeUtil; -import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.git.SubmoduleException; -import com.google.gerrit.server.notedb.ChangeUpdate; -import com.google.gerrit.server.project.ProjectState; -import com.google.gerrit.server.update.BatchUpdateOp; -import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RepoContext; -import com.google.gwtorm.server.OrmException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ReceiveCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -abstract class SubmitStrategyOp implements BatchUpdateOp { - 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; - private CodeReviewCommit alreadyMergedCommit; - private boolean changeAlreadyMerged; - - 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 { - logDebug("{}#updateRepo for change {}", getClass().getSimpleName(), toMerge.change().getId()); - checkState( - ctx.getRevWalk() == args.rw, - "SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same" - + " CodeReviewRevWalk instance from the SubmitStrategy.Arguments: %s != %s", - ctx.getRevWalk(), - args.rw); - // Run the submit strategy implementation and record the merge tip state so - // we can create the ref update. - CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip(); - alreadyMergedCommit = getAlreadyMergedCommit(ctx); - if (alreadyMergedCommit == null) { - updateRepoImpl(ctx); - } else { - logDebug("Already merged as {}", alreadyMergedCommit.name()); - } - CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip(); - - if (Objects.equals(tipBefore, tipAfter)) { - logDebug("Did not move tip", getClass().getSimpleName()); - return; - } else if (tipAfter == null) { - logDebug("No merge tip, no update to perform"); - return; - } - logDebug("Moved tip from {} to {}", tipBefore, tipAfter); - - 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); - args.submoduleOp.addBranchTip(getDest(), tipAfter); - } - - 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.getRevWalk(), commit); - } catch (Exception e) { - throw new IntegrationException( - "Submit would store invalid" - + " project configuration " - + commit.name() - + " for " - + getProject(), - e); - } - } - } - - private CodeReviewCommit getAlreadyMergedCommit(RepoContext ctx) throws IOException { - CodeReviewCommit tip = args.mergeTip.getInitialTip(); - if (tip == null) { - return null; - } - CodeReviewRevWalk rw = (CodeReviewRevWalk) ctx.getRevWalk(); - Change.Id id = getId(); - String refPrefix = id.toRefPrefix(); - - Map<String, ObjectId> refs = ctx.getRepoView().getRefs(refPrefix); - List<CodeReviewCommit> commits = new ArrayList<>(refs.size()); - for (Map.Entry<String, ObjectId> e : refs.entrySet()) { - PatchSet.Id psId = PatchSet.Id.fromRef(refPrefix + e.getKey()); - if (psId == null) { - continue; - } - try { - CodeReviewCommit c = rw.parseCommit(e.getValue()); - c.setPatchsetId(psId); - commits.add(c); - } catch (MissingObjectException | IncorrectObjectTypeException ex) { - continue; // Bogus ref, can't be merged into tip so we don't care. - } - } - Collections.sort( - commits, ReviewDbUtil.intKeyOrdering().reverse().onResultOf(c -> c.getPatchsetId())); - CodeReviewCommit result = MergeUtil.findAnyMergedInto(rw, commits, tip); - if (result == null) { - return null; - } - - // Some patch set of this change is actually merged into the target - // branch, most likely because a previous run of MergeOp failed after - // updateRepo, during updateChange. - // - // Do the best we can to clean this up: mark the change as merged and set - // the current patch set. Don't touch the dest branch at all. This can - // lead to some odd situations like another change in the set merging in - // a different patch set of this change, but that's unavoidable at this - // point. At least the change will end up in the right state. - // - // TODO(dborowitz): Consider deleting later junk patch set refs. They - // presumably don't have PatchSets pointing to them. - rw.parseBody(result); - result.add(args.canMergeFlag); - PatchSet.Id psId = result.getPatchsetId(); - result.copyFrom(toMerge); - result.setPatchsetId(psId); // Got overwriten by copyFrom. - result.setStatusCode(CommitMergeStatus.ALREADY_MERGED); - args.commitStatus.put(result); - return result; - } - - @Override - public final boolean updateChange(ChangeContext ctx) throws Exception { - logDebug("{}#updateChange for change {}", getClass().getSimpleName(), toMerge.change().getId()); - toMerge.setNotes(ctx.getNotes()); // Update change and notes from ctx. - PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId()); - PatchSet.Id newPsId; - - if (ctx.getChange().getStatus() == Change.Status.MERGED) { - // Either another thread won a race, or we are retrying a whole topic submission after one - // repo failed with lock failure. - if (alreadyMergedCommit == null) { - logDebug( - "Change is already merged according to its status, but we were unable to find it" - + " merged into the current tip ({})", - args.mergeTip.getCurrentTip().name()); - } else { - logDebug("Change is already merged"); - } - changeAlreadyMerged = true; - return false; - } - - if (alreadyMergedCommit != null) { - alreadyMergedCommit.setNotes(ctx.getNotes()); - mergedPatchSet = getOrCreateAlreadyMergedPatchSet(ctx); - newPsId = mergedPatchSet.getId(); - } else { - PatchSet newPatchSet = updateChangeImpl(ctx); - newPsId = checkNotNull(ctx.getChange().currentPatchSetId()); - if (newPatchSet == null) { - checkState( - oldPsId.equals(newPsId), - "patch set advanced from %s to %s but updateChangeImpl did not" - + " return new patch set instance", - oldPsId, - newPsId); - // Ok to use stale notes to get the old patch set, which didn't change - // during the submit strategy. - mergedPatchSet = - checkNotNull( - args.psUtil.get(ctx.getDb(), ctx.getNotes(), oldPsId), - "missing old patch set %s", - oldPsId); - } else { - PatchSet.Id n = newPatchSet.getId(); - checkState( - !n.equals(oldPsId) && n.equals(newPsId), - "current patch was %s and is now %s, but updateChangeImpl returned" - + " new patch set instance at %s", - oldPsId, - newPsId, - n); - mergedPatchSet = newPatchSet; - } - } - - Change c = ctx.getChange(); - Change.Id id = c.getId(); - CodeReviewCommit commit = args.commitStatus.get(id); - checkNotNull(commit, "missing commit for change " + id); - CommitMergeStatus s = commit.getStatusCode(); - checkNotNull(s, "status not set for change " + id + " expected to previously fail fast"); - logDebug("Status of change {} ({}) on {}: {}", id, commit.name(), c.getDest(), s); - setApproval(ctx, args.caller); - - mergeResultRev = - alreadyMergedCommit == null - ? args.mergeTip.getMergeResults().get(commit) - // Our fixup code is not smart enough to find a merge commit - // corresponding to the merge result. This results in a different - // ChangeMergedEvent in the fixup case, but we'll just live with that. - : alreadyMergedCommit; - try { - setMerged(ctx, message(ctx, commit, s)); - } catch (OrmException err) { - String msg = "Error updating change status for " + id; - log.error(msg, err); - args.commitStatus.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 PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) - throws IOException, OrmException { - PatchSet.Id psId = alreadyMergedCommit.getPatchsetId(); - logDebug("Fixing up already-merged patch set {}", psId); - PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes()); - ctx.getRevWalk().parseBody(alreadyMergedCommit); - ctx.getChange() - .setCurrentPatchSet( - psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject()); - PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId); - if (existing != null) { - logDebug("Patch set row exists, only updating change"); - return existing; - } - // No patch set for the already merged commit, although we know it came form - // a patch set ref. Fix up the database. Note that this uses the current - // user as the uploader, which is as good a guess as any. - List<String> groups = - prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit); - return args.psUtil.insert( - ctx.getDb(), - ctx.getRevWalk(), - ctx.getUpdate(psId), - psId, - alreadyMergedCommit, - groups, - null, - null); - } - - private void setApproval(ChangeContext ctx, IdentifiedUser user) - throws OrmException, IOException { - Change.Id id = ctx.getChange().getId(); - List<SubmitRecord> records = args.commitStatus.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(args.submissionId, 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, IOException { - PatchSet.Id psId = update.getPatchSetId(); - Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>(); - for (PatchSetApproval psa : - args.approvalsUtil.byPatchSet( - ctx.getDb(), - ctx.getNotes(), - ctx.getUser(), - psId, - ctx.getRevWalk(), - ctx.getRepoView().getConfig())) { - byKey.put(psa.getKey(), psa); - } - - submitter = - ApprovalsUtil.newApproval(psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen()); - byKey.put(submitter.getKey(), submitter); - - // 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.getNotes(), ctx.getUser(), 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().upsert(zero(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.isLegacySubmit()) { - logDebug("Adding submit label " + psa); - update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue()); - } - } - } - - private static Function<PatchSetApproval, PatchSetApproval> convertPatchSet( - final PatchSet.Id psId) { - return psa -> { - if (psa.getPatchSetId().equals(psId)) { - return psa; - } - return new PatchSetApproval(psId, psa); - }; - } - - private static Iterable<PatchSetApproval> convertPatchSet( - Iterable<PatchSetApproval> approvals, PatchSet.Id psId) { - return Iterables.transform(approvals, convertPatchSet(psId)); - } - - private static Iterable<PatchSetApproval> zero(Iterable<PatchSetApproval> approvals) { - return Iterables.transform( - approvals, - a -> { - PatchSetApproval copy = new PatchSetApproval(a.getPatchSetId(), a); - copy.setValue((short) 0); - return copy; - }); - } - - 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, CodeReviewCommit commit, CommitMergeStatus s) - throws OrmException { - checkNotNull(s, "CommitMergeStatus may not be null"); - String txt = s.getMessage(); - if (s == CommitMergeStatus.CLEAN_MERGE) { - return message(ctx, commit.getPatchsetId(), txt + getByAccountName()); - } else if (s == CommitMergeStatus.CLEAN_REBASE || s == CommitMergeStatus.CLEAN_PICK) { - return message( - ctx, commit.getPatchsetId(), txt + " as " + commit.name() + getByAccountName()); - } else if (s == CommitMergeStatus.SKIPPED_IDENTICAL_TREE) { - return message(ctx, commit.getPatchsetId(), txt); - } else if (s == CommitMergeStatus.ALREADY_MERGED) { - // Best effort to mimic the message that would have happened had this - // succeeded the first time around. - switch (args.submitType) { - case FAST_FORWARD_ONLY: - case MERGE_ALWAYS: - case MERGE_IF_NECESSARY: - return message(ctx, commit, CommitMergeStatus.CLEAN_MERGE); - case CHERRY_PICK: - return message(ctx, commit, CommitMergeStatus.CLEAN_PICK); - case REBASE_IF_NECESSARY: - case REBASE_ALWAYS: - return message(ctx, commit, CommitMergeStatus.CLEAN_REBASE); - default: - throw new IllegalStateException( - "unexpected submit type " - + args.submitType.toString() - + " for change " - + commit.change().getId()); - } - } else { - throw new IllegalStateException( - "unexpected status " - + s - + " for change " - + commit.change().getId() - + "; expected to previously fail fast"); - } - } - - private ChangeMessage message(ChangeContext ctx, PatchSet.Id psId, String body) { - return ChangeMessagesUtil.newMessage( - psId, ctx.getUser(), ctx.getWhen(), body, ChangeMessagesUtil.TAG_MERGED); - } - - private void setMerged(ChangeContext ctx, ChangeMessage msg) throws OrmException { - Change c = ctx.getChange(); - ReviewDb db = ctx.getDb(); - logDebug("Setting change {} merged", c.getId()); - c.setStatus(Change.Status.MERGED); - c.setSubmissionId(args.submissionId.toStringForStorage()); - - // 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 { - if (changeAlreadyMerged) { - // TODO(dborowitz): This is suboptimal behavior in the presence of retries: postUpdate steps - // will never get run for changes that submitted successfully on any but the final attempt. - // This is primarily a temporary workaround for the fact that the submitter field is not - // populated in the changeAlreadyMerged case. - // - // If we naively execute postUpdate even if the change is already merged when updateChange - // being, then we are subject to a race where postUpdate steps are run twice if two submit - // processes run at the same time. - logDebug("Skipping post-update steps for change {}", getId()); - return; - } - 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()); - try (Repository git = args.repoManager.openRepository(getProject())) { - git.setGitwebDescription(p.getProject().getDescription()); - } catch (IOException e) { - log.error("cannot update description of " + p.getName(), e); - } - } - } - - // 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( - ctx.getProject(), - getId(), - submitter.getAccountId(), - args.submitInput.notify, - args.accountsToNotify) - .sendAsync(); - } catch (Exception e) { - log.error("Cannot email merged notification for " + getId(), e); - } - if (mergeResultRev != null && !args.dryrun) { - args.changeMerged.fire( - updatedChange, - mergedPatchSet, - args.accountCache.get(submitter.getAccountId()).getAccount(), - args.mergeTip.getCurrentTip().name(), - ctx.getWhen()); - } - } - - /** - * @see #updateRepo(RepoContext) - * @param ctx - */ - protected void updateRepoImpl(RepoContext ctx) throws Exception {} - - /** - * @see #updateChange(ChangeContext) - * @param ctx - * @return a new patch set if one was created by the submit strategy, or null if not. - */ - protected PatchSet updateChangeImpl(ChangeContext ctx) throws Exception { - return null; - } - - /** - * @see #postUpdate(Context) - * @param ctx - */ - protected void postUpdateImpl(Context ctx) throws Exception {} - - /** - * Amend the commit with gitlink update - * - * @param commit - */ - protected CodeReviewCommit amendGitlink(CodeReviewCommit commit) throws IntegrationException { - if (!args.submoduleOp.hasSubscription(args.destBranch)) { - return commit; - } - - // Modify the commit with gitlink update - try { - return args.submoduleOp.composeGitlinksCommit(args.destBranch, commit); - } catch (SubmoduleException | IOException e) { - throw new IntegrationException( - "cannot update gitlink for the commit at branch: " + args.destBranch); - } - } - - 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); - } -} |