blob: 384a5899c3353d252ac8dbe4da01841e64dd34f2 [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;
25import com.google.common.collect.ImmutableMap;
26import com.google.common.collect.Maps;
27
28import org.eclipse.jgit.errors.ConfigInvalidException;
29import org.eclipse.jgit.errors.LargeObjectException;
30import org.eclipse.jgit.http.server.ServletUtils;
31import org.eclipse.jgit.lib.FileMode;
32import org.eclipse.jgit.lib.ObjectId;
33import org.eclipse.jgit.lib.ObjectLoader;
34import org.eclipse.jgit.lib.Repository;
35import org.eclipse.jgit.revwalk.RevCommit;
36import org.eclipse.jgit.revwalk.RevObject;
37import org.eclipse.jgit.revwalk.RevTree;
38import org.eclipse.jgit.revwalk.RevWalk;
39import org.eclipse.jgit.submodule.SubmoduleWalk;
40import org.eclipse.jgit.treewalk.TreeWalk;
41import org.eclipse.jgit.util.RawParseUtils;
42import org.slf4j.Logger;
43import org.slf4j.LoggerFactory;
44
45import java.io.IOException;
46import java.util.Map;
47import java.util.regex.Pattern;
48
49import javax.servlet.http.HttpServletRequest;
50import javax.servlet.http.HttpServletResponse;
51
52/** Serves an HTML page with detailed information about a path within a tree. */
53// TODO(dborowitz): Handle non-UTF-8 names.
54public class PathServlet extends BaseServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080055 private static final long serialVersionUID = 1L;
Dave Borowitz9de65952012-08-13 16:09:45 -070056 private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
57
58 /**
59 * Submodule URLs where we know there is a web page if the user visits the
60 * repository URL verbatim in a web browser.
61 */
62 private static final Pattern VERBATIM_SUBMODULE_URL_PATTERN =
63 Pattern.compile("^(" + Joiner.on('|').join(
64 "https?://[^.]+.googlesource.com/.*",
65 "https?://[^.]+.googlecode.com/.*",
66 "https?://code.google.com/p/.*",
67 "https?://github.com/.*") + ")$", Pattern.CASE_INSENSITIVE);
68
69 static enum FileType {
70 TREE(FileMode.TREE),
71 SYMLINK(FileMode.SYMLINK),
72 REGULAR_FILE(FileMode.REGULAR_FILE),
73 EXECUTABLE_FILE(FileMode.EXECUTABLE_FILE),
74 GITLINK(FileMode.GITLINK);
75
76 private final FileMode mode;
77
78 private FileType(FileMode mode) {
79 this.mode = mode;
80 }
81
82 static FileType forEntry(TreeWalk tw) {
83 int mode = tw.getRawMode(0);
84 for (FileType type : values()) {
85 if (type.mode.equals(mode)) {
86 return type;
87 }
88 }
89 return null;
90 }
91 }
92
93 public PathServlet(Renderer renderer) {
94 super(renderer);
95 }
96
97 @Override
98 protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
99 GitilesView view = ViewFilter.getView(req);
100 Repository repo = ServletUtils.getRepository(req);
101
102 RevWalk rw = new RevWalk(repo);
103 try {
104 RevObject obj = rw.peel(rw.parseAny(view.getRevision().getId()));
105 RevTree root;
106
107 switch (obj.getType()) {
108 case OBJ_COMMIT:
109 root = ((RevCommit) obj).getTree();
110 break;
111 case OBJ_TREE:
112 root = (RevTree) obj;
113 break;
114 default:
115 res.setStatus(SC_NOT_FOUND);
116 return;
117 }
118
119 TreeWalk tw;
120 FileType type;
Dave Borowitz1c70f302012-12-20 16:43:50 -0800121 ObjectId treeId = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700122 String path = view.getTreePath();
123 if (path.isEmpty()) {
124 tw = new TreeWalk(rw.getObjectReader());
125 tw.addTree(root);
126 tw.setRecursive(false);
127 type = FileType.TREE;
Dave Borowitz1c70f302012-12-20 16:43:50 -0800128 treeId = root;
Dave Borowitz9de65952012-08-13 16:09:45 -0700129 } else {
130 tw = TreeWalk.forPath(rw.getObjectReader(), path, root);
131 if (tw == null) {
132 res.setStatus(SC_NOT_FOUND);
133 return;
134 }
135 type = FileType.forEntry(tw);
136 if (type == FileType.TREE) {
Dave Borowitz1c70f302012-12-20 16:43:50 -0800137 treeId = tw.getObjectId(0);
Dave Borowitz9de65952012-08-13 16:09:45 -0700138 tw.enterSubtree();
139 tw.setRecursive(false);
140 }
141 }
142
143 switch (type) {
144 case TREE:
Dave Borowitz1c70f302012-12-20 16:43:50 -0800145 showTree(req, res, rw, tw, treeId);
Dave Borowitz9de65952012-08-13 16:09:45 -0700146 break;
147 case SYMLINK:
148 showSymlink(req, res, rw, tw);
149 break;
150 case REGULAR_FILE:
151 case EXECUTABLE_FILE:
152 showFile(req, res, rw, tw);
153 break;
154 case GITLINK:
155 showGitlink(req, res, rw, tw, root);
156 break;
157 default:
158 log.error("Bad file type: %s", type);
159 res.setStatus(SC_NOT_FOUND);
160 break;
161 }
162 } catch (LargeObjectException e) {
163 res.setStatus(SC_INTERNAL_SERVER_ERROR);
164 } finally {
165 rw.release();
166 }
167 }
168
169 private void showTree(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
170 ObjectId id) throws IOException {
171 GitilesView view = ViewFilter.getView(req);
172 // TODO(sop): Allow caching trees by SHA-1 when no S cookie is sent.
173 render(req, res, "gitiles.pathDetail", ImmutableMap.of(
174 "title", !view.getTreePath().isEmpty() ? view.getTreePath() : "/",
175 "type", FileType.TREE.toString(),
176 "data", new TreeSoyData(rw, view).toSoyData(id, tw)));
177 }
178
179 private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw)
180 throws IOException {
181 GitilesView view = ViewFilter.getView(req);
182 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
183 render(req, res, "gitiles.pathDetail", ImmutableMap.of(
184 "title", ViewFilter.getView(req).getTreePath(),
185 "type", FileType.forEntry(tw).toString(),
186 "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0))));
187 }
188
189 private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
190 TreeWalk tw) throws IOException {
191 GitilesView view = ViewFilter.getView(req);
192 ObjectId id = tw.getObjectId(0);
193 Map<String, Object> data = Maps.newHashMap();
194
195 ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB);
196 String target;
197 try {
198 target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE));
199 } catch (LargeObjectException.OutOfMemory e) {
200 throw e;
201 } catch (LargeObjectException e) {
202 data.put("sha", ObjectId.toString(id));
203 data.put("data", null);
204 data.put("size", Long.toString(loader.getSize()));
205 render(req, res, "gitiles.pathDetail", ImmutableMap.of(
206 "title", ViewFilter.getView(req).getTreePath(),
207 "type", FileType.REGULAR_FILE.toString(),
208 "data", data));
209 return;
210 }
211
212 String url = resolveTargetUrl(
213 GitilesView.path()
214 .copyFrom(view)
215 .setTreePath(dirname(view.getTreePath()))
216 .build(),
217 target);
218 data.put("title", view.getTreePath());
219 data.put("target", target);
220 if (url != null) {
221 data.put("targetUrl", url);
222 }
223
224 // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
225 render(req, res, "gitiles.pathDetail", ImmutableMap.of(
226 "title", ViewFilter.getView(req).getTreePath(),
227 "type", FileType.SYMLINK.toString(),
228 "data", data));
229 }
230
231 private static String dirname(String path) {
232 while (path.charAt(path.length() - 1) == '/') {
233 path = path.substring(0, path.length() - 1);
234 }
235 int lastSlash = path.lastIndexOf('/');
236 if (lastSlash > 0) {
237 return path.substring(0, lastSlash - 1);
238 } else if (lastSlash == 0) {
239 return "/";
240 } else {
241 return ".";
242 }
243 }
244
245 private void showGitlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
246 TreeWalk tw, RevTree root) throws IOException {
247 GitilesView view = ViewFilter.getView(req);
248 SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root,
249 view.getTreePath());
250
251 String remoteUrl;
252 try {
253 remoteUrl = sw.getRemoteUrl();
254 } catch (ConfigInvalidException e) {
255 throw new IOException(e);
256 } finally {
257 sw.release();
258 }
259
260 Map<String, Object> data = Maps.newHashMap();
261 data.put("sha", ObjectId.toString(tw.getObjectId(0)));
262 data.put("remoteUrl", remoteUrl);
263
264 // TODO(dborowitz): Guess when we can put commit SHAs in the URL.
265 String httpUrl = resolveHttpUrl(remoteUrl);
266 if (httpUrl != null) {
267 data.put("httpUrl", httpUrl);
268 }
269
270 // TODO(sop): Allow caching links by SHA-1 when no S cookie is sent.
271 render(req, res, "gitiles.pathDetail", ImmutableMap.of(
272 "title", view.getTreePath(),
273 "type", FileType.GITLINK.toString(),
274 "data", data));
275 }
276
277 private static String resolveHttpUrl(String remoteUrl) {
278 return VERBATIM_SUBMODULE_URL_PATTERN.matcher(remoteUrl).matches() ? remoteUrl : null;
279 }
280}