summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/StarredChangesUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/StarredChangesUtil.java')
-rw-r--r--java/com/google/gerrit/server/StarredChangesUtil.java519
1 files changed, 519 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
new file mode 100644
index 0000000000..4e349e6948
--- /dev/null
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -0,0 +1,519 @@
+// 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+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.server.change.ChangeResource;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+@Singleton
+public class StarredChangesUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @AutoValue
+ public abstract static class StarField {
+ private static final String SEPARATOR = ":";
+
+ public static StarField parse(String s) {
+ int p = s.indexOf(SEPARATOR);
+ if (p >= 0) {
+ Integer id = Ints.tryParse(s.substring(0, p));
+ if (id == null) {
+ return null;
+ }
+ Account.Id accountId = new Account.Id(id);
+ String label = s.substring(p + 1);
+ return create(accountId, label);
+ }
+ return null;
+ }
+
+ public static StarField create(Account.Id accountId, String label) {
+ return new AutoValue_StarredChangesUtil_StarField(accountId, label);
+ }
+
+ public abstract Account.Id accountId();
+
+ public abstract String label();
+
+ @Override
+ public String toString() {
+ return accountId() + SEPARATOR + label();
+ }
+ }
+
+ @AutoValue
+ public abstract static class StarRef {
+ private static final StarRef MISSING =
+ new AutoValue_StarredChangesUtil_StarRef(null, ImmutableSortedSet.of());
+
+ private static StarRef create(Ref ref, Iterable<String> labels) {
+ return new AutoValue_StarredChangesUtil_StarRef(
+ requireNonNull(ref), ImmutableSortedSet.copyOf(labels));
+ }
+
+ @Nullable
+ public abstract Ref ref();
+
+ public abstract ImmutableSortedSet<String> labels();
+
+ public ObjectId objectId() {
+ return ref() != null ? ref().getObjectId() : ObjectId.zeroId();
+ }
+ }
+
+ public static class IllegalLabelException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ IllegalLabelException(String message) {
+ super(message);
+ }
+ }
+
+ public static class InvalidLabelsException extends IllegalLabelException {
+ private static final long serialVersionUID = 1L;
+
+ InvalidLabelsException(Set<String> invalidLabels) {
+ super(String.format("invalid labels: %s", Joiner.on(", ").join(invalidLabels)));
+ }
+ }
+
+ public static class MutuallyExclusiveLabelsException extends IllegalLabelException {
+ private static final long serialVersionUID = 1L;
+
+ MutuallyExclusiveLabelsException(String label1, String label2) {
+ super(
+ String.format(
+ "The labels %s and %s are mutually exclusive. Only one of them can be set.",
+ label1, label2));
+ }
+ }
+
+ public static final String DEFAULT_LABEL = "star";
+ public static final String IGNORE_LABEL = "ignore";
+ public static final String REVIEWED_LABEL = "reviewed";
+ public static final String UNREVIEWED_LABEL = "unreviewed";
+ public static final ImmutableSortedSet<String> DEFAULT_LABELS =
+ ImmutableSortedSet.of(DEFAULT_LABEL);
+
+ private final GitRepositoryManager repoManager;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final AllUsersName allUsers;
+ private final Provider<ReviewDb> dbProvider;
+ private final Provider<PersonIdent> serverIdent;
+ private final ChangeIndexer indexer;
+ private final Provider<InternalChangeQuery> queryProvider;
+
+ @Inject
+ StarredChangesUtil(
+ GitRepositoryManager repoManager,
+ GitReferenceUpdated gitRefUpdated,
+ AllUsersName allUsers,
+ Provider<ReviewDb> dbProvider,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ ChangeIndexer indexer,
+ Provider<InternalChangeQuery> queryProvider) {
+ this.repoManager = repoManager;
+ this.gitRefUpdated = gitRefUpdated;
+ this.allUsers = allUsers;
+ this.dbProvider = dbProvider;
+ this.serverIdent = serverIdent;
+ this.indexer = indexer;
+ this.queryProvider = queryProvider;
+ }
+
+ public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId)
+ throws OrmException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format(
+ "Reading stars from change %d for account %d failed",
+ changeId.get(), accountId.get()),
+ e);
+ }
+ }
+
+ public ImmutableSortedSet<String> star(
+ Account.Id accountId,
+ Project.NameKey project,
+ Change.Id changeId,
+ Set<String> labelsToAdd,
+ Set<String> labelsToRemove)
+ throws OrmException, IllegalLabelException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ String refName = RefNames.refsStarredChanges(changeId, accountId);
+ StarRef old = readLabels(repo, refName);
+
+ Set<String> labels = new HashSet<>(old.labels());
+ if (labelsToAdd != null) {
+ labels.addAll(labelsToAdd);
+ }
+ if (labelsToRemove != null) {
+ labels.removeAll(labelsToRemove);
+ }
+
+ if (labels.isEmpty()) {
+ deleteRef(repo, refName, old.objectId());
+ } else {
+ checkMutuallyExclusiveLabels(labels);
+ updateLabels(repo, refName, old.objectId(), labels);
+ }
+
+ indexer.index(dbProvider.get(), project, changeId);
+ return ImmutableSortedSet.copyOf(labels);
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
+ e);
+ }
+ }
+
+ public void unstarAll(Project.NameKey project, Change.Id changeId) throws OrmException {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.setRefLogIdent(serverIdent.get());
+ batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
+ for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
+ String refName = RefNames.refsStarredChanges(changeId, accountId);
+ Ref ref = repo.getRefDatabase().getRef(refName);
+ batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
+ }
+ batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ for (ReceiveCommand command : batchUpdate.getCommands()) {
+ if (command.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException(
+ String.format(
+ "Unstar change %d failed, ref %s could not be deleted: %s",
+ changeId.get(), command.getRefName(), command.getResult()));
+ }
+ }
+ indexer.index(dbProvider.get(), project, changeId);
+ } catch (IOException e) {
+ throw new OrmException(String.format("Unstar change %d failed", changeId.get()), e);
+ }
+ }
+
+ public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) throws OrmException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
+ for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
+ Integer id = Ints.tryParse(refPart);
+ if (id == null) {
+ continue;
+ }
+ Account.Id accountId = new Account.Id(id);
+ builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
+ }
+ return builder.build();
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format("Get accounts that starred change %d failed", changeId.get()), e);
+ }
+ }
+
+ public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId)
+ throws OrmException {
+ List<ChangeData> changeData =
+ queryProvider
+ .get()
+ .setRequestedFields(ChangeField.ID, ChangeField.STAR)
+ .byLegacyChangeId(changeId);
+ if (changeData.size() != 1) {
+ throw new NoSuchChangeException(changeId);
+ }
+ return changeData.get(0).stars();
+ }
+
+ private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
+ RefDatabase refDb = repo.getRefDatabase();
+ return refDb.getRefsByPrefix(prefix).stream()
+ .map(r -> r.getName().substring(prefix.length()))
+ .collect(toSet());
+ }
+
+ public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
+ return ref != null ? ref.getObjectId() : ObjectId.zeroId();
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log(
+ "Getting star object ID for account %d on change %d failed",
+ accountId.get(), changeId.get());
+ return ObjectId.zeroId();
+ }
+ }
+
+ public void ignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ star(
+ rsrc.getUser().asIdentifiedUser().getAccountId(),
+ rsrc.getProject(),
+ rsrc.getChange().getId(),
+ ImmutableSet.of(IGNORE_LABEL),
+ ImmutableSet.of());
+ }
+
+ public void unignore(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ star(
+ rsrc.getUser().asIdentifiedUser().getAccountId(),
+ rsrc.getProject(),
+ rsrc.getChange().getId(),
+ ImmutableSet.of(),
+ ImmutableSet.of(IGNORE_LABEL));
+ }
+
+ public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) throws OrmException {
+ return getLabels(accountId, changeId).contains(IGNORE_LABEL);
+ }
+
+ public boolean isIgnored(ChangeResource rsrc) throws OrmException {
+ return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
+ }
+
+ private static String getReviewedLabel(Change change) {
+ return getReviewedLabel(change.currentPatchSetId().get());
+ }
+
+ private static String getReviewedLabel(int ps) {
+ return REVIEWED_LABEL + "/" + ps;
+ }
+
+ private static String getUnreviewedLabel(Change change) {
+ return getUnreviewedLabel(change.currentPatchSetId().get());
+ }
+
+ private static String getUnreviewedLabel(int ps) {
+ return UNREVIEWED_LABEL + "/" + ps;
+ }
+
+ public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ star(
+ rsrc.getUser().asIdentifiedUser().getAccountId(),
+ rsrc.getProject(),
+ rsrc.getChange().getId(),
+ ImmutableSet.of(getReviewedLabel(rsrc.getChange())),
+ ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
+ }
+
+ public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ star(
+ rsrc.getUser().asIdentifiedUser().getAccountId(),
+ rsrc.getProject(),
+ rsrc.getChange().getId(),
+ ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())),
+ ImmutableSet.of(getReviewedLabel(rsrc.getChange())));
+ }
+
+ public static StarRef readLabels(Repository repo, String refName) throws IOException {
+ try (TraceTimer traceTimer = TraceContext.newTimer("Read star labels from %s", refName)) {
+ Ref ref = repo.exactRef(refName);
+ if (ref == null) {
+ return StarRef.MISSING;
+ }
+
+ try (ObjectReader reader = repo.newObjectReader()) {
+ ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
+ return StarRef.create(
+ ref,
+ Splitter.on(CharMatcher.whitespace())
+ .omitEmptyStrings()
+ .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
+ }
+ }
+ }
+
+ public static ObjectId writeLabels(Repository repo, Collection<String> labels)
+ throws IOException, InvalidLabelsException {
+ validateLabels(labels);
+ try (ObjectInserter oi = repo.newObjectInserter()) {
+ ObjectId id =
+ oi.insert(
+ Constants.OBJ_BLOB,
+ labels.stream().sorted().distinct().collect(joining("\n")).getBytes(UTF_8));
+ oi.flush();
+ return id;
+ }
+ }
+
+ private static void checkMutuallyExclusiveLabels(Set<String> labels)
+ throws MutuallyExclusiveLabelsException {
+ if (labels.containsAll(ImmutableSet.of(DEFAULT_LABEL, IGNORE_LABEL))) {
+ throw new MutuallyExclusiveLabelsException(DEFAULT_LABEL, IGNORE_LABEL);
+ }
+
+ Set<Integer> reviewedPatchSets = getStarredPatchSets(labels, REVIEWED_LABEL);
+ Set<Integer> unreviewedPatchSets = getStarredPatchSets(labels, UNREVIEWED_LABEL);
+ Optional<Integer> ps =
+ Sets.intersection(reviewedPatchSets, unreviewedPatchSets).stream().findFirst();
+ if (ps.isPresent()) {
+ throw new MutuallyExclusiveLabelsException(
+ getReviewedLabel(ps.get()), getUnreviewedLabel(ps.get()));
+ }
+ }
+
+ public static Set<Integer> getStarredPatchSets(Set<String> labels, String label) {
+ return labels.stream()
+ .filter(l -> l.startsWith(label + "/"))
+ .filter(l -> Ints.tryParse(l.substring(label.length() + 1)) != null)
+ .map(l -> Integer.valueOf(l.substring(label.length() + 1)))
+ .collect(toSet());
+ }
+
+ private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
+ if (labels == null) {
+ return;
+ }
+
+ SortedSet<String> invalidLabels = new TreeSet<>();
+ for (String label : labels) {
+ if (CharMatcher.whitespace().matchesAnyOf(label)) {
+ invalidLabels.add(label);
+ }
+ }
+ if (!invalidLabels.isEmpty()) {
+ throw new InvalidLabelsException(invalidLabels);
+ }
+ }
+
+ private void updateLabels(
+ Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
+ throws IOException, OrmException, InvalidLabelsException {
+ try (TraceTimer traceTimer =
+ TraceContext.newTimer("Update star labels in %s (labels=%s)", refName, labels);
+ RevWalk rw = new RevWalk(repo)) {
+ RefUpdate u = repo.updateRef(refName);
+ u.setExpectedOldObjectId(oldObjectId);
+ u.setForceUpdate(true);
+ u.setNewObjectId(writeLabels(repo, labels));
+ u.setRefLogIdent(serverIdent.get());
+ u.setRefLogMessage("Update star labels", true);
+ RefUpdate.Result result = u.update(rw);
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ gitRefUpdated.fire(allUsers, u, null);
+ return;
+ case IO_FAILURE:
+ case LOCK_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new OrmException(
+ String.format("Update star labels on ref %s failed: %s", refName, result.name()));
+ }
+ }
+ }
+
+ private void deleteRef(Repository repo, String refName, ObjectId oldObjectId)
+ throws IOException, OrmException {
+ if (ObjectId.zeroId().equals(oldObjectId)) {
+ // ref doesn't exist
+ return;
+ }
+
+ try (TraceTimer traceTimer = TraceContext.newTimer("Delete star labels in %s", refName)) {
+ RefUpdate u = repo.updateRef(refName);
+ u.setForceUpdate(true);
+ u.setExpectedOldObjectId(oldObjectId);
+ u.setRefLogIdent(serverIdent.get());
+ u.setRefLogMessage("Unstar change", true);
+ RefUpdate.Result result = u.delete();
+ switch (result) {
+ case FORCED:
+ gitRefUpdated.fire(allUsers, u, null);
+ return;
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case IO_FAILURE:
+ case LOCK_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new OrmException(
+ String.format("Delete star ref %s failed: %s", refName, result.name()));
+ }
+ }
+ }
+}