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;
16 :
17 : import static java.nio.charset.StandardCharsets.UTF_8;
18 : import static java.util.Objects.requireNonNull;
19 : import static java.util.stream.Collectors.joining;
20 : import static java.util.stream.Collectors.toSet;
21 :
22 : import com.google.auto.value.AutoValue;
23 : import com.google.common.base.CharMatcher;
24 : import com.google.common.base.Joiner;
25 : import com.google.common.base.Splitter;
26 : import com.google.common.collect.ImmutableListMultimap;
27 : import com.google.common.collect.ImmutableMap;
28 : import com.google.common.collect.ImmutableSet;
29 : import com.google.common.collect.ImmutableSortedSet;
30 : import com.google.common.flogger.FluentLogger;
31 : import com.google.common.primitives.Ints;
32 : import com.google.gerrit.common.Nullable;
33 : import com.google.gerrit.entities.Account;
34 : import com.google.gerrit.entities.Change;
35 : import com.google.gerrit.entities.RefNames;
36 : import com.google.gerrit.exceptions.StorageException;
37 : import com.google.gerrit.git.GitUpdateFailureException;
38 : import com.google.gerrit.git.LockFailureException;
39 : import com.google.gerrit.server.config.AllUsersName;
40 : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
41 : import com.google.gerrit.server.git.GitRepositoryManager;
42 : import com.google.gerrit.server.index.change.ChangeField;
43 : import com.google.gerrit.server.logging.Metadata;
44 : import com.google.gerrit.server.logging.TraceContext;
45 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
46 : import com.google.gerrit.server.project.NoSuchChangeException;
47 : import com.google.gerrit.server.query.change.ChangeData;
48 : import com.google.gerrit.server.query.change.InternalChangeQuery;
49 : import com.google.inject.Inject;
50 : import com.google.inject.Provider;
51 : import com.google.inject.Singleton;
52 : import java.io.IOException;
53 : import java.util.Collection;
54 : import java.util.Collections;
55 : import java.util.List;
56 : import java.util.NavigableSet;
57 : import java.util.Set;
58 : import java.util.TreeSet;
59 : import org.eclipse.jgit.lib.BatchRefUpdate;
60 : import org.eclipse.jgit.lib.Constants;
61 : import org.eclipse.jgit.lib.NullProgressMonitor;
62 : import org.eclipse.jgit.lib.ObjectId;
63 : import org.eclipse.jgit.lib.ObjectInserter;
64 : import org.eclipse.jgit.lib.ObjectLoader;
65 : import org.eclipse.jgit.lib.ObjectReader;
66 : import org.eclipse.jgit.lib.PersonIdent;
67 : import org.eclipse.jgit.lib.Ref;
68 : import org.eclipse.jgit.lib.RefDatabase;
69 : import org.eclipse.jgit.lib.RefUpdate;
70 : import org.eclipse.jgit.lib.Repository;
71 : import org.eclipse.jgit.revwalk.RevWalk;
72 : import org.eclipse.jgit.transport.ReceiveCommand;
73 :
74 : @Singleton
75 : public class StarredChangesUtil {
76 150 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
77 :
78 : @AutoValue
79 0 : public abstract static class StarField {
80 : private static final String SEPARATOR = ":";
81 :
82 : @Nullable
83 : public static StarField parse(String s) {
84 0 : int p = s.indexOf(SEPARATOR);
85 0 : if (p >= 0) {
86 0 : Integer id = Ints.tryParse(s.substring(0, p));
87 0 : if (id == null) {
88 0 : return null;
89 : }
90 0 : Account.Id accountId = Account.id(id);
91 0 : String label = s.substring(p + 1);
92 0 : return create(accountId, label);
93 : }
94 0 : return null;
95 : }
96 :
97 : public static StarField create(Account.Id accountId, String label) {
98 0 : return new AutoValue_StarredChangesUtil_StarField(accountId, label);
99 : }
100 :
101 : public abstract Account.Id accountId();
102 :
103 : public abstract String label();
104 :
105 : @Override
106 : public final String toString() {
107 0 : return accountId() + SEPARATOR + label();
108 : }
109 : }
110 :
111 10 : public enum Operation {
112 10 : ADD,
113 10 : REMOVE
114 : }
115 :
116 : @AutoValue
117 103 : public abstract static class StarRef {
118 103 : private static final StarRef MISSING =
119 103 : new AutoValue_StarredChangesUtil_StarRef(null, Collections.emptyNavigableSet());
120 :
121 : private static StarRef create(Ref ref, Iterable<String> labels) {
122 8 : return new AutoValue_StarredChangesUtil_StarRef(
123 8 : requireNonNull(ref), ImmutableSortedSet.copyOf(labels));
124 : }
125 :
126 : @Nullable
127 : public abstract Ref ref();
128 :
129 : public abstract NavigableSet<String> labels();
130 :
131 : public ObjectId objectId() {
132 10 : return ref() != null ? ref().getObjectId() : ObjectId.zeroId();
133 : }
134 : }
135 :
136 : public static class IllegalLabelException extends Exception {
137 : private static final long serialVersionUID = 1L;
138 :
139 : IllegalLabelException(String message) {
140 0 : super(message);
141 0 : }
142 : }
143 :
144 : public static class InvalidLabelsException extends IllegalLabelException {
145 : private static final long serialVersionUID = 1L;
146 :
147 : InvalidLabelsException(Set<String> invalidLabels) {
148 0 : super(String.format("invalid labels: %s", Joiner.on(", ").join(invalidLabels)));
149 0 : }
150 : }
151 :
152 : public static class MutuallyExclusiveLabelsException extends IllegalLabelException {
153 : private static final long serialVersionUID = 1L;
154 :
155 : MutuallyExclusiveLabelsException(String label1, String label2) {
156 0 : super(
157 0 : String.format(
158 : "The labels %s and %s are mutually exclusive. Only one of them can be set.",
159 : label1, label2));
160 0 : }
161 : }
162 :
163 : public static final String DEFAULT_LABEL = "star";
164 :
165 : private final GitRepositoryManager repoManager;
166 : private final GitReferenceUpdated gitRefUpdated;
167 : private final AllUsersName allUsers;
168 : private final Provider<PersonIdent> serverIdent;
169 : private final Provider<InternalChangeQuery> queryProvider;
170 :
171 : @Inject
172 : StarredChangesUtil(
173 : GitRepositoryManager repoManager,
174 : GitReferenceUpdated gitRefUpdated,
175 : AllUsersName allUsers,
176 : @GerritPersonIdent Provider<PersonIdent> serverIdent,
177 150 : Provider<InternalChangeQuery> queryProvider) {
178 150 : this.repoManager = repoManager;
179 150 : this.gitRefUpdated = gitRefUpdated;
180 150 : this.allUsers = allUsers;
181 150 : this.serverIdent = serverIdent;
182 150 : this.queryProvider = queryProvider;
183 150 : }
184 :
185 : public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
186 103 : try (Repository repo = repoManager.openRepository(allUsers)) {
187 103 : return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
188 0 : } catch (IOException e) {
189 0 : throw new StorageException(
190 0 : String.format(
191 : "Reading stars from change %d for account %d failed",
192 0 : changeId.get(), accountId.get()),
193 : e);
194 : }
195 : }
196 :
197 : public void star(Account.Id accountId, Change.Id changeId, Operation op)
198 : throws IllegalLabelException {
199 10 : try (Repository repo = repoManager.openRepository(allUsers)) {
200 10 : String refName = RefNames.refsStarredChanges(changeId, accountId);
201 10 : StarRef old = readLabels(repo, refName);
202 :
203 10 : NavigableSet<String> labels = new TreeSet<>(old.labels());
204 10 : switch (op) {
205 : case ADD:
206 10 : labels.add(DEFAULT_LABEL);
207 10 : break;
208 : case REMOVE:
209 3 : labels.remove(DEFAULT_LABEL);
210 : break;
211 : }
212 :
213 10 : if (labels.isEmpty()) {
214 3 : deleteRef(repo, refName, old.objectId());
215 : } else {
216 10 : updateLabels(repo, refName, old.objectId(), labels);
217 : }
218 0 : } catch (IOException e) {
219 0 : throw new StorageException(
220 0 : String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
221 : e);
222 10 : }
223 10 : }
224 :
225 : /**
226 : * Unstar the given change for all users.
227 : *
228 : * <p>Intended for use only when we're about to delete a change. For that reason, the change is
229 : * not reindexed.
230 : *
231 : * @param changeId change ID.
232 : * @throws IOException if an error occurred.
233 : */
234 : public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
235 11 : try (Repository repo = repoManager.openRepository(allUsers);
236 11 : RevWalk rw = new RevWalk(repo)) {
237 11 : BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
238 11 : batchUpdate.setAllowNonFastForwards(true);
239 11 : batchUpdate.setRefLogIdent(serverIdent.get());
240 11 : batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
241 11 : for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
242 0 : String refName = RefNames.refsStarredChanges(changeId, accountId);
243 0 : Ref ref = repo.getRefDatabase().exactRef(refName);
244 0 : if (ref != null) {
245 0 : batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
246 : }
247 0 : }
248 11 : batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
249 11 : for (ReceiveCommand command : batchUpdate.getCommands()) {
250 0 : if (command.getResult() != ReceiveCommand.Result.OK) {
251 0 : String message =
252 0 : String.format(
253 : "Unstar change %d failed, ref %s could not be deleted: %s",
254 0 : changeId.get(), command.getRefName(), command.getResult());
255 0 : if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
256 0 : throw new LockFailureException(message, batchUpdate);
257 : }
258 0 : throw new GitUpdateFailureException(message, batchUpdate);
259 : }
260 0 : }
261 : }
262 11 : }
263 :
264 : public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
265 103 : try (Repository repo = repoManager.openRepository(allUsers)) {
266 103 : ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
267 103 : for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
268 1 : Integer id = Ints.tryParse(refPart);
269 1 : if (id == null) {
270 0 : continue;
271 : }
272 1 : Account.Id accountId = Account.id(id);
273 1 : builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
274 1 : }
275 103 : return builder.build();
276 0 : } catch (IOException e) {
277 0 : throw new StorageException(
278 0 : String.format("Get accounts that starred change %d failed", changeId.get()), e);
279 : }
280 : }
281 :
282 : public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, String label) {
283 4 : try (Repository repo = repoManager.openRepository(allUsers)) {
284 4 : ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
285 4 : for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
286 4 : Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
287 : // Skip all refs that don't correspond with accountId.
288 4 : if (currentAccountId == null || !currentAccountId.equals(accountId)) {
289 4 : continue;
290 : }
291 : // Skip all refs that don't contain the required label.
292 4 : StarRef starRef = readLabels(repo, ref.getName());
293 4 : if (!starRef.labels().contains(label)) {
294 0 : continue;
295 : }
296 :
297 : // Skip invalid change ids.
298 4 : Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
299 4 : if (changeId == null) {
300 0 : continue;
301 : }
302 4 : builder.add(changeId);
303 4 : }
304 4 : return builder.build();
305 0 : } catch (IOException e) {
306 0 : throw new StorageException(
307 0 : String.format("Get starred changes for account %d failed", accountId.get()), e);
308 : }
309 : }
310 :
311 : public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
312 11 : List<ChangeData> changeData =
313 : queryProvider
314 11 : .get()
315 11 : .setRequestedFields(ChangeField.ID, ChangeField.STAR)
316 11 : .byLegacyChangeId(changeId);
317 11 : if (changeData.size() != 1) {
318 0 : throw new NoSuchChangeException(changeId);
319 : }
320 11 : return changeData.get(0).stars();
321 : }
322 :
323 : private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
324 103 : RefDatabase refDb = repo.getRefDatabase();
325 103 : return refDb.getRefsByPrefix(prefix).stream()
326 103 : .map(r -> r.getName().substring(prefix.length()))
327 103 : .collect(toSet());
328 : }
329 :
330 : public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
331 5 : try (Repository repo = repoManager.openRepository(allUsers)) {
332 5 : Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
333 5 : return ref != null ? ref.getObjectId() : ObjectId.zeroId();
334 0 : } catch (IOException e) {
335 0 : logger.atSevere().withCause(e).log(
336 : "Getting star object ID for account %d on change %d failed",
337 0 : accountId.get(), changeId.get());
338 0 : return ObjectId.zeroId();
339 : }
340 : }
341 :
342 : public static StarRef readLabels(Repository repo, String refName) throws IOException {
343 103 : try (TraceTimer traceTimer =
344 103 : TraceContext.newTimer(
345 103 : "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
346 103 : Ref ref = repo.exactRef(refName);
347 103 : return readLabels(repo, ref);
348 : }
349 : }
350 :
351 : public static StarRef readLabels(Repository repo, Ref ref) throws IOException {
352 103 : if (ref == null) {
353 103 : return StarRef.MISSING;
354 : }
355 8 : try (TraceTimer traceTimer =
356 8 : TraceContext.newTimer(
357 8 : String.format("Read star labels from %s (without ref lookup)", ref.getName()));
358 8 : ObjectReader reader = repo.newObjectReader()) {
359 8 : ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
360 8 : return StarRef.create(
361 : ref,
362 8 : Splitter.on(CharMatcher.whitespace())
363 8 : .omitEmptyStrings()
364 8 : .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
365 : }
366 : }
367 :
368 : public static ObjectId writeLabels(Repository repo, Collection<String> labels)
369 : throws IOException, InvalidLabelsException {
370 10 : validateLabels(labels);
371 10 : try (ObjectInserter oi = repo.newObjectInserter()) {
372 10 : ObjectId id =
373 10 : oi.insert(
374 : Constants.OBJ_BLOB,
375 10 : labels.stream().sorted().distinct().collect(joining("\n")).getBytes(UTF_8));
376 10 : oi.flush();
377 10 : return id;
378 : }
379 : }
380 :
381 : private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
382 10 : if (labels == null) {
383 0 : return;
384 : }
385 :
386 10 : NavigableSet<String> invalidLabels = new TreeSet<>();
387 10 : for (String label : labels) {
388 10 : if (CharMatcher.whitespace().matchesAnyOf(label)) {
389 0 : invalidLabels.add(label);
390 : }
391 10 : }
392 10 : if (!invalidLabels.isEmpty()) {
393 0 : throw new InvalidLabelsException(invalidLabels);
394 : }
395 10 : }
396 :
397 : private void updateLabels(
398 : Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
399 : throws IOException, InvalidLabelsException {
400 10 : try (TraceTimer traceTimer =
401 10 : TraceContext.newTimer(
402 : "Update star labels",
403 10 : Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
404 10 : RevWalk rw = new RevWalk(repo)) {
405 10 : RefUpdate u = repo.updateRef(refName);
406 10 : u.setExpectedOldObjectId(oldObjectId);
407 10 : u.setForceUpdate(true);
408 10 : u.setNewObjectId(writeLabels(repo, labels));
409 10 : u.setRefLogIdent(serverIdent.get());
410 10 : u.setRefLogMessage("Update star labels", true);
411 10 : RefUpdate.Result result = u.update(rw);
412 10 : switch (result) {
413 : case NEW:
414 : case FORCED:
415 : case NO_CHANGE:
416 : case FAST_FORWARD:
417 10 : gitRefUpdated.fire(allUsers, u, null);
418 10 : return;
419 : case LOCK_FAILURE:
420 0 : throw new LockFailureException(
421 0 : String.format("Update star labels on ref %s failed", refName), u);
422 : case IO_FAILURE:
423 : case NOT_ATTEMPTED:
424 : case REJECTED:
425 : case REJECTED_CURRENT_BRANCH:
426 : case RENAMED:
427 : case REJECTED_MISSING_OBJECT:
428 : case REJECTED_OTHER_REASON:
429 : default:
430 0 : throw new StorageException(
431 0 : String.format("Update star labels on ref %s failed: %s", refName, result.name()));
432 : }
433 : }
434 : }
435 :
436 : private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
437 3 : if (ObjectId.zeroId().equals(oldObjectId)) {
438 : // ref doesn't exist
439 0 : return;
440 : }
441 :
442 3 : try (TraceTimer traceTimer =
443 3 : TraceContext.newTimer(
444 3 : "Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
445 3 : RefUpdate u = repo.updateRef(refName);
446 3 : u.setForceUpdate(true);
447 3 : u.setExpectedOldObjectId(oldObjectId);
448 3 : u.setRefLogIdent(serverIdent.get());
449 3 : u.setRefLogMessage("Unstar change", true);
450 3 : RefUpdate.Result result = u.delete();
451 3 : switch (result) {
452 : case FORCED:
453 3 : gitRefUpdated.fire(allUsers, u, null);
454 3 : return;
455 : case LOCK_FAILURE:
456 0 : throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
457 : case NEW:
458 : case NO_CHANGE:
459 : case FAST_FORWARD:
460 : case IO_FAILURE:
461 : case NOT_ATTEMPTED:
462 : case REJECTED:
463 : case REJECTED_CURRENT_BRANCH:
464 : case RENAMED:
465 : case REJECTED_MISSING_OBJECT:
466 : case REJECTED_OTHER_REASON:
467 : default:
468 0 : throw new StorageException(
469 0 : String.format("Delete star ref %s failed: %s", refName, result.name()));
470 : }
471 : }
472 : }
473 : }
|