blob: 8196267950bcea73d5fac52193f7c13750b113ea [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
Dave Borowitzc410f962014-09-23 10:49:26 -070017import static com.google.common.base.MoreObjects.toStringHelper;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static com.google.common.base.Preconditions.checkNotNull;
Dave Borowitzc410f962014-09-23 10:49:26 -070019import static java.util.Objects.hash;
Dave Borowitz9de65952012-08-13 16:09:45 -070020
Dave Borowitzfe8fdab2014-11-04 16:19:33 -080021import com.google.common.annotations.VisibleForTesting;
22import com.google.common.base.CharMatcher;
Dave Borowitzfe8fdab2014-11-04 16:19:33 -080023import com.google.common.base.Splitter;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -080024import com.google.common.base.Strings;
Dave Borowitz3b744b12016-08-19 16:11:10 -040025import java.io.IOException;
26import java.util.Objects;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -080027import java.util.Optional;
28import java.util.regex.Matcher;
29import java.util.regex.Pattern;
Dave Borowitz93c1fca2013-09-25 16:37:43 -070030import org.eclipse.jgit.errors.AmbiguousObjectException;
Dave Borowitz48ca6702013-04-09 11:52:41 -070031import org.eclipse.jgit.errors.MissingObjectException;
Dave Borowitz5f7e8b72013-01-07 09:31:41 -080032import org.eclipse.jgit.errors.RevisionSyntaxException;
Dave Borowitz9de65952012-08-13 16:09:45 -070033import org.eclipse.jgit.lib.ObjectId;
34import org.eclipse.jgit.lib.Repository;
35import org.eclipse.jgit.revwalk.RevCommit;
Dave Borowitz48ca6702013-04-09 11:52:41 -070036import org.eclipse.jgit.revwalk.RevObject;
37import org.eclipse.jgit.revwalk.RevTag;
Dave Borowitz9de65952012-08-13 16:09:45 -070038import org.eclipse.jgit.revwalk.RevWalk;
39
Dave Borowitz9de65952012-08-13 16:09:45 -070040/** Object to parse revisions out of Gitiles paths. */
41class RevisionParser {
Dave Borowitz9de65952012-08-13 16:09:45 -070042 private static final Splitter OPERATOR_SPLITTER = Splitter.on(CharMatcher.anyOf("^~"));
43
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -080044 // The ref name part of a revision expression ends at the first
45 // appearance of ^, ~, :, or @{ (see git-check-ref-format(1)).
46 private static final Pattern END_OF_REF = Pattern.compile("[\\^~:]|@\\{");
47
Dave Borowitz9de65952012-08-13 16:09:45 -070048 static class Result {
49 private final Revision revision;
50 private final Revision oldRevision;
Dave Borowitz06b88d22013-06-19 15:19:14 -070051 private final String path;
Dave Borowitz9de65952012-08-13 16:09:45 -070052
53 @VisibleForTesting
54 Result(Revision revision) {
Dave Borowitz06b88d22013-06-19 15:19:14 -070055 this(revision, null, "");
Dave Borowitz9de65952012-08-13 16:09:45 -070056 }
57
58 @VisibleForTesting
Dave Borowitz06b88d22013-06-19 15:19:14 -070059 Result(Revision revision, Revision oldRevision, String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -070060 this.revision = revision;
61 this.oldRevision = oldRevision;
Dave Borowitz06b88d22013-06-19 15:19:14 -070062 this.path = path;
Dave Borowitz9de65952012-08-13 16:09:45 -070063 }
64
65 public Revision getRevision() {
66 return revision;
67 }
68
69 public Revision getOldRevision() {
70 return oldRevision;
71 }
72
Dave Borowitz06b88d22013-06-19 15:19:14 -070073 public String getPath() {
74 return path;
75 }
76
Dave Borowitz9de65952012-08-13 16:09:45 -070077 @Override
78 public boolean equals(Object o) {
79 if (o instanceof Result) {
80 Result r = (Result) o;
Dave Borowitzc410f962014-09-23 10:49:26 -070081 return Objects.equals(revision, r.revision)
82 && Objects.equals(oldRevision, r.oldRevision)
83 && Objects.equals(path, r.path);
Dave Borowitz9de65952012-08-13 16:09:45 -070084 }
85 return false;
86 }
87
88 @Override
89 public int hashCode() {
Dave Borowitzc410f962014-09-23 10:49:26 -070090 return hash(revision, oldRevision, path);
Dave Borowitz9de65952012-08-13 16:09:45 -070091 }
92
93 @Override
94 public String toString() {
Dave Borowitzc410f962014-09-23 10:49:26 -070095 return toStringHelper(this)
Dave Borowitz9de65952012-08-13 16:09:45 -070096 .omitNullValues()
Dave Borowitz06b88d22013-06-19 15:19:14 -070097 .add("revision", revision.getName())
98 .add("oldRevision", oldRevision != null ? oldRevision.getName() : null)
99 .add("path", path)
Dave Borowitz9de65952012-08-13 16:09:45 -0700100 .toString();
101 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700102 }
103
104 private final Repository repo;
105 private final GitilesAccess access;
106 private final VisibilityCache cache;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800107 private final BranchRedirect branchRedirect;
Dave Borowitz9de65952012-08-13 16:09:45 -0700108
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800109 RevisionParser(
110 Repository repo, GitilesAccess access, VisibilityCache cache, BranchRedirect branchRedirect) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700111 this.repo = checkNotNull(repo, "repo");
112 this.access = checkNotNull(access, "access");
113 this.cache = checkNotNull(cache, "cache");
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800114 this.branchRedirect = checkNotNull(branchRedirect, "branchRedirect");
Dave Borowitz9de65952012-08-13 16:09:45 -0700115 }
116
117 Result parse(String path) throws IOException {
Dave Borowitz06b88d22013-06-19 15:19:14 -0700118 if (path.startsWith("/")) {
119 path = path.substring(1);
120 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800121 if (Strings.isNullOrEmpty(path)) {
122 return null;
123 }
Shawn Pearceb5ad0a02015-05-24 20:33:17 -0700124 try (RevWalk walk = new RevWalk(repo)) {
Jonathan Niederc63c0592019-03-07 14:40:49 -0800125 walk.setRetainBody(false);
126
Dave Borowitz9de65952012-08-13 16:09:45 -0700127 Revision oldRevision = null;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800128 Revision oldRevisionRedirected = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700129
130 StringBuilder b = new StringBuilder();
131 boolean first = true;
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800132 for (String part : PathUtil.SPLITTER.split(path)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700133 if (part.isEmpty()) {
134 return null; // No valid revision contains empty segments.
135 }
136 if (!first) {
137 b.append('/');
138 }
139
140 if (oldRevision == null) {
141 int dots = part.indexOf("..");
142 int firstParent = part.indexOf("^!");
143 if (dots == 0 || firstParent == 0) {
144 return null;
145 } else if (dots > 0) {
Dave Borowitz27058932014-12-03 15:44:46 -0800146 b.append(part, 0, dots);
Dave Borowitz9de65952012-08-13 16:09:45 -0700147 String oldName = b.toString();
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800148 String oldNameRedirect = getRedirectFor(oldName);
149
150 if (!isValidRevision(oldNameRedirect)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700151 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700152 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800153 RevObject old = resolve(oldNameRedirect, walk);
David Pursehouseb3b630f2016-06-15 21:51:18 +0900154 if (old == null) {
155 return null;
156 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800157 /*
158 * Retain oldRevision with the old name (non-redirected-path) since it is used in
159 * determining the Revision path (start index of the path from the name).
160 * For example: For a master -> main redirect,
161 * original path: /master/index.c is updated to /main/index.c
162 * To parse the ref/path to build Revision object we look at the original path.
163 */
David Pursehouseb3b630f2016-06-15 21:51:18 +0900164 oldRevision = Revision.peel(oldName, old, walk);
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800165 oldRevisionRedirected = Revision.peel(oldNameRedirect, old, walk);
Dave Borowitz9de65952012-08-13 16:09:45 -0700166 part = part.substring(dots + 2);
167 b = new StringBuilder();
168 } else if (firstParent > 0) {
169 if (firstParent != part.length() - 2) {
170 return null;
171 }
Dave Borowitz27058932014-12-03 15:44:46 -0800172 b.append(part, 0, part.length() - 2);
Dave Borowitz9de65952012-08-13 16:09:45 -0700173 String name = b.toString();
174 if (!isValidRevision(name)) {
175 return null;
176 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800177
178 String nameRedirected = getRedirectFor(name);
179 RevObject obj = resolve(nameRedirected, walk);
Dave Borowitz48ca6702013-04-09 11:52:41 -0700180 if (obj == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700181 return null;
182 }
Dave Borowitz48ca6702013-04-09 11:52:41 -0700183 while (obj instanceof RevTag) {
184 obj = ((RevTag) obj).getObject();
185 walk.parseHeaders(obj);
186 }
187 if (!(obj instanceof RevCommit)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700188 return null; // Not a commit, ^! is invalid.
189 }
Dave Borowitz48ca6702013-04-09 11:52:41 -0700190 RevCommit c = (RevCommit) obj;
Dave Borowitz9de65952012-08-13 16:09:45 -0700191 if (c.getParentCount() > 0) {
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800192 oldRevisionRedirected = Revision.peeled(nameRedirected + "^", c.getParent(0));
Dave Borowitz9de65952012-08-13 16:09:45 -0700193 } else {
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800194 oldRevisionRedirected = Revision.NULL;
Dave Borowitz9de65952012-08-13 16:09:45 -0700195 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200196 Result result =
197 new Result(
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800198 Revision.peeled(nameRedirected, c),
199 oldRevisionRedirected,
200 path.substring(name.length() + 2));
Dave Borowitz9de65952012-08-13 16:09:45 -0700201 return isVisible(walk, result) ? result : null;
202 }
203 }
204 b.append(part);
205
206 String name = b.toString();
207 if (!isValidRevision(name)) {
208 return null;
209 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800210 String nameRedirected = getRedirectFor(name);
211
212 RevObject obj = resolve(nameRedirected, walk);
Dave Borowitz48ca6702013-04-09 11:52:41 -0700213 if (obj != null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700214 int pathStart;
215 if (oldRevision == null) {
216 pathStart = name.length(); // foo
217 } else {
218 // foo..bar (foo may be empty)
219 pathStart = oldRevision.getName().length() + 2 + name.length();
220 }
Dave Borowitze360d5c2015-09-16 16:53:30 -0400221 Result result =
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800222 new Result(
223 Revision.peel(nameRedirected, obj, walk),
224 oldRevisionRedirected,
225 path.substring(pathStart));
Dave Borowitz9de65952012-08-13 16:09:45 -0700226 return isVisible(walk, result) ? result : null;
227 }
228 first = false;
229 }
230 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700231 }
232 }
233
Dave Borowitz48ca6702013-04-09 11:52:41 -0700234 private RevObject resolve(String name, RevWalk walk) throws IOException {
Dave Borowitz5f7e8b72013-01-07 09:31:41 -0800235 try {
Dave Borowitz48ca6702013-04-09 11:52:41 -0700236 ObjectId id = repo.resolve(name);
237 return id != null ? walk.parseAny(id) : null;
Dave Borowitz93c1fca2013-09-25 16:37:43 -0700238 } catch (AmbiguousObjectException e) {
239 // TODO(dborowitz): Render a helpful disambiguation page.
240 return null;
Dave Borowitz27058932014-12-03 15:44:46 -0800241 } catch (RevisionSyntaxException | MissingObjectException e) {
Dave Borowitz48ca6702013-04-09 11:52:41 -0700242 return null;
Dave Borowitz5f7e8b72013-01-07 09:31:41 -0800243 }
244 }
245
Dave Borowitz9de65952012-08-13 16:09:45 -0700246 private static boolean isValidRevision(String revision) {
247 // Disallow some uncommon but valid revision expressions that either we
248 // don't support or we represent differently in our URLs.
Jonathan Nieder4a7dac02019-07-01 16:15:03 -0700249 return !revision.contains(":")
250 && !revision.contains("^{")
251 && !revision.contains("@{")
252 && !revision.equals("@");
Dave Borowitz9de65952012-08-13 16:09:45 -0700253 }
254
255 private boolean isVisible(RevWalk walk, Result result) throws IOException {
256 String maybeRef = OPERATOR_SPLITTER.split(result.getRevision().getName()).iterator().next();
Dave Borowitz14cad732016-05-26 17:34:19 -0400257 if (repo.findRef(maybeRef) != null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700258 // Name contains a visible ref; skip expensive reachability check.
259 return true;
260 }
Dave Borowitzdf363d22013-06-10 11:18:04 -0700261 ObjectId id = result.getRevision().getId();
262 if (!cache.isVisible(repo, walk, access, id)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700263 return false;
264 }
David Pursehousec53de2b2019-06-01 15:27:25 +0900265 if (result.getOldRevision() != null && !Revision.isNull(result.getOldRevision())) {
Dave Borowitzdf363d22013-06-10 11:18:04 -0700266 return cache.isVisible(repo, walk, access, result.getOldRevision().getId(), id);
Dave Borowitz9de65952012-08-13 16:09:45 -0700267 }
David Pursehouseb3b630f2016-06-15 21:51:18 +0900268 return true;
Dave Borowitz9de65952012-08-13 16:09:45 -0700269 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800270
271 /**
272 * It replaces the ref in the revision expression to the redirected refName, without changing the
273 * behavior of the expression.
274 *
275 * <p>For eg: branch redirect {master -> main} would yield {master -> main}, {refs/heads/master^
276 * -> refs/heads/main^}, {refs/heads/master^ -> refs/heads/main^}. It does expand to a full
277 * refName even for shorter refNames.
278 */
279 private String getRedirectFor(String revisionExpression) {
280 String refName = refPart(revisionExpression);
281 Optional<String> redirect = branchRedirect.getRedirectBranch(repo, refName);
282 if (redirect.isPresent()) {
283 return redirect.get() + revisionExpression.substring(refName.length());
284 }
285 return revisionExpression;
286 }
287
288 private static String refPart(String revisionExpression) {
289 Matcher m = END_OF_REF.matcher(revisionExpression);
290 if (!m.find()) { // no terminator -> the whole string is a ref name.
291 return revisionExpression;
292 }
293 return revisionExpression.substring(0, m.start());
294 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700295}