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()