blob: 2d1805451f7d8aa8e26fee890558a4128ec29a61 [file] [log] [blame]
// 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 com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
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.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.GitUpdateFailureException;
import com.google.gerrit.git.LockFailureException;
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.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.update.context.RefUpdateContext;
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.Collections;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
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 = ":";
@Nullable
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 = 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 final String toString() {
return accountId() + SEPARATOR + label();
}
}
public enum Operation {
ADD,
REMOVE
}
@AutoValue
public abstract static class StarRef {
private static final StarRef MISSING =
new AutoValue_StarredChangesUtil_StarRef(null, Collections.emptyNavigableSet());
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 NavigableSet<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";
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsers;
private final Provider<PersonIdent> serverIdent;
@Inject
StarredChangesUtil(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
AllUsersName allUsers,
@GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.repoManager = repoManager;
this.gitRefUpdated = gitRefUpdated;
this.allUsers = allUsers;
this.serverIdent = serverIdent;
}
public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
} catch (IOException e) {
throw new StorageException(
String.format(
"Reading stars from change %d for account %d failed",
changeId.get(), accountId.get()),
e);
}
}
public void star(Account.Id accountId, Change.Id changeId, Operation op)
throws IllegalLabelException {
try (Repository repo = repoManager.openRepository(allUsers)) {
String refName = RefNames.refsStarredChanges(changeId, accountId);
StarRef old = readLabels(repo, refName);
NavigableSet<String> labels = new TreeSet<>(old.labels());
switch (op) {
case ADD:
labels.add(DEFAULT_LABEL);
break;
case REMOVE:
labels.remove(DEFAULT_LABEL);
break;
}
if (labels.isEmpty()) {
deleteRef(repo, refName, old.objectId());
} else {
updateLabels(repo, refName, old.objectId(), labels);
}
} catch (IOException e) {
throw new StorageException(
String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
e);
}
}
/**
* Unstar the given change for all users.
*
* <p>Intended for use only when we're about to delete a change. For that reason, the change is
* not reindexed.
*
* @param changeId change ID.
* @throws IOException if an error occurred.
*/
public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
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 : getStars(repo, changeId)) {
String refName = RefNames.refsStarredChanges(changeId, accountId);
Ref ref = repo.getRefDatabase().exactRef(refName);
if (ref != null) {
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) {
String message =
String.format(
"Unstar change %d failed, ref %s could not be deleted: %s",
changeId.get(), command.getRefName(), command.getResult());
if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
throw new LockFailureException(message, batchUpdate);
}
throw new GitUpdateFailureException(message, batchUpdate);
}
}
}
}
public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
for (Account.Id accountId : getStars(repo, changeId)) {
builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
}
return builder.build();
} catch (IOException e) {
throw new StorageException(
String.format("Get accounts that starred change %d failed", changeId.get()), e);
}
}
public ImmutableSet<Change.Id> byAccountId(Account.Id accountId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
// Skip all refs that don't correspond with accountId.
if (currentAccountId == null || !currentAccountId.equals(accountId)) {
continue;
}
// Skip all refs that don't contain the required label.
StarRef starRef = readLabels(repo, ref.getName());
if (!starRef.labels().contains(DEFAULT_LABEL)) {
continue;
}
// Skip invalid change ids.
Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
if (changeId == null) {
continue;
}
builder.add(changeId);
}
return builder.build();
} catch (IOException e) {
throw new StorageException(
String.format("Get starred changes for account %d failed", accountId.get()), e);
}
}
private static Set<Account.Id> getStars(Repository allUsers, Change.Id changeId)
throws IOException {
String prefix = RefNames.refsStarredChangesPrefix(changeId);
RefDatabase refDb = allUsers.getRefDatabase();
return refDb.getRefsByPrefix(prefix).stream()
.map(r -> r.getName().substring(prefix.length()))
.map(refPart -> Ints.tryParse(refPart))
.filter(Objects::nonNull)
.map(id -> Account.id(id))
.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 static StarRef readLabels(Repository repo, String refName) throws IOException {
try (TraceTimer traceTimer =
TraceContext.newTimer(
"Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
Ref ref = repo.exactRef(refName);
return readLabels(repo, ref);
}
}
public static StarRef readLabels(Repository repo, Ref ref) throws IOException {
if (ref == null) {
return StarRef.MISSING;
}
try (TraceTimer traceTimer =
TraceContext.newTimer(
String.format("Read star labels from %s (without ref lookup)", ref.getName()));
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 validateLabels(Collection<String> labels) throws InvalidLabelsException {
if (labels == null) {
return;
}
NavigableSet<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, InvalidLabelsException {
try (TraceTimer traceTimer =
TraceContext.newTimer(
"Update star labels",
Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
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);
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
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 LOCK_FAILURE:
throw new LockFailureException(
String.format("Update star labels on ref %s failed", refName), u);
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
throw new StorageException(
String.format("Update star labels on ref %s failed: %s", refName, result.name()));
}
}
}
}
private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
if (ObjectId.zeroId().equals(oldObjectId)) {
// ref doesn't exist
return;
}
try (TraceTimer traceTimer =
TraceContext.newTimer(
"Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
RefUpdate u = repo.updateRef(refName);
u.setForceUpdate(true);
u.setExpectedOldObjectId(oldObjectId);
u.setRefLogIdent(serverIdent.get());
u.setRefLogMessage("Unstar change", true);
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
RefUpdate.Result result = u.delete();
switch (result) {
case FORCED:
gitRefUpdated.fire(allUsers, u, null);
return;
case LOCK_FAILURE:
throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
case NEW:
case NO_CHANGE:
case FAST_FORWARD:
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
throw new StorageException(
String.format("Delete star ref %s failed: %s", refName, result.name()));
}
}
}
}
}