blob: 7fbf86c8fca7d31e01377af38f2e55e25785f5f3 [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;
Matthias Sohnc156c962023-09-30 22:15:23 +020030import javax.annotation.Nullable;
Dave Borowitz93c1fca2013-09-25 16:37:43 -070031import org.eclipse.jgit.errors.AmbiguousObjectException;
Dave Borowitz48ca6702013-04-09 11:52:41 -070032import org.eclipse.jgit.errors.MissingObjectException;
Dave Borowitz5f7e8b72013-01-07 09:31:41 -080033import org.eclipse.jgit.errors.RevisionSyntaxException;
Dave Borowitz9de65952012-08-13 16:09:45 -070034import org.eclipse.jgit.lib.ObjectId;
35import org.eclipse.jgit.lib.Repository;
36import org.eclipse.jgit.revwalk.RevCommit;
Dave Borowitz48ca6702013-04-09 11:52:41 -070037import org.eclipse.jgit.revwalk.RevObject;
38import org.eclipse.jgit.revwalk.RevTag;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import org.eclipse.jgit.revwalk.RevWalk;
40
Dave Borowitz9de65952012-08-13 16:09:45 -070041/** Object to parse revisions out of Gitiles paths. */
42class RevisionParser {
Dave Borowitz9de65952012-08-13 16:09:45 -070043 private static final Splitter OPERATOR_SPLITTER = Splitter.on(CharMatcher.anyOf("^~"));
44
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -080045 // The ref name part of a revision expression ends at the first
46 // appearance of ^, ~, :, or @{ (see git-check-ref-format(1)).
47 private static final Pattern END_OF_REF = Pattern.compile("[\\^~:]|@\\{");
48
Dave Borowitz9de65952012-08-13 16:09:45 -070049 static class Result {
50 private final Revision revision;
51 private final Revision oldRevision;
Dave Borowitz06b88d22013-06-19 15:19:14 -070052 private final String path;
Dave Borowitz9de65952012-08-13 16:09:45 -070053
54 @VisibleForTesting
55 Result(Revision revision) {
Dave Borowitz06b88d22013-06-19 15:19:14 -070056 this(revision, null, "");
Dave Borowitz9de65952012-08-13 16:09:45 -070057 }
58
59 @VisibleForTesting
Dave Borowitz06b88d22013-06-19 15:19:14 -070060 Result(Revision revision, Revision oldRevision, String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -070061 this.revision = revision;
62 this.oldRevision = oldRevision;
Dave Borowitz06b88d22013-06-19 15:19:14 -070063 this.path = path;
Dave Borowitz9de65952012-08-13 16:09:45 -070064 }
65
66 public Revision getRevision() {
67 return revision;
68 }
69
70 public Revision getOldRevision() {
71 return oldRevision;
72 }
73
Dave Borowitz06b88d22013-06-19 15:19:14 -070074 public String getPath() {
75 return path;
76 }
77
Dave Borowitz9de65952012-08-13 16:09:45 -070078 @Override
79 public boolean equals(Object o) {
80 if (o instanceof Result) {
81 Result r = (Result) o;
Dave Borowitzc410f962014-09-23 10:49:26 -070082 return Objects.equals(revision, r.revision)
83 && Objects.equals(oldRevision, r.oldRevision)
84 && Objects.equals(path, r.path);
Dave Borowitz9de65952012-08-13 16:09:45 -070085 }
86 return false;
87 }
88
89 @Override
90 public int hashCode() {
Dave Borowitzc410f962014-09-23 10:49:26 -070091 return hash(revision, oldRevision, path);
Dave Borowitz9de65952012-08-13 16:09:45 -070092 }
93
94 @Override
95 public String toString() {
Dave Borowitzc410f962014-09-23 10:49:26 -070096 return toStringHelper(this)
Dave Borowitz9de65952012-08-13 16:09:45 -070097 .omitNullValues()
Dave Borowitz06b88d22013-06-19 15:19:14 -070098 .add("revision", revision.getName())
99 .add("oldRevision", oldRevision != null ? oldRevision.getName() : null)
100 .add("path", path)
Dave Borowitz9de65952012-08-13 16:09:45 -0700101 .toString();
102 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700103 }
104
105 private final Repository repo;
106 private final GitilesAccess access;
107 private final VisibilityCache cache;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800108 private final BranchRedirect branchRedirect;
Dave Borowitz9de65952012-08-13 16:09:45 -0700109
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800110 RevisionParser(
111 Repository repo, GitilesAccess access, VisibilityCache cache, BranchRedirect branchRedirect) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700112 this.repo = checkNotNull(repo, "repo");
113 this.access = checkNotNull(access, "access");
114 this.cache = checkNotNull(cache, "cache");
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800115 this.branchRedirect = checkNotNull(branchRedirect, "branchRedirect");
Dave Borowitz9de65952012-08-13 16:09:45 -0700116 }
117
Matthias Sohnc156c962023-09-30 22:15:23 +0200118 @Nullable
Dave Borowitz9de65952012-08-13 16:09:45 -0700119 Result parse(String path) throws IOException {
Dave Borowitz06b88d22013-06-19 15:19:14 -0700120 if (path.startsWith("/")) {
121 path = path.substring(1);
122 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800123 if (Strings.isNullOrEmpty(path)) {
124 return null;
125 }
Shawn Pearceb5ad0a02015-05-24 20:33:17 -0700126 try (RevWalk walk = new RevWalk(repo)) {
Jonathan Niederc63c0592019-03-07 14:40:49 -0800127 walk.setRetainBody(false);
128
Dave Borowitz9de65952012-08-13 16:09:45 -0700129 Revision oldRevision = null;
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800130 Revision oldRevisionRedirected = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700131
132 StringBuilder b = new StringBuilder();
133 boolean first = true;
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800134 for (String part : PathUtil.SPLITTER.split(path)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700135 if (part.isEmpty()) {
136 return null; // No valid revision contains empty segments.
137 }
138 if (!first) {
139 b.append('/');
140 }
141
142 if (oldRevision == null) {
143 int dots = part.indexOf("..");
144 int firstParent = part.indexOf("^!");
145 if (dots == 0 || firstParent == 0) {
146 return null;
147 } else if (dots > 0) {
Dave Borowitz27058932014-12-03 15:44:46 -0800148 b.append(part, 0, dots);
Dave Borowitz9de65952012-08-13 16:09:45 -0700149 String oldName = b.toString();
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800150 String oldNameRedirect = getRedirectFor(oldName);
151
152 if (!isValidRevision(oldNameRedirect)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700153 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700154 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800155 RevObject old = resolve(oldNameRedirect, walk);
David Pursehouseb3b630f2016-06-15 21:51:18 +0900156 if (old == null) {
157 return null;
158 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800159 /*
160 * Retain oldRevision with the old name (non-redirected-path) since it is used in
161 * determining the Revision path (start index of the path from the name).
162 * For example: For a master -> main redirect,
163 * original path: /master/index.c is updated to /main/index.c
164 * To parse the ref/path to build Revision object we look at the original path.
165 */
David Pursehouseb3b630f2016-06-15 21:51:18 +0900166 oldRevision = Revision.peel(oldName, old, walk);
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800167 oldRevisionRedirected = Revision.peel(oldNameRedirect, old, walk);
Dave Borowitz9de65952012-08-13 16:09:45 -0700168 part = part.substring(dots + 2);
169 b = new StringBuilder();
170 } else if (firstParent > 0) {
171 if (firstParent != part.length() - 2) {
172 return null;
173 }
Dave Borowitz27058932014-12-03 15:44:46 -0800174 b.append(part, 0, part.length() - 2);
Dave Borowitz9de65952012-08-13 16:09:45 -0700175 String name = b.toString();
176 if (!isValidRevision(name)) {
177 return null;
178 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800179
180 String nameRedirected = getRedirectFor(name);
181 RevObject obj = resolve(nameRedirected, walk);
Dave Borowitz48ca6702013-04-09 11:52:41 -0700182 if (obj == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700183 return null;
184 }
Dave Borowitz48ca6702013-04-09 11:52:41 -0700185 while (obj instanceof RevTag) {
186 obj = ((RevTag) obj).getObject();
187 walk.parseHeaders(obj);
188 }
189 if (!(obj instanceof RevCommit)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700190 return null; // Not a commit, ^! is invalid.
191 }
Dave Borowitz48ca6702013-04-09 11:52:41 -0700192 RevCommit c = (RevCommit) obj;
Dave Borowitz9de65952012-08-13 16:09:45 -0700193 if (c.getParentCount() > 0) {
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800194 oldRevisionRedirected = Revision.peeled(nameRedirected + "^", c.getParent(0));
Dave Borowitz9de65952012-08-13 16:09:45 -0700195 } else {
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800196 oldRevisionRedirected = Revision.NULL;
Dave Borowitz9de65952012-08-13 16:09:45 -0700197 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200198 Result result =
199 new Result(
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800200 Revision.peeled(nameRedirected, c),
201 oldRevisionRedirected,
202 path.substring(name.length() + 2));
Dave Borowitz9de65952012-08-13 16:09:45 -0700203 return isVisible(walk, result) ? result : null;
204 }
205 }
206 b.append(part);
207
208 String name = b.toString();
209 if (!isValidRevision(name)) {
210 return null;
211 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800212 String nameRedirected = getRedirectFor(name);
213
214 RevObject obj = resolve(nameRedirected, walk);
Dave Borowitz48ca6702013-04-09 11:52:41 -0700215 if (obj != null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700216 int pathStart;
217 if (oldRevision == null) {
218 pathStart = name.length(); // foo
219 } else {
220 // foo..bar (foo may be empty)
221 pathStart = oldRevision.getName().length() + 2 + name.length();
222 }
Dave Borowitze360d5c2015-09-16 16:53:30 -0400223 Result result =
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800224 new Result(
225 Revision.peel(nameRedirected, obj, walk),
226 oldRevisionRedirected,
227 path.substring(pathStart));
Dave Borowitz9de65952012-08-13 16:09:45 -0700228 return isVisible(walk, result) ? result : null;
229 }
230 first = false;
231 }
232 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700233 }
234 }
235
Matthias Sohnc156c962023-09-30 22:15:23 +0200236 private @Nullable RevObject resolve(String name, RevWalk walk) throws IOException {
Dave Borowitz5f7e8b72013-01-07 09:31:41 -0800237 try {
Dave Borowitz48ca6702013-04-09 11:52:41 -0700238 ObjectId id = repo.resolve(name);
239 return id != null ? walk.parseAny(id) : null;
Dave Borowitz93c1fca2013-09-25 16:37:43 -0700240 } catch (AmbiguousObjectException e) {
241 // TODO(dborowitz): Render a helpful disambiguation page.
242 return null;
Dave Borowitz27058932014-12-03 15:44:46 -0800243 } catch (RevisionSyntaxException | MissingObjectException e) {
Dave Borowitz48ca6702013-04-09 11:52:41 -0700244 return null;
Dave Borowitz5f7e8b72013-01-07 09:31:41 -0800245 }
246 }
247
Dave Borowitz9de65952012-08-13 16:09:45 -0700248 private static boolean isValidRevision(String revision) {
249 // Disallow some uncommon but valid revision expressions that either we
250 // don't support or we represent differently in our URLs.
Jonathan Nieder4a7dac02019-07-01 16:15:03 -0700251 return !revision.contains(":")
252 && !revision.contains("^{")
253 && !revision.contains("@{")
254 && !revision.equals("@");
Dave Borowitz9de65952012-08-13 16:09:45 -0700255 }
256
257 private boolean isVisible(RevWalk walk, Result result) throws IOException {
258 String maybeRef = OPERATOR_SPLITTER.split(result.getRevision().getName()).iterator().next();
Dave Borowitz14cad732016-05-26 17:34:19 -0400259 if (repo.findRef(maybeRef) != null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700260 // Name contains a visible ref; skip expensive reachability check.
261 return true;
262 }
Dave Borowitzdf363d22013-06-10 11:18:04 -0700263 ObjectId id = result.getRevision().getId();
264 if (!cache.isVisible(repo, walk, access, id)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700265 return false;
266 }
David Pursehousec53de2b2019-06-01 15:27:25 +0900267 if (result.getOldRevision() != null && !Revision.isNull(result.getOldRevision())) {
Dave Borowitzdf363d22013-06-10 11:18:04 -0700268 return cache.isVisible(repo, walk, access, result.getOldRevision().getId(), id);
Dave Borowitz9de65952012-08-13 16:09:45 -0700269 }
David Pursehouseb3b630f2016-06-15 21:51:18 +0900270 return true;
Dave Borowitz9de65952012-08-13 16:09:45 -0700271 }
Ronald Bhuleskar999a71d2021-11-19 15:24:51 -0800272
273 /**
274 * It replaces the ref in the revision expression to the redirected refName, without changing the
275 * behavior of the expression.
276 *
277 * <p>For eg: branch redirect {master -> main} would yield {master -> main}, {refs/heads/master^
278 * -> refs/heads/main^}, {refs/heads/master^ -> refs/heads/main^}. It does expand to a full
279 * refName even for shorter refNames.
280 */
281 private String getRedirectFor(String revisionExpression) {
282 String refName = refPart(revisionExpression);
283 Optional<String> redirect = branchRedirect.getRedirectBranch(repo, refName);
284 if (redirect.isPresent()) {
285 return redirect.get() + revisionExpression.substring(refName.length());
286 }
287 return revisionExpression;
288 }
289
290 private static String refPart(String revisionExpression) {
291 Matcher m = END_OF_REF.matcher(revisionExpression);
292 if (!m.find()) { // no terminator -> the whole string is a ref name.
293 return revisionExpression;
294 }
295 return revisionExpression.substring(0, m.start());
296 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700297}