| // Copyright (C) 2015 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.change; |
| |
| import static com.google.common.flogger.LazyArgs.lazy; |
| |
| import com.google.common.collect.ImmutableCollection; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.restapi.MethodNotAllowedException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.server.PatchSetUtil; |
| import com.google.gerrit.server.StarredChangesWriter; |
| import com.google.gerrit.server.extensions.events.ChangeDeleted; |
| import com.google.gerrit.server.plugincontext.PluginItemContext; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.update.BatchUpdateOp; |
| import com.google.gerrit.server.update.ChangeContext; |
| import com.google.gerrit.server.update.RepoContext; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Map; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| public class DeleteChangeOp implements BatchUpdateOp { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public interface Factory { |
| DeleteChangeOp create(Change.Id id); |
| } |
| |
| private final PatchSetUtil psUtil; |
| private final StarredChangesWriter starredChangesWriter; |
| private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore; |
| private final ChangeData.Factory changeDataFactory; |
| private final ChangeDeleted changeDeleted; |
| private final Change.Id id; |
| |
| @Inject |
| DeleteChangeOp( |
| PatchSetUtil psUtil, |
| StarredChangesWriter starredChangesWriter, |
| PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore, |
| ChangeData.Factory changeDataFactory, |
| ChangeDeleted changeDeleted, |
| @Assisted Change.Id id) { |
| this.psUtil = psUtil; |
| this.starredChangesWriter = starredChangesWriter; |
| this.accountPatchReviewStore = accountPatchReviewStore; |
| this.changeDataFactory = changeDataFactory; |
| this.changeDeleted = changeDeleted; |
| this.id = id; |
| } |
| |
| // The relative order of updateChange and updateRepo doesn't matter as long as all operations are |
| // executed in a single atomic BatchRefUpdate. Actually deleting the change refs first would not |
| // fail gracefully if the second delete fails, but fortunately that's not what happens. |
| @Override |
| public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException { |
| ImmutableCollection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes()); |
| |
| ensureDeletable(ctx, id, patchSets); |
| // Cleaning up is only possible as long as the change and its elements are |
| // still part of the database. |
| ChangeData cd = changeDataFactory.create(ctx.getChange()); |
| cleanUpReferences(cd); |
| |
| logger.atFine().log( |
| "Deleting change %s, current patch set %d is commit %s", |
| id, |
| ctx.getChange().currentPatchSetId().get(), |
| lazy( |
| () -> |
| patchSets.stream() |
| .filter(p -> p.number() == ctx.getChange().currentPatchSetId().get()) |
| .findAny() |
| .map(p -> p.commitId().name()) |
| .orElse("n/a"))); |
| ctx.deleteChange(); |
| changeDeleted.fire(cd, ctx.getAccount(), ctx.getWhen()); |
| return true; |
| } |
| |
| private void ensureDeletable(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets) |
| throws ResourceConflictException, MethodNotAllowedException, IOException { |
| if (ctx.getChange().isMerged()) { |
| throw new MethodNotAllowedException("Deleting merged change " + id + " is not allowed"); |
| } |
| for (PatchSet patchSet : patchSets) { |
| if (isPatchSetMerged(ctx, patchSet)) { |
| throw new ResourceConflictException( |
| String.format( |
| "Cannot delete change %s: patch set %s is already merged", id, patchSet.number())); |
| } |
| } |
| } |
| |
| private boolean isPatchSetMerged(ChangeContext ctx, PatchSet patchSet) throws IOException { |
| Optional<ObjectId> destId = ctx.getRepoView().getRef(ctx.getChange().getDest().branch()); |
| if (!destId.isPresent()) { |
| return false; |
| } |
| |
| RevWalk revWalk = ctx.getRevWalk(); |
| return revWalk.isMergedInto( |
| revWalk.parseCommit(patchSet.commitId()), revWalk.parseCommit(destId.get())); |
| } |
| |
| private void cleanUpReferences(ChangeData cd) throws IOException { |
| accountPatchReviewStore.run(s -> s.clearReviewed(cd.virtualId())); |
| |
| // Non-atomic operation on All-Users refs; not much we can do to make it atomic. |
| starredChangesWriter.unstarAllForChangeDeletion(cd.virtualId()); |
| } |
| |
| @Override |
| public void updateRepo(RepoContext ctx) throws IOException { |
| String changeRefPrefix = RefNames.changeRefPrefix(id); |
| for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(changeRefPrefix).entrySet()) { |
| removeRef(ctx, e, changeRefPrefix); |
| } |
| removeUserEdits(ctx); |
| } |
| |
| private void removeUserEdits(RepoContext ctx) throws IOException { |
| String prefix = RefNames.REFS_USERS; |
| String editRef = String.format("/edit-%s/", id); |
| for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) { |
| if (e.getKey().contains(editRef)) { |
| removeRef(ctx, e, prefix); |
| } |
| } |
| } |
| |
| private void removeRef(RepoContext ctx, Map.Entry<String, ObjectId> entry, String prefix) |
| throws IOException { |
| ctx.addRefUpdate(entry.getValue(), ObjectId.zeroId(), prefix + entry.getKey()); |
| } |
| } |