Line data Source code
1 : // Copyright (C) 2016 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.restapi.project;
16 :
17 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
18 : import static com.google.gerrit.entities.RefNames.isConfigRef;
19 : import static java.lang.String.format;
20 : import static org.eclipse.jgit.lib.Constants.R_REFS;
21 : import static org.eclipse.jgit.lib.Constants.R_TAGS;
22 : import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
23 :
24 : import com.google.common.collect.ImmutableListMultimap;
25 : import com.google.common.collect.ImmutableSet;
26 : import com.google.common.collect.Iterables;
27 : import com.google.common.flogger.FluentLogger;
28 : import com.google.gerrit.common.Nullable;
29 : import com.google.gerrit.entities.BranchNameKey;
30 : import com.google.gerrit.entities.Project;
31 : import com.google.gerrit.exceptions.StorageException;
32 : import com.google.gerrit.extensions.restapi.AuthException;
33 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
34 : import com.google.gerrit.git.LockFailureException;
35 : import com.google.gerrit.server.IdentifiedUser;
36 : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
37 : import com.google.gerrit.server.git.GitRepositoryManager;
38 : import com.google.gerrit.server.permissions.PermissionBackend;
39 : import com.google.gerrit.server.permissions.PermissionBackendException;
40 : import com.google.gerrit.server.permissions.RefPermission;
41 : import com.google.gerrit.server.project.ProjectState;
42 : import com.google.gerrit.server.project.RefValidationHelper;
43 : import com.google.gerrit.server.query.change.InternalChangeQuery;
44 : import com.google.inject.Inject;
45 : import com.google.inject.Provider;
46 : import com.google.inject.Singleton;
47 : import java.io.IOException;
48 : import org.eclipse.jgit.lib.BatchRefUpdate;
49 : import org.eclipse.jgit.lib.NullProgressMonitor;
50 : import org.eclipse.jgit.lib.ObjectId;
51 : import org.eclipse.jgit.lib.Ref;
52 : import org.eclipse.jgit.lib.RefUpdate;
53 : import org.eclipse.jgit.lib.Repository;
54 : import org.eclipse.jgit.revwalk.RevWalk;
55 : import org.eclipse.jgit.transport.ReceiveCommand;
56 : import org.eclipse.jgit.transport.ReceiveCommand.Result;
57 :
58 : @Singleton
59 : public class DeleteRef {
60 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
61 :
62 : private final Provider<IdentifiedUser> identifiedUser;
63 : private final PermissionBackend permissionBackend;
64 : private final GitRepositoryManager repoManager;
65 : private final GitReferenceUpdated referenceUpdated;
66 : private final RefValidationHelper refDeletionValidator;
67 : private final Provider<InternalChangeQuery> queryProvider;
68 :
69 : @Inject
70 : DeleteRef(
71 : Provider<IdentifiedUser> identifiedUser,
72 : PermissionBackend permissionBackend,
73 : GitRepositoryManager repoManager,
74 : GitReferenceUpdated referenceUpdated,
75 : RefValidationHelper.Factory refDeletionValidatorFactory,
76 146 : Provider<InternalChangeQuery> queryProvider) {
77 146 : this.identifiedUser = identifiedUser;
78 146 : this.permissionBackend = permissionBackend;
79 146 : this.repoManager = repoManager;
80 146 : this.referenceUpdated = referenceUpdated;
81 146 : this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE);
82 146 : this.queryProvider = queryProvider;
83 146 : }
84 :
85 : /**
86 : * Deletes a single ref from the repository.
87 : *
88 : * @param projectState the {@code ProjectState} of the project containing the target ref.
89 : * @param ref the ref to be deleted.
90 : */
91 : public void deleteSingleRef(ProjectState projectState, String ref)
92 : throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
93 2 : deleteSingleRef(projectState, ref, null);
94 2 : }
95 :
96 : /**
97 : * Deletes a single ref from the repository.
98 : *
99 : * @param projectState the {@code ProjectState} of the project containing the target ref.
100 : * @param ref the ref to be deleted.
101 : * @param prefix the prefix of the ref.
102 : */
103 : public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
104 : throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
105 7 : if (prefix != null && !ref.startsWith(R_REFS)) {
106 0 : ref = prefix + ref;
107 : }
108 :
109 7 : projectState.checkStatePermitsWrite();
110 7 : permissionBackend
111 7 : .currentUser()
112 7 : .project(projectState.getNameKey())
113 7 : .ref(ref)
114 6 : .check(RefPermission.DELETE);
115 :
116 6 : try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
117 : RefUpdate.Result result;
118 6 : RefUpdate u = repository.updateRef(ref);
119 6 : u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
120 6 : u.setNewObjectId(ObjectId.zeroId());
121 6 : u.setForceUpdate(true);
122 6 : refDeletionValidator.validateRefOperation(
123 6 : projectState.getName(),
124 6 : identifiedUser.get(),
125 : u,
126 6 : /* pushOptions */ ImmutableListMultimap.of());
127 5 : result = u.delete();
128 :
129 5 : switch (result) {
130 : case NEW:
131 : case NO_CHANGE:
132 : case FAST_FORWARD:
133 : case FORCED:
134 5 : referenceUpdated.fire(
135 5 : projectState.getNameKey(),
136 : u,
137 : ReceiveCommand.Type.DELETE,
138 5 : identifiedUser.get().state());
139 5 : break;
140 :
141 : case REJECTED_CURRENT_BRANCH:
142 0 : logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
143 0 : throw new ResourceConflictException("cannot delete current branch");
144 :
145 : case LOCK_FAILURE:
146 0 : throw new LockFailureException(String.format("Cannot delete %s", ref), u);
147 : case IO_FAILURE:
148 : case NOT_ATTEMPTED:
149 : case REJECTED:
150 : case RENAMED:
151 : case REJECTED_MISSING_OBJECT:
152 : case REJECTED_OTHER_REASON:
153 : default:
154 0 : throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
155 : }
156 : }
157 5 : }
158 :
159 : /**
160 : * Deletes a set of refs from the repository.
161 : *
162 : * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
163 : * @param refsToDelete the refs to be deleted.
164 : * @param prefix the prefix to add to abbreviated refs, eg. "refs/heads/".
165 : */
166 : public void deleteMultipleRefs(
167 : ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
168 : throws IOException, ResourceConflictException, PermissionBackendException, AuthException {
169 2 : if (refsToDelete.isEmpty()) {
170 0 : return;
171 : }
172 :
173 2 : if (refsToDelete.size() == 1) {
174 0 : deleteSingleRef(projectState, Iterables.getOnlyElement(refsToDelete), prefix);
175 0 : return;
176 : }
177 :
178 2 : try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
179 2 : BatchRefUpdate batchUpdate = repository.getRefDatabase().newBatchUpdate();
180 2 : batchUpdate.setAtomic(false);
181 : ImmutableSet<String> refs =
182 2 : prefix == null
183 0 : ? refsToDelete
184 2 : : refsToDelete.stream()
185 2 : .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref)
186 2 : .collect(toImmutableSet());
187 2 : for (String ref : refs) {
188 2 : batchUpdate.addCommand(createDeleteCommand(projectState, repository, ref));
189 2 : }
190 2 : try (RevWalk rw = new RevWalk(repository)) {
191 2 : batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
192 : }
193 2 : StringBuilder errorMessages = new StringBuilder();
194 2 : for (ReceiveCommand command : batchUpdate.getCommands()) {
195 2 : if (command.getResult() == Result.OK) {
196 2 : postDeletion(projectState.getNameKey(), command);
197 : } else {
198 2 : appendAndLogErrorMessage(errorMessages, command);
199 : }
200 2 : }
201 2 : if (errorMessages.length() > 0) {
202 2 : throw new ResourceConflictException(errorMessages.toString());
203 : }
204 : }
205 2 : }
206 :
207 : private ReceiveCommand createDeleteCommand(
208 : ProjectState projectState, Repository r, String refName)
209 : throws IOException, ResourceConflictException, PermissionBackendException {
210 2 : Ref ref = r.getRefDatabase().exactRef(refName);
211 : ReceiveCommand command;
212 2 : if (ref == null) {
213 2 : command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), refName);
214 2 : command.setResult(
215 : Result.REJECTED_OTHER_REASON,
216 : "it doesn't exist or you do not have permission to delete it");
217 2 : return command;
218 : }
219 2 : command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
220 :
221 2 : if (isConfigRef(refName)) {
222 : // Never allow to delete the meta config branch.
223 0 : command.setResult(Result.REJECTED_OTHER_REASON, "not allowed to delete branch " + refName);
224 : } else {
225 : try {
226 2 : permissionBackend
227 2 : .currentUser()
228 2 : .project(projectState.getNameKey())
229 2 : .ref(refName)
230 2 : .check(RefPermission.DELETE);
231 2 : } catch (AuthException denied) {
232 2 : command.setResult(
233 : Result.REJECTED_OTHER_REASON,
234 : "it doesn't exist or you do not have permission to delete it");
235 2 : }
236 : }
237 :
238 2 : if (!projectState.statePermitsWrite()) {
239 0 : command.setResult(Result.REJECTED_OTHER_REASON, "project state does not permit write");
240 : }
241 :
242 2 : if (!refName.startsWith(R_TAGS)) {
243 1 : BranchNameKey branchKey = BranchNameKey.create(projectState.getNameKey(), ref.getName());
244 1 : if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
245 0 : command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
246 : }
247 : }
248 :
249 2 : RefUpdate u = r.updateRef(refName);
250 2 : u.setForceUpdate(true);
251 2 : u.setExpectedOldObjectId(r.exactRef(refName).getObjectId());
252 2 : u.setNewObjectId(ObjectId.zeroId());
253 2 : refDeletionValidator.validateRefOperation(
254 2 : projectState.getName(),
255 2 : identifiedUser.get(),
256 : u,
257 2 : /* pushOptions */ ImmutableListMultimap.of());
258 2 : return command;
259 : }
260 :
261 : private void appendAndLogErrorMessage(StringBuilder errorMessages, ReceiveCommand cmd) {
262 : String msg;
263 2 : switch (cmd.getResult()) {
264 : case REJECTED_CURRENT_BRANCH:
265 0 : msg = format("Cannot delete %s: it is the current branch", cmd.getRefName());
266 0 : break;
267 : case REJECTED_OTHER_REASON:
268 2 : msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
269 2 : break;
270 : case LOCK_FAILURE:
271 : case NOT_ATTEMPTED:
272 : case OK:
273 : case REJECTED_MISSING_OBJECT:
274 : case REJECTED_NOCREATE:
275 : case REJECTED_NODELETE:
276 : case REJECTED_NONFASTFORWARD:
277 : default:
278 0 : msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
279 : break;
280 : }
281 2 : logger.atSevere().log("%s", msg);
282 2 : errorMessages.append(msg);
283 2 : errorMessages.append("\n");
284 2 : }
285 :
286 : private void postDeletion(Project.NameKey project, ReceiveCommand cmd) {
287 2 : referenceUpdated.fire(project, cmd, identifiedUser.get().state());
288 2 : }
289 : }
|