blob: fff423ad527bda0b9da812165972f959718a06bc [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
17import static com.google.gitiles.TreeSoyData.resolveTargetUrl;
18import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
19import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
20import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
21import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
22import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
23
24import com.google.common.base.Joiner;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080025import com.google.common.collect.ImmutableList;
Dave Borowitz9de65952012-08-13 16:09:45 -070026import com.google.common.collect.ImmutableMap;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080027import com.google.common.collect.Lists;
Dave Borowitz9de65952012-08-13 16:09:45 -070028import com.google.common.collect.Maps;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080029import com.google.common.primitives.Bytes;
Dave Borowitz9de65952012-08-13 16:09:45 -070030
31import org.eclipse.jgit.errors.ConfigInvalidException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080032import org.eclipse.jgit.errors.IncorrectObjectTypeException;
Dave Borowitz9de65952012-08-13 16:09:45 -070033import org.eclipse.jgit.errors.LargeObjectException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080034import org.eclipse.jgit.errors.MissingObjectException;
35import org.eclipse.jgit.errors.StopWalkException;
Dave Borowitz9de65952012-08-13 16:09:45 -070036import org.eclipse.jgit.http.server.ServletUtils;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080037import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070038import org.eclipse.jgit.lib.FileMode;
39import org.eclipse.jgit.lib.ObjectId;
40import org.eclipse.jgit.lib.ObjectLoader;
41import org.eclipse.jgit.lib.Repository;
42import org.eclipse.jgit.revwalk.RevCommit;
43import org.eclipse.jgit.revwalk.RevObject;
44import org.eclipse.jgit.revwalk.RevTree;
45import org.eclipse.jgit.revwalk.RevWalk;
46import org.eclipse.jgit.submodule.SubmoduleWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080047import org.eclipse.jgit.treewalk.CanonicalTreeParser;
Dave Borowitz9de65952012-08-13 16:09:45 -070048import org.eclipse.jgit.treewalk.TreeWalk;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080049import org.eclipse.jgit.treewalk.filter.TreeFilter;
Dave Borowitz9de65952012-08-13 16:09:45 -070050import org.eclipse.jgit.util.RawParseUtils;
51import org.slf4j.Logger;
52import org.slf4j.LoggerFactory;
53
54import java.io.IOException;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080055import java.util.List;
Dave Borowitz9de65952012-08-13 16:09:45 -070056import java.util.Map;
57import java.util.regex.Pattern;
58
59import javax.servlet.http.HttpServletRequest;
60import javax.servlet.http.HttpServletResponse;
61
62/** Serves an HTML page with detailed information about a path within a tree. */
63// TODO(dborowitz): Handle non-UTF-8 names.
64public class PathServlet extends BaseServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080065 private static final long serialVersionUID = 1L;
Dave Borowitz9de65952012-08-13 16:09:45 -070066 private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
67
68 /**
69 * Submodule URLs where we know there is a web page if the user visits the
70 * repository URL verbatim in a web browser.
71 */
72 private static final Pattern VERBATIM_SUBMODULE_URL_PATTERN =
73 Pattern.compile("^(" + Joiner.on('|').join(
74 "https?://[^.]+.googlesource.com/.*",
75 "https?://[^.]+.googlecode.com/.*",
76 "https?://code.google.com/p/.*",
77 "https?://github.com/.*") + ")$", Pattern.CASE_INSENSITIVE);
78
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080079 static final String AUTODIVE_PARAM = "autodive";
80 static final String NO_AUTODIVE_VALUE = "0";
81
Dave Borowitz9de65952012-08-13 16:09:45 -070082 static enum FileType {
83 TREE(FileMode.TREE),
84 SYMLINK(FileMode.SYMLINK),
85 REGULAR_FILE(FileMode.REGULAR_FILE),
86 EXECUTABLE_FILE(FileMode.EXECUTABLE_FILE),
87 GITLINK(FileMode.GITLINK);
88
89 private final FileMode mode;
90
91 private FileType(FileMode mode) {
92 this.mode = mode;
93 }
94
95 static FileType forEntry(TreeWalk tw) {
96 int mode = tw.getRawMode(0);
97 for (FileType type : values()) {
98 if (type.mode.equals(mode)) {
99 return type;
100 }
101 }
102 return null;
103 }
104 }
105
106 public PathServlet(Renderer renderer) {
107 super(renderer);
108 }
109
110 @Override
111 protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
112 GitilesView view = ViewFilter.getView(req);
113 Repository repo = ServletUtils.getRepository(req);
114
115 RevWalk rw = new RevWalk(repo);
116 try {
117 RevObject obj = rw.peel(rw.parseAny(view.getRevision().getId()));
118 RevTree root;
119
120 switch (obj.getType()) {
121 case OBJ_COMMIT:
122 root = ((RevCommit) obj).getTree();
123 break;
124 case OBJ_TREE:
125 root = (RevTree) obj;
126 break;
127 default:
128 res.setStatus(SC_NOT_FOUND);
129 return;
130 }
131
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800132 TreeWalk tw = new TreeWalk(rw.getObjectReader());
133 tw.addTree(root);
134 tw.setRecursive(false);
Dave Borowitz9de65952012-08-13 16:09:45 -0700135 FileType type;
136 String path = view.getTreePath();
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800137 List<Boolean> hasSingleTree;
138
Dave Borowitz9de65952012-08-13 16:09:45 -0700139 if (path.isEmpty()) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700140 type = FileType.TREE;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800141 hasSingleTree = ImmutableList.<Boolean> of();
Dave Borowitz9de65952012-08-13 16:09:45 -0700142 } else {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800143 hasSingleTree = walkToPath(tw, path);
144 if (hasSingleTree == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700145 res.setStatus(SC_NOT_FOUND);
146 return;
147 }
148 type = FileType.forEntry(tw);
Dave Borowitz9de65952012-08-13 16:09:45 -0700149 }
150
151 switch (type) {
152 case TREE:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800153 ObjectId treeId;
154 if (path.isEmpty()) {
155 treeId = root;
156 } else {
157 treeId = tw.getObjectId(0);
158 tw.enterSubtree();
159 tw.setRecursive(false);
160 }
161 showTree(req, res, rw, tw, treeId, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700162 break;
163 case SYMLINK:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800164 showSymlink(req, res, rw, tw, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700165 break;
166 case REGULAR_FILE:
167 case EXECUTABLE_FILE:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800168 showFile(req, res, rw, tw, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700169 break;
170 case GITLINK:
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800171 showGitlink(req, res, rw, tw, root, hasSingleTree);
Dave Borowitz9de65952012-08-13 16:09:45 -0700172 break;
173 default:
Dave Borowitzfd25c3a2013-01-11 14:37:11 -0800174 log.error("Bad file type: {}", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700175 res.setStatus(SC_NOT_FOUND);
176 break;
177 }
178 } catch (LargeObjectException e) {
179 res.setStatus(SC_INTERNAL_SERVER_ERROR);
180 } finally {
181 rw.release();
182 }
183 }
184
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800185 private static class AutoDiveFilter extends TreeFilter {
186 /** @see GitilesView#getBreadcrumbs(List<Boolean>) */
187 List<Boolean> hasSingleTree;
188
189 private final byte[] pathRaw;
190 private int count;
191 private boolean done;
192
193 AutoDiveFilter(String pathStr) {
194 hasSingleTree = Lists.newArrayList();
195 pathRaw = Constants.encode(pathStr);
196 }
197
198 @Override
199 public boolean include(TreeWalk tw) throws MissingObjectException,
200 IncorrectObjectTypeException, IOException {
201 count++;
202 int cmp = tw.isPathPrefix(pathRaw, pathRaw.length);
203 if (cmp > 0) {
204 throw StopWalkException.INSTANCE;
205 }
206 boolean include;
207 if (cmp == 0) {
208 if (!isDone(tw)) {
209 hasSingleTree.add(hasSingleTreeEntry(tw));
210 }
211 include = true;
212 } else {
213 include = false;
214 }
215 if (tw.isSubtree()) {
216 count = 0;
217 }
218 return include;
219 }
220
221 private boolean hasSingleTreeEntry(TreeWalk tw) throws IOException {
222 if (count != 1 || !FileMode.TREE.equals(tw.getRawMode(0))) {
223 return false;
224 }
225 CanonicalTreeParser p = new CanonicalTreeParser();
226 p.reset(tw.getObjectReader(), tw.getObjectId(0));
227 p.next();
228 return p.eof();
229 }
230
231 @Override
232 public boolean shouldBeRecursive() {
233 return Bytes.indexOf(pathRaw, (byte)'/') >= 0;
234 }
235
236 @Override
237 public TreeFilter clone() {
238 return this;
239 }
240
241 private boolean isDone(TreeWalk tw) {
242 if (!done) {
243 done = pathRaw.length == tw.getPathLength();
244 }
245 return done;
246 }
247 }
248
249 private List<Boolean> walkToPath(TreeWalk tw, String pathString) throws IOException {
250 AutoDiveFilter f = new AutoDiveFilter(pathString);
251 tw.setFilter(f);
252 while (tw.next()) {
253 if (f.isDone(tw)) {
254 return f.hasSingleTree;
255 } else if (tw.isSubtree()) {
256 tw.enterSubtree();
257 }
258 }
259 return null;
260 }
261
Dave Borowitz9de65952012-08-13 16:09:45 -0700262 private void showTree(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800263 ObjectId id, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700264 GitilesView view = ViewFilter.getView(req);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800265 List<String> autodive = view.getParameters().get(AUTODIVE_PARAM);
266 if (autodive.size() != 1 || !NO_AUTODIVE_VALUE.equals(autodive.get(0))) {
267 byte[] path = Constants.encode(view.getTreePath());
268 CanonicalTreeParser child = getOnlyChildSubtree(rw, id, path);
269 if (child != null) {
270 while (true) {
271 path = new byte[child.getEntryPathLength()];
272 System.arraycopy(child.getEntryPathBuffer(), 0, path, 0, child.getEntryPathLength());
273 CanonicalTreeParser next = getOnlyChildSubtree(rw, child.getEntryObjectId(), path);
274 if (next == null) {
275 break;
276 }
277 child = next;
278 }
279 res.sendRedirect(GitilesView.path().copyFrom(view)
280 .setTreePath(
281 RawParseUtils.decode(child.getEntryPathBuffer(), 0, child.getEntryPathLength()))
282 .toUrl());
283 return;
284 }
285 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700286 // TODO(sop): Allow caching trees by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800287 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitz9de65952012-08-13 16:09:45 -0700288 "title", !view.getTreePath().isEmpty() ? view.getTreePath() : "/",
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800289 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700290 "type", FileType.TREE.toString(),
291 "data", new TreeSoyData(rw, view).toSoyData(id, tw)));
292 }
293
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800294 private CanonicalTreeParser getOnlyChildSubtree(RevWalk rw, ObjectId id, byte[] prefix)
Dave Borowitz9de65952012-08-13 16:09:45 -0700295 throws IOException {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800296 CanonicalTreeParser p = new CanonicalTreeParser(prefix, rw.getObjectReader(), id);
297 if (p.eof() || p.getEntryFileMode() != FileMode.TREE) {
298 return null;
299 }
300 p.next(1);
301 return p.eof() ? p : null;
302 }
303
304 private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
305 List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700306 GitilesView view = ViewFilter.getView(req);
307 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800308 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitz9de65952012-08-13 16:09:45 -0700309 "title", ViewFilter.getView(req).getTreePath(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800310 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700311 "type", FileType.forEntry(tw).toString(),
312 "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0))));
313 }
314
315 private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800316 TreeWalk tw, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700317 GitilesView view = ViewFilter.getView(req);
318 ObjectId id = tw.getObjectId(0);
319 Map<String, Object> data = Maps.newHashMap();
320
321 ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB);
322 String target;
323 try {
324 target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE));
325 } catch (LargeObjectException.OutOfMemory e) {
326 throw e;
327 } catch (LargeObjectException e) {
328 data.put("sha", ObjectId.toString(id));
329 data.put("data", null);
330 data.put("size", Long.toString(loader.getSize()));
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800331 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitz9de65952012-08-13 16:09:45 -0700332 "title", ViewFilter.getView(req).getTreePath(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800333 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700334 "type", FileType.REGULAR_FILE.toString(),
335 "data", data));
336 return;
337 }
338
339 String url = resolveTargetUrl(
340 GitilesView.path()
341 .copyFrom(view)
342 .setTreePath(dirname(view.getTreePath()))
343 .build(),
344 target);
345 data.put("title", view.getTreePath());
346 data.put("target", target);
347 if (url != null) {
348 data.put("targetUrl", url);
349 }
350
351 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800352 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitz9de65952012-08-13 16:09:45 -0700353 "title", ViewFilter.getView(req).getTreePath(),
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800354 "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
Dave Borowitz9de65952012-08-13 16:09:45 -0700355 "type", FileType.SYMLINK.toString(),
356 "data", data));
357 }
358
359 private static String dirname(String path) {
360 while (path.charAt(path.length() - 1) == '/') {
361 path = path.substring(0, path.length() - 1);
362 }
363 int lastSlash = path.lastIndexOf('/');
364 if (lastSlash > 0) {
365 return path.substring(0, lastSlash - 1);
366 } else if (lastSlash == 0) {
367 return "/";
368 } else {
369 return ".";
370 }
371 }
372
373 private void showGitlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800374 TreeWalk tw, RevTree root, List<Boolean> hasSingleTree) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700375 GitilesView view = ViewFilter.getView(req);
376 SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root,
377 view.getTreePath());
378
379 String remoteUrl;
380 try {
381 remoteUrl = sw.getRemoteUrl();
382 } catch (ConfigInvalidException e) {
383 throw new IOException(e);
384 } finally {
385 sw.release();
386 }
387
388 Map<String, Object> data = Maps.newHashMap();
389 data.put("sha", ObjectId.toString(tw.getObjectId(0)));
390 data.put("remoteUrl", remoteUrl);
391
392 // TODO(dborowitz): Guess when we can put commit SHAs in the URL.
393 String httpUrl = resolveHttpUrl(remoteUrl);
394 if (httpUrl != null) {
395 data.put("httpUrl", httpUrl);
396 }
397
398 // TODO(sop): Allow caching links by SHA-1 when no S cookie is sent.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800399 renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
Dave Borowitz9de65952012-08-13 16:09:45 -0700400 "title", view.getTreePath(),
401 "type", FileType.GITLINK.toString(),
402 "data", data));
403 }
404
405 private static String resolveHttpUrl(String remoteUrl) {
406 return VERBATIM_SUBMODULE_URL_PATTERN.matcher(remoteUrl).matches() ? remoteUrl : null;
407 }
408}