blob: 872372236bac3d7aece41def27a4224e9856362b [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 Borowitzbcd753d2013-02-08 11:10:19 -080017import static com.google.common.base.Preconditions.checkNotNull;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static com.google.gitiles.TreeSoyData.resolveTargetUrl;
19import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
20import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
21import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
22import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
23import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
24
25import com.google.common.base.Joiner;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080026import com.google.common.collect.ImmutableList;
Dave Borowitz9de65952012-08-13 16:09:45 -070027import com.google.common.collect.ImmutableMap;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080028import com.google.common.collect.Lists;
Dave Borowitz9de65952012-08-13 16:09:45 -070029import com.google.common.collect.Maps;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080030import com.google.common.primitives.Bytes;
Dave Borowitz9de65952012-08-13 16:09:45 -070031
32import org.eclipse.jgit.errors.ConfigInvalidException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080033import org.eclipse.jgit.errors.IncorrectObjectTypeException;
Dave Borowitz9de65952012-08-13 16:09:45 -070034import org.eclipse.jgit.errors.LargeObjectException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080035import org.eclipse.jgit.errors.MissingObjectException;
36import org.eclipse.jgit.errors.StopWalkException;
Dave Borowitz9de65952012-08-13 16:09:45 -070037import org.eclipse.jgit.http.server.ServletUtils;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080038import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import org.eclipse.jgit.lib.FileMode;
40import org.eclipse.jgit.lib.ObjectId;
41import org.eclipse.jgit.lib.ObjectLoader;
42import org.eclipse.jgit.lib.Repository;
43import org.eclipse.jgit.revwalk.RevCommit;
44import org.eclipse.jgit.revwalk.RevObject;
45import org.eclipse.jgit.revwalk.RevTree;
46import org.eclipse.jgit.revwalk.RevWalk;
47import org.eclipse.jgit.submodule.SubmoduleWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080048import org.eclipse.jgit.treewalk.CanonicalTreeParser;
Dave Borowitz9de65952012-08-13 16:09:45 -070049import org.eclipse.jgit.treewalk.TreeWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080050import org.eclipse.jgit.treewalk.filter.TreeFilter;
Dave Borowitz9de65952012-08-13 16:09:45 -070051import org.eclipse.jgit.util.RawParseUtils;
52import org.slf4j.Logger;
53import org.slf4j.LoggerFactory;
54
55import java.io.IOException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080056import java.util.List;
Dave Borowitz9de65952012-08-13 16:09:45 -070057import java.util.Map;
58import java.util.regex.Pattern;
59
60import javax.servlet.http.HttpServletRequest;
61import javax.servlet.http.HttpServletResponse;
62
63/** Serves an HTML page with detailed information about a path within a tree. */
64// TODO(dborowitz): Handle non-UTF-8 names.
65public class PathServlet extends BaseServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080066 private static final long serialVersionUID = 1L;
Dave Borowitz9de65952012-08-13 16:09:45 -070067 private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
68
69 /**
70 * Submodule URLs where we know there is a web page if the user visits the
71 * repository URL verbatim in a web browser.
72 */
73 private static final Pattern VERBATIM_SUBMODULE_URL_PATTERN =
74 Pattern.compile("^(" + Joiner.on('|').join(
75 "https?://[^.]+.googlesource.com/.*",
76 "https?://[^.]+.googlecode.com/.*",
77 "https?://code.google.com/p/.*",
78 "https?://github.com/.*") + ")$", Pattern.CASE_INSENSITIVE);
79
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080080 static final String AUTODIVE_PARAM = "autodive";
81 static final String NO_AUTODIVE_VALUE = "0";
82
Dave Borowitz9de65952012-08-13 16:09:45 -070083 static enum FileType {
84 TREE(FileMode.TREE),
85 SYMLINK(FileMode.SYMLINK),
86 REGULAR_FILE(FileMode.REGULAR_FILE),
87 EXECUTABLE_FILE(FileMode.EXECUTABLE_FILE),
88 GITLINK(FileMode.GITLINK);
89
90 private final FileMode mode;
91
92 private FileType(FileMode mode) {
93 this.mode = mode;
94 }
95
96 static FileType forEntry(TreeWalk tw) {
97 int mode = tw.getRawMode(0);
98 for (FileType type : values()) {
99 if (type.mode.equals(mode)) {
100 return type;
101 }
102 }
103 return null;
104 }
105 }
106
Dave Borowitzded109a2014-03-03 15:25:39 -0500107 private final GitilesAccess.Factory accessFactory;
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800108 private final GitilesUrls urls;
109
Dave Borowitzded109a2014-03-03 15:25:39 -0500110 public PathServlet(GitilesAccess.Factory accessFactory, Renderer renderer, GitilesUrls urls) {
111 super(renderer);
112 this.accessFactory = checkNotNull(accessFactory, "accessFactory");
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800113 this.urls = checkNotNull(urls, "urls");
Dave Borowitz9de65952012-08-13 16:09:45 -0700114 }
115
116 @Override
117 protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
118 GitilesView view = ViewFilter.getView(req);
119 Repository repo = ServletUtils.getRepository(req);
120
121 RevWalk rw = new RevWalk(repo);
122 try {
123 RevObject obj = rw.peel(rw.parseAny(view.getRevision().getId()));
124 RevTree root;
125
126 switch (obj.getType()) {
127 case OBJ_COMMIT:
128 root = ((RevCommit) obj).getTree();
129 break;
130 case OBJ_TREE:
131 root = (RevTree) obj;
132 break;
133 default:
134 res.setStatus(SC_NOT_FOUND);
135 return;
136 }
137
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800138 TreeWalk tw = new TreeWalk(rw.getObjectReader());
139 tw.addTree(root);
140 tw.setRecursive(false);
Dave Borowitz9de65952012-08-13 16:09:45 -0700141 FileType type;
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700142 String path = view.getPathPart();
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800143 List<Boolean> hasSingleTree;
144
Dave Borowitz9de65952012-08-13 16:09:45 -0700145 if (path.isEmpty()) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700146 type = FileType.TREE;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800147 hasSingleTree = ImmutableList.<Boolean> of();
Dave Borowitz9de65952012-08-13 16:09:45 -0700148 } else {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800149 hasSingleTree = walkToPath(tw, path);
150 if (hasSingleTree == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700151 res.setStatus(SC_NOT_FOUND);
152 return;
153 }
154 type = FileType.forEntry(tw);
Dave Borowitz9de65952012-08-13 16:09:45 -0700155 }
156
157 switch (type) {
158 case TREE:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800159 ObjectId treeId;
160 if (path.isEmpty()) {
161 treeId = root;
162 } else {
163 treeId = tw.getObjectId(0);
164 tw.enterSubtree();
165 tw.setRecursive(false);
166 }
167 showTree(req, res, rw, tw, treeId, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700168 break;
169 case SYMLINK:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800170 showSymlink(req, res, rw, tw, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700171 break;
172 case REGULAR_FILE:
173 case EXECUTABLE_FILE:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800174 showFile(req, res, rw, tw, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700175 break;
176 case GITLINK:
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700177 showGitlink(req, res, tw, root);
Dave Borowitz9de65952012-08-13 16:09:45 -0700178 break;
179 default:
Dave Borowitzfd25c3a2013-01-11 14:37:11 -0800180 log.error("Bad file type: {}", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700181 res.setStatus(SC_NOT_FOUND);
182 break;
183 }
184 } catch (LargeObjectException e) {
185 res.setStatus(SC_INTERNAL_SERVER_ERROR);
186 } finally {
187 rw.release();
188 }
189 }
190
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800191 private static class AutoDiveFilter extends TreeFilter {
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700192 /** @see GitilesView#getBreadcrumbs(List) */
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800193 List<Boolean> hasSingleTree;
194
195 private final byte[] pathRaw;
196 private int count;
197 private boolean done;
198
199 AutoDiveFilter(String pathStr) {
200 hasSingleTree = Lists.newArrayList();
201 pathRaw = Constants.encode(pathStr);
202 }
203
204 @Override
205 public boolean include(TreeWalk tw) throws MissingObjectException,
206 IncorrectObjectTypeException, IOException {
207 count++;
208 int cmp = tw.isPathPrefix(pathRaw, pathRaw.length);
209 if (cmp > 0) {
210 throw StopWalkException.INSTANCE;
211 }
212 boolean include;
213 if (cmp == 0) {
214 if (!isDone(tw)) {
215 hasSingleTree.add(hasSingleTreeEntry(tw));
216 }
217 include = true;
218 } else {
219 include = false;
220 }
221 if (tw.isSubtree()) {
222 count = 0;
223 }
224 return include;
225 }
226
227 private boolean hasSingleTreeEntry(TreeWalk tw) throws IOException {
228 if (count != 1 || !FileMode.TREE.equals(tw.getRawMode(0))) {
229 return false;
230 }
231 CanonicalTreeParser p = new CanonicalTreeParser();
232 p.reset(tw.getObjectReader(), tw.getObjectId(0));
233 p.next();
234 return p.eof();
235 }
236
237 @Override
238 public boolean shouldBeRecursive() {
239 return Bytes.indexOf(pathRaw, (byte)'/') >= 0;
240 }
241
242 @Override
243 public TreeFilter clone() {
244 return this;
245 }
246
247 private boolean isDone(TreeWalk tw) {
248 if (!done) {
249 done = pathRaw.length == tw.getPathLength();
250 }
251 return done;
252 }
253 }
254
255 private List<Boolean> walkToPath(TreeWalk tw, String pathString) throws IOException {
256 AutoDiveFilter f = new AutoDiveFilter(pathString);
257 tw.setFilter(f);
258 while (tw.next()) {
259 if (f.isDone(tw)) {
260 return f.hasSingleTree;
261 } else if (tw.isSubtree()) {
262 tw.enterSubtree();
263 }
264 }
265 return null;
266 }
267
Dave Borowitz9de65952012-08-13 16:09:45 -0700268 private void showTree(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800269 ObjectId id, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700270 GitilesView view = ViewFilter.getView(req);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800271 List<String> autodive = view.getParameters().get(AUTODIVE_PARAM);
272 if (autodive.size() != 1 || !NO_AUTODIVE_VALUE.equals(autodive.get(0))) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700273 byte[] path = Constants.encode(view.getPathPart());
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800274 CanonicalTreeParser child = getOnlyChildSubtree(rw, id, path);
275 if (child != null) {
276 while (true) {
277 path = new byte[child.getEntryPathLength()];
278 System.arraycopy(child.getEntryPathBuffer(), 0, path, 0, child.getEntryPathLength());
279 CanonicalTreeParser next = getOnlyChildSubtree(rw, child.getEntryObjectId(), path);
280 if (next == null) {
281 break;
282 }
283 child = next;
284 }
285 res.sendRedirect(GitilesView.path().copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700286 .setPathPart(
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800287 RawParseUtils.decode(child.getEntryPathBuffer(), 0, child.getEntryPathLength()))
288 .toUrl());
289 return;
290 }
291 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700292 // TODO(sop): Allow caching trees by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800293 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700294 "title", !view.getPathPart().isEmpty() ? view.getPathPart() : "/",
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800295 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700296 "type", FileType.TREE.toString(),
Dave Borowitzc782ebe2013-11-11 11:43:29 -0800297 "data", new TreeSoyData(rw, view)
Dave Borowitzded109a2014-03-03 15:25:39 -0500298 .setArchiveFormat(getArchiveFormat(accessFactory.forRequest(req)))
Dave Borowitzc782ebe2013-11-11 11:43:29 -0800299 .toSoyData(id, tw)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700300 }
301
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800302 private CanonicalTreeParser getOnlyChildSubtree(RevWalk rw, ObjectId id, byte[] prefix)
Dave Borowitz9de65952012-08-13 16:09:45 -0700303 throws IOException {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800304 CanonicalTreeParser p = new CanonicalTreeParser(prefix, rw.getObjectReader(), id);
305 if (p.eof() || p.getEntryFileMode() != FileMode.TREE) {
306 return null;
307 }
308 p.next(1);
309 return p.eof() ? p : null;
310 }
311
312 private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
313 List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700314 GitilesView view = ViewFilter.getView(req);
315 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800316 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700317 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800318 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700319 "type", FileType.forEntry(tw).toString(),
320 "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0))));
321 }
322
323 private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800324 TreeWalk tw, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700325 GitilesView view = ViewFilter.getView(req);
326 ObjectId id = tw.getObjectId(0);
327 Map<String, Object> data = Maps.newHashMap();
328
329 ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB);
330 String target;
331 try {
332 target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE));
333 } catch (LargeObjectException.OutOfMemory e) {
334 throw e;
335 } catch (LargeObjectException e) {
336 data.put("sha", ObjectId.toString(id));
337 data.put("data", null);
338 data.put("size", Long.toString(loader.getSize()));
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800339 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700340 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800341 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700342 "type", FileType.REGULAR_FILE.toString(),
343 "data", data));
344 return;
345 }
346
347 String url = resolveTargetUrl(
348 GitilesView.path()
349 .copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700350 .setPathPart(dirname(view.getPathPart()))
Dave Borowitz9de65952012-08-13 16:09:45 -0700351 .build(),
352 target);
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700353 data.put("title", view.getPathPart());
Dave Borowitz9de65952012-08-13 16:09:45 -0700354 data.put("target", target);
355 if (url != null) {
356 data.put("targetUrl", url);
357 }
358
359 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800360 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700361 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800362 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700363 "type", FileType.SYMLINK.toString(),
364 "data", data));
365 }
366
367 private static String dirname(String path) {
368 while (path.charAt(path.length() - 1) == '/') {
369 path = path.substring(0, path.length() - 1);
370 }
371 int lastSlash = path.lastIndexOf('/');
372 if (lastSlash > 0) {
373 return path.substring(0, lastSlash - 1);
374 } else if (lastSlash == 0) {
375 return "/";
376 } else {
377 return ".";
378 }
379 }
380
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700381 private void showGitlink(HttpServletRequest req, HttpServletResponse res, TreeWalk tw,
382 RevTree root) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700383 GitilesView view = ViewFilter.getView(req);
384 SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root,
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700385 view.getPathPart());
Dave Borowitz9de65952012-08-13 16:09:45 -0700386
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800387 String modulesUrl;
388 String remoteUrl = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700389 try {
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800390 modulesUrl = sw.getModulesUrl();
391 if (modulesUrl != null && (modulesUrl.startsWith("./") || modulesUrl.startsWith("../"))) {
392 String moduleRepo = Paths.simplifyPathUpToRoot(modulesUrl, view.getRepositoryName());
393 if (moduleRepo != null) {
394 modulesUrl = urls.getBaseGitUrl(req) + moduleRepo;
395 }
396 } else {
397 remoteUrl = sw.getRemoteUrl();
398 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700399 } catch (ConfigInvalidException e) {
400 throw new IOException(e);
401 } finally {
402 sw.release();
403 }
404
405 Map<String, Object> data = Maps.newHashMap();
406 data.put("sha", ObjectId.toString(tw.getObjectId(0)));
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800407 data.put("remoteUrl", remoteUrl != null ? remoteUrl : modulesUrl);
Dave Borowitz9de65952012-08-13 16:09:45 -0700408
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800409 // TODO(dborowitz): Guess when we can put commit SHAs in the URL.
410 String httpUrl = resolveHttpUrl(remoteUrl);
411 if (httpUrl != null) {
412 data.put("httpUrl", httpUrl);
413 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700414
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800415 // TODO(sop): Allow caching links by SHA-1 when no S cookie is sent.
416 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700417 "title", view.getPathPart(),
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800418 "type", FileType.GITLINK.toString(),
419 "data", data));
Dave Borowitz9de65952012-08-13 16:09:45 -0700420 }
421
422 private static String resolveHttpUrl(String remoteUrl) {
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800423 if (remoteUrl == null) {
424 return null;
425 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700426 return VERBATIM_SUBMODULE_URL_PATTERN.matcher(remoteUrl).matches() ? remoteUrl : null;
427 }
428}