blob: 8196267950bcea73d5fac52193f7c13750b113ea [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 com.google.common.base.Strings;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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("^~"));
// The ref name part of a revision expression ends at the first
// appearance of ^, ~, :, or @{ (see git-check-ref-format(1)).
private static final Pattern END_OF_REF = Pattern.compile("[\\^~:]|@\\{");
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;
private final BranchRedirect branchRedirect;
RevisionParser(
Repository repo, GitilesAccess access, VisibilityCache cache, BranchRedirect branchRedirect) {
this.repo = checkNotNull(repo, "repo");
this.access = checkNotNull(access, "access");
this.cache = checkNotNull(cache, "cache");
this.branchRedirect = checkNotNull(branchRedirect, "branchRedirect");
}
Result parse(String path) throws IOException {
if (path.startsWith("/")) {
path = path.substring(1);
}
if (Strings.isNullOrEmpty(path)) {
return null;
}
try (RevWalk walk = new RevWalk(repo)) {
walk.setRetainBody(false);
Revision oldRevision = null;
Revision oldRevisionRedirected = 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();
String oldNameRedirect = getRedirectFor(oldName);
if (!isValidRevision(oldNameRedirect)) {
return null;
}
RevObject old = resolve(oldNameRedirect, walk);
if (old == null) {
return null;
}
/*
* Retain oldRevision with the old name (non-redirected-path) since it is used in
* determining the Revision path (start index of the path from the name).
* For example: For a master -> main redirect,
* original path: /master/index.c is updated to /main/index.c
* To parse the ref/path to build Revision object we look at the original path.
*/
oldRevision = Revision.peel(oldName, old, walk);
oldRevisionRedirected = Revision.peel(oldNameRedirect, 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;
}
String nameRedirected = getRedirectFor(name);
RevObject obj = resolve(nameRedirected, 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) {
oldRevisionRedirected = Revision.peeled(nameRedirected + "^", c.getParent(0));
} else {
oldRevisionRedirected = Revision.NULL;
}
Result result =
new Result(
Revision.peeled(nameRedirected, c),
oldRevisionRedirected,
path.substring(name.length() + 2));
return isVisible(walk, result) ? result : null;
}
}
b.append(part);
String name = b.toString();
if (!isValidRevision(name)) {
return null;
}
String nameRedirected = getRedirectFor(name);
RevObject obj = resolve(nameRedirected, 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(nameRedirected, obj, walk),
oldRevisionRedirected,
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("@{")
&& !revision.equals("@");
}
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 && !Revision.isNull(result.getOldRevision())) {
return cache.isVisible(repo, walk, access, result.getOldRevision().getId(), id);
}
return true;
}
/**
* It replaces the ref in the revision expression to the redirected refName, without changing the
* behavior of the expression.
*
* <p>For eg: branch redirect {master -> main} would yield {master -> main}, {refs/heads/master^
* -> refs/heads/main^}, {refs/heads/master^ -> refs/heads/main^}. It does expand to a full
* refName even for shorter refNames.
*/
private String getRedirectFor(String revisionExpression) {
String refName = refPart(revisionExpression);
Optional<String> redirect = branchRedirect.getRedirectBranch(repo, refName);
if (redirect.isPresent()) {
return redirect.get() + revisionExpression.substring(refName.length());
}
return revisionExpression;
}
private static String refPart(String revisionExpression) {
Matcher m = END_OF_REF.matcher(revisionExpression);
if (!m.find()) { // no terminator -> the whole string is a ref name.
return revisionExpression;
}
return revisionExpression.substring(0, m.start());
}
}