Line data Source code
1 : // Copyright (C) 2015 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.change; 16 : 17 : import static com.google.common.flogger.LazyArgs.lazy; 18 : 19 : import com.google.common.flogger.FluentLogger; 20 : import com.google.gerrit.entities.Change; 21 : import com.google.gerrit.entities.PatchSet; 22 : import com.google.gerrit.entities.RefNames; 23 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 24 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 25 : import com.google.gerrit.extensions.restapi.RestApiException; 26 : import com.google.gerrit.server.PatchSetUtil; 27 : import com.google.gerrit.server.StarredChangesUtil; 28 : import com.google.gerrit.server.extensions.events.ChangeDeleted; 29 : import com.google.gerrit.server.plugincontext.PluginItemContext; 30 : import com.google.gerrit.server.query.change.ChangeData; 31 : import com.google.gerrit.server.update.BatchUpdateOp; 32 : import com.google.gerrit.server.update.ChangeContext; 33 : import com.google.gerrit.server.update.RepoContext; 34 : import com.google.inject.Inject; 35 : import com.google.inject.assistedinject.Assisted; 36 : import java.io.IOException; 37 : import java.util.Collection; 38 : import java.util.Map; 39 : import java.util.Optional; 40 : import org.eclipse.jgit.lib.ObjectId; 41 : import org.eclipse.jgit.revwalk.RevWalk; 42 : 43 : public class DeleteChangeOp implements BatchUpdateOp { 44 11 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 45 : 46 : public interface Factory { 47 : DeleteChangeOp create(Change.Id id); 48 : } 49 : 50 : private final PatchSetUtil psUtil; 51 : private final StarredChangesUtil starredChangesUtil; 52 : private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore; 53 : private final ChangeData.Factory changeDataFactory; 54 : private final ChangeDeleted changeDeleted; 55 : private final Change.Id id; 56 : 57 : @Inject 58 : DeleteChangeOp( 59 : PatchSetUtil psUtil, 60 : StarredChangesUtil starredChangesUtil, 61 : PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore, 62 : ChangeData.Factory changeDataFactory, 63 : ChangeDeleted changeDeleted, 64 11 : @Assisted Change.Id id) { 65 11 : this.psUtil = psUtil; 66 11 : this.starredChangesUtil = starredChangesUtil; 67 11 : this.accountPatchReviewStore = accountPatchReviewStore; 68 11 : this.changeDataFactory = changeDataFactory; 69 11 : this.changeDeleted = changeDeleted; 70 11 : this.id = id; 71 11 : } 72 : 73 : // The relative order of updateChange and updateRepo doesn't matter as long as all operations are 74 : // executed in a single atomic BatchRefUpdate. Actually deleting the change refs first would not 75 : // fail gracefully if the second delete fails, but fortunately that's not what happens. 76 : @Override 77 : public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException { 78 11 : Collection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes()); 79 : 80 11 : ensureDeletable(ctx, id, patchSets); 81 : // Cleaning up is only possible as long as the change and its elements are 82 : // still part of the database. 83 11 : cleanUpReferences(id); 84 : 85 11 : logger.atFine().log( 86 : "Deleting change %s, current patch set %d is commit %s", 87 : id, 88 11 : ctx.getChange().currentPatchSetId().get(), 89 11 : lazy( 90 : () -> 91 4 : patchSets.stream() 92 4 : .filter(p -> p.number() == ctx.getChange().currentPatchSetId().get()) 93 4 : .findAny() 94 4 : .map(p -> p.commitId().name()) 95 4 : .orElse("n/a"))); 96 11 : ctx.deleteChange(); 97 11 : changeDeleted.fire(changeDataFactory.create(ctx.getChange()), ctx.getAccount(), ctx.getWhen()); 98 11 : return true; 99 : } 100 : 101 : private void ensureDeletable(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets) 102 : throws ResourceConflictException, MethodNotAllowedException, IOException { 103 11 : if (ctx.getChange().isMerged()) { 104 0 : throw new MethodNotAllowedException("Deleting merged change " + id + " is not allowed"); 105 : } 106 11 : for (PatchSet patchSet : patchSets) { 107 11 : if (isPatchSetMerged(ctx, patchSet)) { 108 1 : throw new ResourceConflictException( 109 1 : String.format( 110 1 : "Cannot delete change %s: patch set %s is already merged", id, patchSet.number())); 111 : } 112 11 : } 113 11 : } 114 : 115 : private boolean isPatchSetMerged(ChangeContext ctx, PatchSet patchSet) throws IOException { 116 11 : Optional<ObjectId> destId = ctx.getRepoView().getRef(ctx.getChange().getDest().branch()); 117 11 : if (!destId.isPresent()) { 118 5 : return false; 119 : } 120 : 121 7 : RevWalk revWalk = ctx.getRevWalk(); 122 7 : return revWalk.isMergedInto( 123 7 : revWalk.parseCommit(patchSet.commitId()), revWalk.parseCommit(destId.get())); 124 : } 125 : 126 : private void cleanUpReferences(Change.Id id) throws IOException { 127 11 : accountPatchReviewStore.run(s -> s.clearReviewed(id)); 128 : 129 : // Non-atomic operation on All-Users refs; not much we can do to make it atomic. 130 11 : starredChangesUtil.unstarAllForChangeDeletion(id); 131 11 : } 132 : 133 : @Override 134 : public void updateRepo(RepoContext ctx) throws IOException { 135 11 : String changeRefPrefix = RefNames.changeRefPrefix(id); 136 11 : for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(changeRefPrefix).entrySet()) { 137 11 : removeRef(ctx, e, changeRefPrefix); 138 11 : } 139 11 : removeUserEdits(ctx); 140 11 : } 141 : 142 : private void removeUserEdits(RepoContext ctx) throws IOException { 143 11 : String prefix = RefNames.REFS_USERS; 144 11 : String editRef = String.format("/edit-%s/", id); 145 11 : for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) { 146 1 : if (e.getKey().contains(editRef)) { 147 1 : removeRef(ctx, e, prefix); 148 : } 149 1 : } 150 11 : } 151 : 152 : private void removeRef(RepoContext ctx, Map.Entry<String, ObjectId> entry, String prefix) 153 : throws IOException { 154 11 : ctx.addRefUpdate(entry.getValue(), ObjectId.zeroId(), prefix + entry.getKey()); 155 11 : } 156 : }