blob: 9debef23208a18f496dab93dddd6a8435058c17c [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;
Dave Borowitzbcd753d2013-02-08 11:10:19 -080019
Dave Borowitz9de65952012-08-13 16:09:45 -070020import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
21import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
Dave Borowitzbcd753d2013-02-08 11:10:19 -080022
Dave Borowitz9de65952012-08-13 16:09:45 -070023import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
24import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
25import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
26
27import com.google.common.base.Joiner;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080028import com.google.common.collect.ImmutableList;
Dave Borowitz9de65952012-08-13 16:09:45 -070029import com.google.common.collect.ImmutableMap;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080030import com.google.common.collect.Lists;
Dave Borowitz9de65952012-08-13 16:09:45 -070031import com.google.common.collect.Maps;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080032import com.google.common.primitives.Bytes;
Dave Borowitz9de65952012-08-13 16:09:45 -070033
34import org.eclipse.jgit.errors.ConfigInvalidException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080035import org.eclipse.jgit.errors.IncorrectObjectTypeException;
Dave Borowitz9de65952012-08-13 16:09:45 -070036import org.eclipse.jgit.errors.LargeObjectException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080037import org.eclipse.jgit.errors.MissingObjectException;
38import org.eclipse.jgit.errors.StopWalkException;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import org.eclipse.jgit.http.server.ServletUtils;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080040import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070041import org.eclipse.jgit.lib.FileMode;
42import org.eclipse.jgit.lib.ObjectId;
43import org.eclipse.jgit.lib.ObjectLoader;
44import org.eclipse.jgit.lib.Repository;
45import org.eclipse.jgit.revwalk.RevCommit;
46import org.eclipse.jgit.revwalk.RevObject;
47import org.eclipse.jgit.revwalk.RevTree;
48import org.eclipse.jgit.revwalk.RevWalk;
49import org.eclipse.jgit.submodule.SubmoduleWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080050import org.eclipse.jgit.treewalk.CanonicalTreeParser;
Dave Borowitz9de65952012-08-13 16:09:45 -070051import org.eclipse.jgit.treewalk.TreeWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080052import org.eclipse.jgit.treewalk.filter.TreeFilter;
Dave Borowitz9de65952012-08-13 16:09:45 -070053import org.eclipse.jgit.util.RawParseUtils;
54import org.slf4j.Logger;
55import org.slf4j.LoggerFactory;
56
57import java.io.IOException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080058import java.util.List;
Dave Borowitz9de65952012-08-13 16:09:45 -070059import java.util.Map;
60import java.util.regex.Pattern;
61
62import javax.servlet.http.HttpServletRequest;
63import javax.servlet.http.HttpServletResponse;
64
65/** Serves an HTML page with detailed information about a path within a tree. */
66// TODO(dborowitz): Handle non-UTF-8 names.
67public class PathServlet extends BaseServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080068 private static final long serialVersionUID = 1L;
Dave Borowitz9de65952012-08-13 16:09:45 -070069 private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
70
71 /**
72 * Submodule URLs where we know there is a web page if the user visits the
73 * repository URL verbatim in a web browser.
74 */
75 private static final Pattern VERBATIM_SUBMODULE_URL_PATTERN =
76 Pattern.compile("^(" + Joiner.on('|').join(
77 "https?://[^.]+.googlesource.com/.*",
78 "https?://[^.]+.googlecode.com/.*",
79 "https?://code.google.com/p/.*",
80 "https?://github.com/.*") + ")$", Pattern.CASE_INSENSITIVE);
81
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080082 static final String AUTODIVE_PARAM = "autodive";
83 static final String NO_AUTODIVE_VALUE = "0";
84
Dave Borowitz9de65952012-08-13 16:09:45 -070085 static enum FileType {
86 TREE(FileMode.TREE),
87 SYMLINK(FileMode.SYMLINK),
88 REGULAR_FILE(FileMode.REGULAR_FILE),
89 EXECUTABLE_FILE(FileMode.EXECUTABLE_FILE),
90 GITLINK(FileMode.GITLINK);
91
92 private final FileMode mode;
93
94 private FileType(FileMode mode) {
95 this.mode = mode;
96 }
97
98 static FileType forEntry(TreeWalk tw) {
99 int mode = tw.getRawMode(0);
100 for (FileType type : values()) {
101 if (type.mode.equals(mode)) {
102 return type;
103 }
104 }
105 return null;
106 }
107 }
108
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800109 private final GitilesUrls urls;
110
111 public PathServlet(Renderer renderer, GitilesUrls urls) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700112 super(renderer);
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 Borowitz4e8ffd82012-12-26 16:01:06 -0800177 showGitlink(req, res, rw, tw, root, hasSingleTree);
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 {
192 /** @see GitilesView#getBreadcrumbs(List<Boolean>) */
193 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(),
297 "data", new TreeSoyData(rw, view).toSoyData(id, tw)));
298 }
299
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800300 private CanonicalTreeParser getOnlyChildSubtree(RevWalk rw, ObjectId id, byte[] prefix)
Dave Borowitz9de65952012-08-13 16:09:45 -0700301 throws IOException {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800302 CanonicalTreeParser p = new CanonicalTreeParser(prefix, rw.getObjectReader(), id);
303 if (p.eof() || p.getEntryFileMode() != FileMode.TREE) {
304 return null;
305 }
306 p.next(1);
307 return p.eof() ? p : null;
308 }
309
310 private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
311 List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700312 GitilesView view = ViewFilter.getView(req);
313 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800314 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700315 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800316 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700317 "type", FileType.forEntry(tw).toString(),
318 "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0))));
319 }
320
321 private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800322 TreeWalk tw, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700323 GitilesView view = ViewFilter.getView(req);
324 ObjectId id = tw.getObjectId(0);
325 Map<String, Object> data = Maps.newHashMap();
326
327 ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB);
328 String target;
329 try {
330 target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE));
331 } catch (LargeObjectException.OutOfMemory e) {
332 throw e;
333 } catch (LargeObjectException e) {
334 data.put("sha", ObjectId.toString(id));
335 data.put("data", null);
336 data.put("size", Long.toString(loader.getSize()));
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800337 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700338 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800339 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700340 "type", FileType.REGULAR_FILE.toString(),
341 "data", data));
342 return;
343 }
344
345 String url = resolveTargetUrl(
346 GitilesView.path()
347 .copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700348 .setPathPart(dirname(view.getPathPart()))
Dave Borowitz9de65952012-08-13 16:09:45 -0700349 .build(),
350 target);
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700351 data.put("title", view.getPathPart());
Dave Borowitz9de65952012-08-13 16:09:45 -0700352 data.put("target", target);
353 if (url != null) {
354 data.put("targetUrl", url);
355 }
356
357 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800358 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700359 "title", ViewFilter.getView(req).getPathPart(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800360 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700361 "type", FileType.SYMLINK.toString(),
362 "data", data));
363 }
364
365 private static String dirname(String path) {
366 while (path.charAt(path.length() - 1) == '/') {
367 path = path.substring(0, path.length() - 1);
368 }
369 int lastSlash = path.lastIndexOf('/');
370 if (lastSlash > 0) {
371 return path.substring(0, lastSlash - 1);
372 } else if (lastSlash == 0) {
373 return "/";
374 } else {
375 return ".";
376 }
377 }
378
379 private void showGitlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800380 TreeWalk tw, RevTree root, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700381 GitilesView view = ViewFilter.getView(req);
382 SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root,
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700383 view.getPathPart());
Dave Borowitz9de65952012-08-13 16:09:45 -0700384
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800385 String modulesUrl;
386 String remoteUrl = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700387 try {
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800388 modulesUrl = sw.getModulesUrl();
389 if (modulesUrl != null && (modulesUrl.startsWith("./") || modulesUrl.startsWith("../"))) {
390 String moduleRepo = Paths.simplifyPathUpToRoot(modulesUrl, view.getRepositoryName());
391 if (moduleRepo != null) {
392 modulesUrl = urls.getBaseGitUrl(req) + moduleRepo;
393 }
394 } else {
395 remoteUrl = sw.getRemoteUrl();
396 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700397 } catch (ConfigInvalidException e) {
398 throw new IOException(e);
399 } finally {
400 sw.release();
401 }
402
403 Map<String, Object> data = Maps.newHashMap();
404 data.put("sha", ObjectId.toString(tw.getObjectId(0)));
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800405 data.put("remoteUrl", remoteUrl != null ? remoteUrl : modulesUrl);
Dave Borowitz9de65952012-08-13 16:09:45 -0700406
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800407 // TODO(dborowitz): Guess when we can put commit SHAs in the URL.
408 String httpUrl = resolveHttpUrl(remoteUrl);
409 if (httpUrl != null) {
410 data.put("httpUrl", httpUrl);
411 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700412
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800413 // TODO(sop): Allow caching links by SHA-1 when no S cookie is sent.
414 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700415 "title", view.getPathPart(),
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800416 "type", FileType.GITLINK.toString(),
417 "data", data));
Dave Borowitz9de65952012-08-13 16:09:45 -0700418 }
419
420 private static String resolveHttpUrl(String remoteUrl) {
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800421 if (remoteUrl == null) {
422 return null;
423 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700424 return VERBATIM_SUBMODULE_URL_PATTERN.matcher(remoteUrl).matches() ? remoteUrl : null;
425 }
426}