blob: 2c0385da9c9c5dcb89777b8daa82da833c45482b [file] [log] [blame]
// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.gitiles;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Collections2.filter;
import static java.util.Objects.hash;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Collections2;
import com.google.common.util.concurrent.ExecutionError;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/** Cache of per-user object visibility. */
public class VisibilityCache {
private static class Key {
private final Object user;
private final String repositoryName;
private final ObjectId objectId;
private Key(Object user, String repositoryName, ObjectId objectId) {
this.user = checkNotNull(user, "user");
this.repositoryName = checkNotNull(repositoryName, "repositoryName");
this.objectId = checkNotNull(objectId, "objectId").copy();
}
@Override
public boolean equals(Object o) {
if (o instanceof Key) {
Key k = (Key) o;
return Objects.equals(user, k.user)
&& Objects.equals(repositoryName, k.repositoryName)
&& Objects.equals(objectId, k.objectId);
}
return false;
}
@Override
public int hashCode() {
return hash(user, repositoryName, objectId);
}
@Override
public String toString() {
return toStringHelper(this)
.add("user", user)
.add("repositoryName", repositoryName)
.add("objectId", objectId)
.toString();
}
}
private final Cache<Key, Boolean> cache;
private final boolean topoSort;
public static CacheBuilder<Object, Object> defaultBuilder() {
return CacheBuilder.newBuilder().maximumSize(1 << 10).expireAfterWrite(30, TimeUnit.MINUTES);
}
public VisibilityCache(boolean topoSort) {
this(topoSort, defaultBuilder());
}
public VisibilityCache(boolean topoSort, CacheBuilder<Object, Object> builder) {
this.cache = builder.build();
this.topoSort = topoSort;
}
public Cache<?, Boolean> getCache() {
return cache;
}
boolean isVisible(
final Repository repo,
final RevWalk walk,
GitilesAccess access,
final ObjectId id,
final ObjectId... knownReachable)
throws IOException {
try {
return cache.get(
new Key(access.getUserKey(), access.getRepositoryName(), id),
new Callable<Boolean>() {
@Override
public Boolean call() throws IOException {
return isVisible(repo, walk, id, Arrays.asList(knownReachable));
}
});
} catch (ExecutionException e) {
Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
throw new IOException(e);
} catch (ExecutionError e) {
// markUninteresting may overflow on pathological repos with very long
// merge chains. Play it safe and return false rather than letting the
// error propagate.
if (e.getCause() instanceof StackOverflowError) {
return false;
}
throw e;
}
}
private boolean isVisible(
Repository repo, RevWalk walk, ObjectId id, Collection<ObjectId> knownReachable)
throws IOException {
RevCommit commit;
try {
commit = walk.parseCommit(id);
} catch (IncorrectObjectTypeException e) {
return false;
}
// If any reference directly points at the requested object, permit display.
// Common for displays of pending patch sets in Gerrit Code Review, or
// bookmarks to the commit a tag points at.
Collection<Ref> allRefs = repo.getRefDatabase().getRefs(RefDatabase.ALL).values();
for (Ref ref : allRefs) {
ref = repo.getRefDatabase().peel(ref);
if (id.equals(ref.getObjectId()) || id.equals(ref.getPeeledObjectId())) {
return true;
}
}
// Check heads first under the assumption that most requests are for refs
// close to a head. Tags tend to be much further back in history and just
// clutter up the priority queue in the common case.
return isReachableFrom(walk, commit, knownReachable)
|| isReachableFromRefs(walk, commit, filter(allRefs, refStartsWith(R_HEADS)))
|| isReachableFromRefs(walk, commit, filter(allRefs, refStartsWith(R_TAGS)))
|| isReachableFromRefs(walk, commit, filter(allRefs, otherRefs()));
}
private static Predicate<Ref> refStartsWith(final String prefix) {
return new Predicate<Ref>() {
@Override
public boolean apply(Ref ref) {
return ref.getName().startsWith(prefix);
}
};
}
@SuppressWarnings("unchecked")
private static Predicate<Ref> otherRefs() {
return not(
Predicates.<Ref>or(
refStartsWith(R_HEADS), refStartsWith(R_TAGS), refStartsWith("refs/changes/")));
}
private boolean isReachableFromRefs(RevWalk walk, RevCommit commit, Collection<Ref> refs)
throws IOException {
return isReachableFrom(
walk,
commit,
Collections2.transform(
refs,
new Function<Ref, ObjectId>() {
@Override
public ObjectId apply(Ref ref) {
if (ref.getPeeledObjectId() != null) {
return ref.getPeeledObjectId();
} else {
return ref.getObjectId();
}
}
}));
}
private boolean isReachableFrom(RevWalk walk, RevCommit commit, Collection<ObjectId> ids)
throws IOException {
if (ids.isEmpty()) {
return false;
}
walk.reset();
if (topoSort) {
walk.sort(RevSort.TOPO);
}
walk.markStart(commit);
for (ObjectId id : ids) {
markUninteresting(walk, id);
}
// If the commit is reachable from any given tip, it will appear to be
// uninteresting to the RevWalk and no output will be produced.
return walk.next() == null;
}
private static void markUninteresting(RevWalk walk, ObjectId id) throws IOException {
if (id == null) {
return;
}
try {
walk.markUninteresting(walk.parseCommit(id));
} catch (IncorrectObjectTypeException | MissingObjectException e) {
// Do nothing, doesn't affect reachability.
}
}
}