Add ROOTED_DOC view to serve Markdown at root of a host This is to support serving [1] at https://gerritcodereview.com/ without having the messy prefix of "homepage/+doc/HEAD" in the path. Google is hosting this DNS name and will run a server installing RootedDocServlet at "/" for gerritcodereview.com. This will mount and serve Markdown, making a new homepage for Gerrit. When running the DevServer set gitiles.docroot to the path of a bare Git repository to run the documentation server. In either case content is served from the "md-pages" branch. [1] https://gerrit.googlesource.com/homepage/+doc/HEAD/ Change-Id: I73ae4451586a5acbb9187b3c4dd13a05e88a1926
diff --git a/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java b/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java index 6d49d4e..f3f6597 100644 --- a/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java +++ b/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java
@@ -17,9 +17,14 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.gitiles.GitilesServlet.STATIC_PREFIX; +import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.gitiles.DebugRenderer; +import com.google.gitiles.GitilesAccess; import com.google.gitiles.GitilesServlet; import com.google.gitiles.PathServlet; +import com.google.gitiles.RepositoryDescription; +import com.google.gitiles.RootedDocServlet; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -34,8 +39,14 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.transport.resolver.RepositoryResolver; import org.eclipse.jgit.util.FS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,6 +59,12 @@ import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; class DevServer { private static final Logger log = LoggerFactory.getLogger(PathServlet.class); @@ -184,15 +201,23 @@ } private Handler appHandler() { - GitilesServlet servlet = new GitilesServlet( - cfg, - new DebugRenderer( - STATIC_PREFIX, - Arrays.asList(cfg.getStringList("gitiles", null, "customTemplates")), - new File(sourceRoot, "gitiles-servlet/src/main/resources/com/google/gitiles/templates") - .getPath(), - firstNonNull(cfg.getString("gitiles", null, "siteTitle"), "Gitiles")), - null, null, null, null, null, null, null); + DebugRenderer renderer = new DebugRenderer( + STATIC_PREFIX, + Arrays.asList(cfg.getStringList("gitiles", null, "customTemplates")), + new File(sourceRoot, "gitiles-servlet/src/main/resources/com/google/gitiles/templates") + .getPath(), + firstNonNull(cfg.getString("gitiles", null, "siteTitle"), "Gitiles")); + + String docRoot = cfg.getString("gitiles", null, "docroot"); + Servlet servlet; + if (!Strings.isNullOrEmpty(docRoot)) { + servlet = createRootedDocServlet(renderer, docRoot); + } else { + servlet = new GitilesServlet( + cfg, + renderer, + null, null, null, null, null, null, null); + } ServletContextHandler handler = new ServletContextHandler(); handler.setContextPath(""); @@ -215,4 +240,71 @@ handler.setHandler(rh); return handler; } + + private Servlet createRootedDocServlet(DebugRenderer renderer, String docRoot) { + File docRepo = new File(docRoot); + final FileKey repoKey = FileKey.exact(docRepo, FS.DETECTED); + + RepositoryResolver<HttpServletRequest> resolver = new RepositoryResolver<HttpServletRequest>() { + @Override + public Repository open(HttpServletRequest req, String name) + throws RepositoryNotFoundException { + try { + return RepositoryCache.open(repoKey, true); + } catch (IOException e) { + throw new RepositoryNotFoundException(repoKey.getFile(), e); + } + } + }; + + return new RootedDocServlet( + resolver, + new RootedDocAccess(docRepo), + renderer); + } + + private class RootedDocAccess implements GitilesAccess.Factory { + private final String repoName; + + RootedDocAccess(File docRepo) { + if (Constants.DOT_GIT.equals(docRepo.getName())) { + repoName = docRepo.getParentFile().getName(); + } else { + repoName = docRepo.getName(); + } + } + + @Override + public GitilesAccess forRequest(HttpServletRequest req) { + return new GitilesAccess() { + @Override + public Map<String, RepositoryDescription> listRepositories(Set<String> branches) { + return Collections.emptyMap(); + } + + @Override + public Object getUserKey() { + return null; + } + + @Override + public String getRepositoryName() { + return repoName; + } + + @Override + public RepositoryDescription getRepositoryDescription() { + RepositoryDescription d = new RepositoryDescription(); + d.name = getRepositoryName(); + return d; + } + + @Override + public Config getConfig() { + return cfg; + } + }; + } + } + }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java index ddb0abf..2734380 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -258,6 +258,7 @@ case BLAME: return new BlameServlet(accessFactory, renderer, blameCache); case DOC: + case ROOTED_DOC: return new DocServlet(accessFactory, renderer); default: throw new IllegalArgumentException("Invalid view type: " + view);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java index a6df07a..aac9f08 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -67,7 +67,8 @@ DESCRIBE, ARCHIVE, BLAME, - DOC; + DOC, + ROOTED_DOC; } /** Exception thrown when building a view that is invalid. */ @@ -81,7 +82,7 @@ /** Builder for views. */ public static class Builder { - private final Type type; + private Type type; private final ListMultimap<String, String> params = LinkedListMultimap.create(); private String hostName; @@ -98,6 +99,10 @@ } public Builder copyFrom(GitilesView other) { + if (type == Type.DOC && other.type == Type.ROOTED_DOC) { + type = Type.ROOTED_DOC; + } + hostName = other.hostName; servletPath = other.servletPath; switch (type) { @@ -107,6 +112,7 @@ // Fallthrough. case PATH: case DOC: + case ROOTED_DOC: case ARCHIVE: case BLAME: case SHOW: @@ -238,6 +244,7 @@ case REFS: case LOG: case DOC: + case ROOTED_DOC: break; default: checkState(path == null, "cannot set path on %s view", type); @@ -332,6 +339,8 @@ break; case DOC: checkDoc(); + case ROOTED_DOC: + checkRootedDoc(); break; } return new GitilesView(type, hostName, servletPath, repositoryName, revision, @@ -395,6 +404,13 @@ private void checkDoc() { checkRevision(); } + + private void checkRootedDoc() { + checkView(hostName != null, "missing hostName on %s view", type); + checkView(servletPath != null, "missing hostName on %s view", type); + checkView(revision != Revision.NULL, "missing revision on %s view", type); + checkView(path != null, "missing path on %s view", type); + } } public static Builder hostIndex() { @@ -445,6 +461,10 @@ return new Builder(Type.DOC); } + public static Builder rootedDoc() { + return new Builder(Type.ROOTED_DOC); + } + static String maybeTrimLeadingAndTrailingSlash(String str) { if (str.startsWith("/")) { str = str.substring(1); @@ -648,6 +668,11 @@ url.append('/').append(path); } break; + case ROOTED_DOC: + if (path != null) { + url.append(path); + } + break; default: throw new IllegalStateException("Unknown view type: " + type); }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RootedDocServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RootedDocServlet.java new file mode 100644 index 0000000..bdf4aa9 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RootedDocServlet.java
@@ -0,0 +1,97 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gitiles; + +import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_REPOSITORY; + +import com.google.gitiles.doc.DocServlet; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.resolver.RepositoryResolver; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves Markdown at the root of a host. */ +public class RootedDocServlet extends HttpServlet { + private static final Logger log = LoggerFactory.getLogger(ViewFilter.class); + private static final long serialVersionUID = 1L; + public static final String BRANCH = "refs/heads/md-pages"; + + private final RepositoryResolver<HttpServletRequest> resolver; + private final DocServlet docServlet; + + public RootedDocServlet(RepositoryResolver<HttpServletRequest> resolver, + GitilesAccess.Factory accessFactory, Renderer renderer) { + this.resolver = resolver; + docServlet = new DocServlet(accessFactory, renderer); + } + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + docServlet.init(config); + } + + @Override + public void service(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + try (Repository repo = resolver.open(req, null); + RevWalk rw = new RevWalk(repo)) { + ObjectId id = repo.resolve(BRANCH); + if (id == null) { + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + RevObject obj = rw.peel(rw.parseAny(id)); + if (!(obj instanceof RevCommit)) { + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + req.setAttribute(ATTRIBUTE_REPOSITORY, repo); + ViewFilter.setView(req, GitilesView.rootedDoc() + .setHostName(req.getServerName()) + .setServletPath(req.getContextPath() + req.getServletPath()) + .setRevision(BRANCH, obj) + .setPathPart(req.getPathInfo()) + .build()); + + docServlet.service(req, res); + } catch (RepositoryNotFoundException | ServiceNotAuthorizedException + | ServiceNotEnabledException e) { + log.error(String.format("cannot open repository for %s", req.getServerName()), e); + res.sendError(HttpServletResponse.SC_NOT_FOUND); + } finally { + ViewFilter.removeView(req); + req.removeAttribute(ATTRIBUTE_REPOSITORY); + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java index d8a0420..390a271 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -67,6 +67,10 @@ req.setAttribute(VIEW_ATTRIBUTE, view); } + static void removeView(HttpServletRequest req) { + req.removeAttribute(VIEW_ATTRIBUTE); + } + static String trimLeadingSlash(String str) { return checkLeadingSlash(str).substring(1); } @@ -121,7 +125,7 @@ try { chain.doFilter(req, res); } finally { - req.removeAttribute(VIEW_ATTRIBUTE); + removeView(req); } }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java index 1f96847..fb922dd 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
@@ -175,9 +175,11 @@ data.put("pageTitle", MoreObjects.firstNonNull( MarkdownUtil.getTitle(doc), view.getPathPart())); - data.put("sourceUrl", GitilesView.show().copyFrom(view).toUrl()); - data.put("logUrl", GitilesView.log().copyFrom(view).toUrl()); - data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl()); + if (view.getType() != GitilesView.Type.ROOTED_DOC) { + data.put("sourceUrl", GitilesView.show().copyFrom(view).toUrl()); + data.put("logUrl", GitilesView.log().copyFrom(view).toUrl()); + data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl()); + } data.put("navbarHtml", new MarkdownToHtml(view, cfg).toSoyHtml(nav)); data.put("bodyHtml", new MarkdownToHtml(view, cfg) .setImageLoader(img)
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java index 6687720..2721299 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -308,7 +308,7 @@ return url; } if (MarkdownUtil.isAbsolutePathToMarkdown(url)) { - return GitilesView.path().copyFrom(view).setPathPart(url).build().toUrl(); + return GitilesView.doc().copyFrom(view).setPathPart(url).build().toUrl(); } if (readme && !url.startsWith("../") && !url.startsWith("./")) { String dir = "";
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy index 88558d9..2d11d97 100644 --- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy
@@ -64,9 +64,9 @@ <div class="footer-line"> <div class="nav-aux"> <ul> - <li><a href="{$sourceUrl}">{msg desc="text for the source link"}source{/msg}</a></li> - <li><a href="{$logUrl}">{msg desc="text for the log link"}log{/msg}</a></li> - <li><a href="{$blameUrl}">{msg desc="text for the blame link"}blame{/msg}</a></li> + {if $sourceUrl}<li><a href="{$sourceUrl}">{msg desc="text for the source link"}source{/msg}</a></li>{/if} + {if $logUrl}<li><a href="{$logUrl}">{msg desc="text for the log link"}log{/msg}</a></li>{/if} + {if $blameUrl}<li><a href="{$blameUrl}">{msg desc="text for the blame link"}blame{/msg}</a></li>{/if} </ul> <div class="gitiles-att"> Powered by <a href="https://code.google.com/p/gitiles/">Gitiles</a>
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java index 9e6ddd8..9197c0c 100644 --- a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java +++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
@@ -327,6 +327,25 @@ } @Test + public void rootedDoc() throws Exception { + ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); + GitilesView view = GitilesView.rootedDoc() + .copyFrom(HOST) + .setRevision(Revision.unpeeled("master", id)) + .setPathPart("/docs/") + .build(); + + assertEquals("/b", view.getServletPath()); + assertEquals(Type.ROOTED_DOC, view.getType()); + assertEquals("host", view.getHostName()); + assertEquals(id, view.getRevision().getId()); + assertEquals("master", view.getRevision().getName()); + assertEquals("docs", view.getPathPart()); + assertTrue(HOST.getParameters().isEmpty()); + assertEquals("/b/docs", view.toUrl()); + } + + @Test public void multiplePathComponents() throws Exception { ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); GitilesView view = GitilesView.path()