| // 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; |
| } |
| } |