blob: 89311a354a2d2d891f73e4f1a7eb18da8a92dda9 [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 java.util.Objects.hash;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import java.io.IOException;
import java.util.Objects;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
/** Object to parse revisions out of Gitiles paths. */
class RevisionParser {
private static final Splitter OPERATOR_SPLITTER = Splitter.on(CharMatcher.anyOf("^~"));
static class Result {
private final Revision revision;
private final Revision oldRevision;
private final String path;
@VisibleForTesting
Result(Revision revision) {
this(revision, null, "");
}
@VisibleForTesting
Result(Revision revision, Revision oldRevision, String path) {
this.revision = revision;
this.oldRevision = oldRevision;
this.path = path;
}
public Revision getRevision() {
return revision;
}
public Revision getOldRevision() {
return oldRevision;
}
public String getPath() {
return path;
}
@Override
public boolean equals(Object o) {
if (o instanceof Result) {
Result r = (Result) o;
return Objects.equals(revision, r.revision)
&& Objects.equals(oldRevision, r.oldRevision)
&& Objects.equals(path, r.path);
}
return false;
}
@Override
public int hashCode() {
return hash(revision, oldRevision, path);
}
@Override
public String toString() {
return toStringHelper(this)
.omitNullValues()
.add("revision", revision.getName())
.add("oldRevision", oldRevision != null ? oldRevision.getName() : null)
.add("path", path)
.toString();
}
}
private final Repository repo;
private final GitilesAccess access;
private final VisibilityCache cache;
RevisionParser(Repository repo, GitilesAccess access, VisibilityCache cache) {
this.repo = checkNotNull(repo, "repo");
this.access = checkNotNull(access, "access");
this.cache = checkNotNull(cache, "cache");
}
Result parse(String path) throws IOException {
if (path.startsWith("/")) {
path = path.substring(1);
}
try (RevWalk walk = new RevWalk(repo)) {
walk.setRetainBody(false);
Revision oldRevision = null;
StringBuilder b = new StringBuilder();
boolean first = true;
for (String part : PathUtil.SPLITTER.split(path)) {
if (part.isEmpty()) {
return null; // No valid revision contains empty segments.
}
if (!first) {
b.append('/');
}
if (oldRevision == null) {
int dots = part.indexOf("..");
int firstParent = part.indexOf("^!");
if (dots == 0 || firstParent == 0) {
return null;
} else if (dots > 0) {
b.append(part, 0, dots);
String oldName = b.toString();
if (!isValidRevision(oldName)) {
return null;
}
RevObject old = resolve(oldName, walk);
if (old == null) {
return null;
}
oldRevision = Revision.peel(oldName, old, walk);
part = part.substring(dots + 2);
b = new StringBuilder();
} else if (firstParent > 0) {
if (firstParent != part.length() - 2) {
return null;
}
b.append(part, 0, part.length() - 2);
String name = b.toString();
if (!isValidRevision(name)) {
return null;
}
RevObject obj = resolve(name, walk);
if (obj == null) {
return null;
}
while (obj instanceof RevTag) {
obj = ((RevTag) obj).getObject();
walk.parseHeaders(obj);
}
if (!(obj instanceof RevCommit)) {
return null; // Not a commit, ^! is invalid.
}
RevCommit c = (RevCommit) obj;
if (c.getParentCount() > 0) {
oldRevision = Revision.peeled(name + "^", c.getParent(0));
} else {
oldRevision = Revision.NULL;
}
Result result =
new Result(
Revision.peeled(name, c), oldRevision, path.substring(name.length() + 2));
return isVisible(walk, result) ? result : null;
}
}
b.append(part);
String name = b.toString();
if (!isValidRevision(name)) {
return null;
}
RevObject obj = resolve(name, walk);
if (obj != null) {
int pathStart;
if (oldRevision == null) {
pathStart = name.length(); // foo
} else {
// foo..bar (foo may be empty)
pathStart = oldRevision.getName().length() + 2 + name.length();
}
Result result =
new Result(Revision.peel(name, obj, walk), oldRevision, path.substring(pathStart));
return isVisible(walk, result) ? result : null;
}
first = false;
}
return null;
}
}
private RevObject resolve(String name, RevWalk walk) throws IOException {
try {
ObjectId id = repo.resolve(name);
return id != null ? walk.parseAny(id) : null;
} catch (AmbiguousObjectException e) {
// TODO(dborowitz): Render a helpful disambiguation page.
return null;
} catch (RevisionSyntaxException | MissingObjectException e) {
return null;
}
}
private static boolean isValidRevision(String revision) {
// Disallow some uncommon but valid revision expressions that either we
// don't support or we represent differently in our URLs.
return !revision.contains(":") && !revision.contains("^{") && !revision.contains("@");
}
private boolean isVisible(RevWalk walk, Result result) throws IOException {
String maybeRef = OPERATOR_SPLITTER.split(result.getRevision().getName()).iterator().next();
if (repo.findRef(maybeRef) != null) {
// Name contains a visible ref; skip expensive reachability check.
return true;
}
ObjectId id = result.getRevision().getId();
if (!cache.isVisible(repo, walk, access, id)) {
return false;
}
if (result.getOldRevision() != null && result.getOldRevision() != Revision.NULL) {
return cache.isVisible(repo, walk, access, result.getOldRevision().getId(), id);
}
return true;
}
}