Merge "Bump commons-compress version to 1.21 and tukaani-xz to 1.9"
diff --git a/.bazelrc b/.bazelrc
index dde54fe..b481c64 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,9 +1,9 @@
-build --workspace_status_command=./tools/workspace-status.sh
+build --workspace_status_command="python3 ./tools/workspace_status.py"
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --experimental_strict_action_env
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
-build --java_toolchain //tools:error_prone_warnings_toolchain
+build --java_toolchain //tools:error_prone_warnings_toolchain_java11
 
 test --build_tests_only
 test --test_output=errors
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..b339fa4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "modules/jgit"]
+	path = modules/jgit
+	url = ../jgit
diff --git a/BUILD b/BUILD
index d65fe65..3d84a08 100644
--- a/BUILD
+++ b/BUILD
@@ -6,7 +6,7 @@
     libs = [
         "//lib/jetty:server",
         "//lib/jetty:servlet",
-        "//lib/slf4j:slf4j-simple",
+        "//lib:slf4j-simple",
         "//java/com/google/gitiles:servlet",
     ],
     web_xml = "//resources:web_xml",
diff --git a/Documentation/developer-guide.md b/Documentation/developer-guide.md
index 1716e80..f0877c5 100644
--- a/Documentation/developer-guide.md
+++ b/Documentation/developer-guide.md
@@ -120,11 +120,9 @@
 Artifacts](https://gerrit-review.googlesource.com/Documentation/dev-release-deploy-config.html)
 for PGP key setup and Google Cloud Storage access setup.
 
-First, increment `GITILES_VERSION` in `version.bzl`. Technically, Gitiles uses
-the
-["build number" scheme](http://mojo.codehaus.org/versions-maven-plugin/version-rules.html),
-which in practice just means incrementing the last component (after the `-`) by
-one. Get your change reviewed and submitted.
+First, increment `GITILES_VERSION` in `version.bzl`, Gitiles uses
+[Semantic Versioning](https://semver.org).
+Get your change reviewed and submitted.
 
 Then, run:
 
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index e6b766c..4c76388 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -446,7 +446,7 @@
 PNG (`*.png`), JPEG (`*.jpg` or `*.jpeg`), GIF (`*.gif`) and WebP (`*.webp`)
 image formats are supported when referenced from the Git repository.
 
-Unsupported extensions or files larger than [image size](#Image-size)
+Unsupported extensions or files larger than [image size](config.md#Image-size)
 limit (default 256K) will display a broken image.
 
 *** note
diff --git a/WORKSPACE b/WORKSPACE
index 2602053..1753885 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -18,10 +18,15 @@
 
 load(
     "@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl",
-    "MAVEN_CENTRAL",
     "maven_jar",
 )
 
+# JGit external repository consumed from git submodule
+local_repository(
+    name = "jgit",
+    path = "modules/jgit",
+)
+
 maven_jar(
     name = "commons-lang3",
     artifact = "org.apache.commons:commons-lang3:3.8.1",
@@ -100,13 +105,7 @@
 )
 
 maven_jar(
-    name = "servlet-api_2_5",
-    artifact = "org.eclipse.jetty.orbit:javax.servlet:2.5.0.v201103041518",
-    sha1 = "9c16011c06bc6fe5e9dba080fcb40ddb4b75dc85",
-)
-
-maven_jar(
-    name = "servlet-api_3_1",
+    name = "servlet-api",
     artifact = "javax.servlet:javax.servlet-api:3.1.0",
     sha1 = "3cd63d075497751784b2fa84be59432f4905bf7c",
 )
@@ -148,40 +147,8 @@
     sha1 = "198ea005f41219f038f4291f0b0e9f3259730e92",
 )
 
-JGIT_VERS = "5.12.0.202106070339-r"
-
-JGIT_REPO = MAVEN_CENTRAL
-
 maven_jar(
-    name = "jgit-lib",
-    artifact = "org.eclipse.jgit:org.eclipse.jgit:" + JGIT_VERS,
-    repository = JGIT_REPO,
-    sha1 = "b7792da62103c956d3e58e29fb2e6e5c5f0e1317",
-)
-
-maven_jar(
-    name = "jgit-servlet",
-    artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + JGIT_VERS,
-    repository = JGIT_REPO,
-    sha1 = "c50ee52951bdcd119af0181926c25e09ae913aab",
-)
-
-maven_jar(
-    name = "jgit-junit",
-    artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + JGIT_VERS,
-    repository = JGIT_REPO,
-    sha1 = "1bb81c9104f318f16748dbaa43f95509a53e7aa0",
-)
-
-maven_jar(
-    name = "jgit-archive",
-    artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + JGIT_VERS,
-    repository = JGIT_REPO,
-    sha1 = "93f59b510a923bd757ea6b2a6e359d222daf2e1d",
-)
-
-maven_jar(
-    name = "ewah",
+    name = "javaewah",
     artifact = "com.googlecode.javaewah:JavaEWAH:1.1.7",
     sha1 = "570dde3cd706ae10c62fe19b150928cfdb415e87",
 )
@@ -210,15 +177,41 @@
 )
 
 maven_jar(
-    name = "hamcrest-core",
-    artifact = "org.hamcrest:hamcrest-core:1.3",
-    sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
+    name = "hamcrest",
+    artifact = "org.hamcrest:hamcrest:2.2",
+    sha1 = "1820c0968dba3a11a1b30669bb1f01978a91dedc",
+)
+
+maven_jar(
+    name = "mockito",
+    artifact = "org.mockito:mockito-core:2.23.0",
+    sha1 = "497ddb32fd5d01f9dbe99a2ec790aeb931dff1b1",
+)
+
+BYTE_BUDDY_VERSION = "1.9.0"
+
+maven_jar(
+    name = "bytebuddy",
+    artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+    sha1 = "8cb0d5baae526c9df46ae17693bbba302640538b",
+)
+
+maven_jar(
+    name = "bytebuddy-agent",
+    artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+    sha1 = "37b5703b4a6290be3fffc63ae9c6bcaaee0ff856",
+)
+
+maven_jar(
+    name = "objenesis",
+    artifact = "org.objenesis:objenesis:2.6",
+    sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
 )
 
 SL_VERS = "1.7.26"
 
 maven_jar(
-    name = "slf4j-api",
+    name = "log-api",
     artifact = "org.slf4j:slf4j-api:" + SL_VERS,
     sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
 )
diff --git a/java/com/google/gitiles/BUILD b/java/com/google/gitiles/BUILD
index a105f38..4b0850a 100644
--- a/java/com/google/gitiles/BUILD
+++ b/java/com/google/gitiles/BUILD
@@ -15,22 +15,22 @@
     "//lib:gfm-tables",
     "//lib:gfm-strikethrough",
     "//lib:prettify",
-    "//lib/jgit:jgit",
-    "//lib/jgit:jgit-servlet",
-    "//lib/slf4j:slf4j-api",
+    "//lib:jgit",
+    "//lib:jgit-servlet",
+    "//lib:slf4j-api",
     "//lib/soy:soy",
     "//java/com/google/gitiles/blame/cache",
 ]
 
 DEPS_ALL = DEPS + [
-    "//lib/jgit:jgit-archive",
+    "//lib:jgit-archive",
     "//lib/guice:guice",
 ]
 
 java_library(
     name = "servlet-api",
     neverlink = 1,
-    exports = ["//lib:servlet-api_2_5"],
+    exports = ["//lib:servlet-api"],
 )
 
 java_library(
diff --git a/java/com/google/gitiles/BranchRedirect.java b/java/com/google/gitiles/BranchRedirect.java
index 099d251..3a34f65 100644
--- a/java/com/google/gitiles/BranchRedirect.java
+++ b/java/com/google/gitiles/BranchRedirect.java
@@ -22,9 +22,9 @@
 
 /**
  * Utility that provides information to replace the URL string that contains a branch name to a new
- * branch name. The updated branch mapping is provided by {@code
- * BranchRedirectFilter#getRedirectBranch} method. If it should update the branch then it is the
- * caller's responsibility to update the URL with updated branch name as redirect.
+ * branch name. The updated branch mapping is provided by {@code BranchRedirect#getRedirectBranch}
+ * method. If it should update the branch then it is the caller's responsibility to update the URL
+ * with updated branch name as redirect.
  *
  * <p>This implementation does not provide a branch redirect mapping. Hence, including this as-is
  * would be a no-op. To make this effective {@code BranchRedirect#getRedirectBranch} needs to be
diff --git a/java/com/google/gitiles/CommitData.java b/java/com/google/gitiles/CommitData.java
index 23f03e8..203051b 100644
--- a/java/com/google/gitiles/CommitData.java
+++ b/java/com/google/gitiles/CommitData.java
@@ -203,7 +203,7 @@
           .toUrl();
     }
 
-    private List<Ref> getRefsById(Repository repo, ObjectId id, String prefix) {
+    private List<Ref> getRefsById(Repository repo, ObjectId id, String prefix) throws IOException {
       if (refsById == null) {
         refsById = repo.getAllRefsByPeeledObjectId();
       }
diff --git a/java/com/google/gitiles/DefaultErrorHandlingFilter.java b/java/com/google/gitiles/DefaultErrorHandlingFilter.java
index ec52bb9..f3e04ea 100644
--- a/java/com/google/gitiles/DefaultErrorHandlingFilter.java
+++ b/java/com/google/gitiles/DefaultErrorHandlingFilter.java
@@ -97,7 +97,8 @@
       HttpServletRequest req, HttpServletResponse res, int status, String message)
       throws IOException {
     res.setStatus(status);
-    renderHtml(req, res, "gitiles.error", ImmutableMap.of("title", message));
+    renderHtml(
+        req, res, "com.google.gitiles.templates.Error.error", ImmutableMap.of("title", message));
   }
 
   protected void renderHtml(
diff --git a/java/com/google/gitiles/DiffServlet.java b/java/com/google/gitiles/DiffServlet.java
index 5a9f07b..03353ac 100644
--- a/java/com/google/gitiles/DiffServlet.java
+++ b/java/com/google/gitiles/DiffServlet.java
@@ -106,7 +106,9 @@
       }
 
       setCacheHeaders(req, res);
-      try (OutputStream out = startRenderStreamingHtml(req, res, "gitiles.diffDetail", data);
+      try (OutputStream out =
+              startRenderStreamingHtml(
+                  req, res, "com.google.gitiles.templates.DiffDetail.diffDetail", data);
           DiffFormatter diff = new HtmlDiffFormatter(renderer, view, out)) {
         formatDiff(repo, oldTree, newTree, view.getPathPart(), diff);
       }
diff --git a/java/com/google/gitiles/HostIndexServlet.java b/java/com/google/gitiles/HostIndexServlet.java
index 6851688..93ff9d0 100644
--- a/java/com/google/gitiles/HostIndexServlet.java
+++ b/java/com/google/gitiles/HostIndexServlet.java
@@ -129,7 +129,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.hostIndex",
+        "com.google.gitiles.templates.HostIndex.hostIndex",
         ImmutableMap.of(
             "hostName",
             hostName,
diff --git a/java/com/google/gitiles/HtmlDiffFormatter.java b/java/com/google/gitiles/HtmlDiffFormatter.java
index 1467d42..76df04c 100644
--- a/java/com/google/gitiles/HtmlDiffFormatter.java
+++ b/java/com/google/gitiles/HtmlDiffFormatter.java
@@ -109,7 +109,7 @@
     getOutputStream()
         .write(
             renderer
-                .newRenderer("gitiles.diffHeader")
+                .newRenderer("com.google.gitiles.templates.DiffDetail.diffHeader")
                 .setData(ImmutableMap.of("firstParts", parts, "rest", rest, "fileIndex", fileIndex))
                 .renderHtml()
                 .get()
diff --git a/java/com/google/gitiles/LogServlet.java b/java/com/google/gitiles/LogServlet.java
index c738f0e..51ab68d 100644
--- a/java/com/google/gitiles/LogServlet.java
+++ b/java/com/google/gitiles/LogServlet.java
@@ -124,7 +124,9 @@
 
       data.put("title", title);
 
-      try (OutputStream out = startRenderStreamingHtml(req, res, "gitiles.logDetail", data)) {
+      try (OutputStream out =
+          startRenderStreamingHtml(
+              req, res, "com.google.gitiles.templates.LogDetail.logDetail", data)) {
         Writer w = newWriter(out, res);
         new LogSoyData(req, access, pretty)
             .renderStreaming(paginator, null, renderer, w, df, LogSoyData.FooterBehavior.NEXT);
@@ -230,7 +232,7 @@
       walk.setFirstParent(true);
     }
     if (isTrue(view, TOPO_ORDER_PARAM)) {
-      walk.sort(RevSort.TOPO, true);
+      walk.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
     }
     if (isTrue(view, REVERSE_PARAM)) {
       walk.sort(RevSort.REVERSE, true);
diff --git a/java/com/google/gitiles/LogSoyData.java b/java/com/google/gitiles/LogSoyData.java
index dc782ab..34b290f 100644
--- a/java/com/google/gitiles/LogSoyData.java
+++ b/java/com/google/gitiles/LogSoyData.java
@@ -93,23 +93,24 @@
     LoggingAdvisingAppendable out = LoggingAdvisingAppendable.delegating(writer);
     renderHtml(
         renderer
-            .newRenderer("gitiles.logEntriesHeader")
+            .newRenderer("com.google.gitiles.templates.LogDetail.logEntriesHeader")
             .setData(toHeaderSoyData(paginator, revision)),
         out);
 
-    SoySauce.Renderer entryRenderer = renderer.newRenderer("gitiles.logEntryWrapper");
+    SoySauce.Renderer entryRenderer =
+        renderer.newRenderer("com.google.gitiles.templates.LogDetail.logEntryWrapper");
     boolean renderedEntries = false;
     for (RevCommit c : paginator) {
       renderHtml(entryRenderer.setData(toEntrySoyData(paginator, c, df)), out);
       renderedEntries = true;
     }
     if (!renderedEntries) {
-      renderHtml(renderer.newRenderer("gitiles.emptyLog"), out);
+      renderHtml(renderer.newRenderer("com.google.gitiles.templates.LogDetail.emptyLog"), out);
     }
 
     renderHtml(
         renderer
-            .newRenderer("gitiles.logEntriesFooter")
+            .newRenderer("com.google.gitiles.templates.LogDetail.logEntriesFooter")
             .setData(toFooterSoyData(paginator, revision, footerBehavior)),
         out);
   }
diff --git a/java/com/google/gitiles/PathServlet.java b/java/com/google/gitiles/PathServlet.java
index 26de757..4054332 100644
--- a/java/com/google/gitiles/PathServlet.java
+++ b/java/com/google/gitiles/PathServlet.java
@@ -68,6 +68,7 @@
 
   static final String MODE_HEADER = "X-Gitiles-Path-Mode";
   static final String TYPE_HEADER = "X-Gitiles-Object-Type";
+  static final String PATH_DETAIL = "com.google.gitiles.templates.PathDetail.pathDetail";
 
   /**
    * Submodule URLs where we know there is a web page if the user visits the repository URL verbatim
@@ -486,7 +487,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.pathDetail",
+        PATH_DETAIL,
         ImmutableMap.of(
             "title", !view.getPathPart().isEmpty() ? view.getPathPart() : "/",
             "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
@@ -515,7 +516,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.pathDetail",
+        PATH_DETAIL,
         ImmutableMap.of(
             "title", ViewFilter.getView(req).getPathPart(),
             "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
@@ -541,7 +542,7 @@
       renderHtml(
           req,
           res,
-          "gitiles.pathDetail",
+          PATH_DETAIL,
           ImmutableMap.of(
               "title", ViewFilter.getView(req).getPathPart(),
               "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
@@ -564,7 +565,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.pathDetail",
+        PATH_DETAIL,
         ImmutableMap.of(
             "title", ViewFilter.getView(req).getPathPart(),
             "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
@@ -621,7 +622,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.pathDetail",
+        PATH_DETAIL,
         ImmutableMap.of(
             "title", view.getPathPart(),
             "type", FileType.GITLINK.toString(),
diff --git a/java/com/google/gitiles/RefServlet.java b/java/com/google/gitiles/RefServlet.java
index fd93126..0df7323 100644
--- a/java/com/google/gitiles/RefServlet.java
+++ b/java/com/google/gitiles/RefServlet.java
@@ -68,7 +68,7 @@
     renderHtml(
         req,
         res,
-        "gitiles.refsDetail",
+        "com.google.gitiles.templates.RefList.refsDetail",
         ImmutableMap.of("branches", getBranchesSoyData(req, 0), "tags", tags));
   }
 
diff --git a/java/com/google/gitiles/Renderer.java b/java/com/google/gitiles/Renderer.java
index 957e50d..65aea7e 100644
--- a/java/com/google/gitiles/Renderer.java
+++ b/java/com/google/gitiles/Renderer.java
@@ -52,7 +52,7 @@
  */
 public abstract class Renderer {
   // Must match .streamingPlaceholder.
-  private static final String PLACEHOLDER = "id=\"STREAMED_OUTPUT_BLOCK\"";
+  private static final String PLACEHOLDER = "id=\"STREAMED-OUTPUT-BLOCK\"";
 
   private static final ImmutableList<String> SOY_FILENAMES =
       ImmutableList.of(
diff --git a/java/com/google/gitiles/RepositoryIndexServlet.java b/java/com/google/gitiles/RepositoryIndexServlet.java
index d057512..0360588 100644
--- a/java/com/google/gitiles/RepositoryIndexServlet.java
+++ b/java/com/google/gitiles/RepositoryIndexServlet.java
@@ -129,7 +129,8 @@
       if (paginator != null) {
         DateFormatter df = new DateFormatter(access, Format.DEFAULT);
         try (OutputStream out =
-            startRenderStreamingHtml(req, res, "gitiles.repositoryIndex", data)) {
+            startRenderStreamingHtml(
+                req, res, "com.google.gitiles.templates.RepositoryIndex.repositoryIndex", data)) {
           Writer w = newWriter(out, res);
           new LogSoyData(req, access, "oneline")
               .renderStreaming(
@@ -137,7 +138,7 @@
           w.flush();
         }
       } else {
-        renderHtml(req, res, "gitiles.repositoryIndex", data);
+        renderHtml(req, res, "com.google.gitiles.templates.RepositoryIndex.repositoryIndex", data);
       }
     }
   }
diff --git a/java/com/google/gitiles/RevisionServlet.java b/java/com/google/gitiles/RevisionServlet.java
index 38aecb6..3aeb352 100644
--- a/java/com/google/gitiles/RevisionServlet.java
+++ b/java/com/google/gitiles/RevisionServlet.java
@@ -134,7 +134,7 @@
       renderHtml(
           req,
           res,
-          "gitiles.revisionDetail",
+          "com.google.gitiles.templates.RevisionDetail.revisionDetail",
           ImmutableMap.of(
               "title", view.getRevision().getName(),
               "objects", soyObjects,
diff --git a/java/com/google/gitiles/TreeSoyData.java b/java/com/google/gitiles/TreeSoyData.java
index e1aeb21..985edfa 100644
--- a/java/com/google/gitiles/TreeSoyData.java
+++ b/java/com/google/gitiles/TreeSoyData.java
@@ -73,8 +73,22 @@
     return lastSlash >= 0 ? "..." + target.substring(lastSlash) : target;
   }
 
-  static int sortByType(Map<String, String> m1, Map<String, String> m2) {
-    return TYPE_WEIGHT.get(m1.get("type")).compareTo(TYPE_WEIGHT.get(m2.get("type")));
+  static String stripEndingSolidus(String p) {
+    return p.endsWith("/") ? p.substring(0, p.length() - 1) : p;
+  }
+
+  static int sortByTypeAlpha(Map<String, String> m1, Map<String, String> m2) {
+    int weightDiff = TYPE_WEIGHT.get(m1.get("type")).compareTo(TYPE_WEIGHT.get(m2.get("type")));
+    if (weightDiff == 0) {
+      String s1 = m1.get("name");
+      String s2 = m2.get("name");
+      if (m1.get("type").equals("TREE")) {
+        s1 = stripEndingSolidus(s1);
+        s2 = stripEndingSolidus(s2);
+      }
+      return s1.compareToIgnoreCase(s2);
+    }
+    return weightDiff;
   }
 
   private final ObjectReader reader;
@@ -141,7 +155,7 @@
       entries.add(entry);
     }
 
-    entries.sort(TreeSoyData::sortByType);
+    entries.sort(TreeSoyData::sortByTypeAlpha);
 
     Map<String, Object> data = Maps.newHashMapWithExpectedSize(3);
     data.put("sha", treeId.name());
diff --git a/java/com/google/gitiles/blame/BlameServlet.java b/java/com/google/gitiles/blame/BlameServlet.java
index ab9cd95..9dd5e91 100644
--- a/java/com/google/gitiles/blame/BlameServlet.java
+++ b/java/com/google/gitiles/blame/BlameServlet.java
@@ -86,7 +86,7 @@
         renderHtml(
             req,
             res,
-            "gitiles.blameDetail",
+            "com.google.gitiles.templates.BlameDetail.blameDetail",
             ImmutableMap.of(
                 "title",
                 title,
@@ -100,7 +100,7 @@
         renderHtml(
             req,
             res,
-            "gitiles.blameDetail",
+            "com.google.gitiles.templates.BlameDetail.blameDetail",
             ImmutableMap.of(
                 "title", title,
                 "breadcrumbs", view.getBreadcrumbs(),
diff --git a/java/com/google/gitiles/blame/cache/BUILD b/java/com/google/gitiles/blame/cache/BUILD
index e1491fc..e256ea3 100644
--- a/java/com/google/gitiles/blame/cache/BUILD
+++ b/java/com/google/gitiles/blame/cache/BUILD
@@ -8,7 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//lib:guava",
-        "//lib/jgit",
+        "//lib:jgit",
     ],
 )
 
@@ -17,7 +17,7 @@
     libs = [
         ":cache",
         "//lib:guava",
-        "//lib/jgit:jgit",
+        "//lib:jgit",
     ],
     pkgs = ["com.google.gitiles.blame.cache"],
     title = "Blame Cache API Documentation",
diff --git a/java/com/google/gitiles/dev/BUILD b/java/com/google/gitiles/dev/BUILD
index 9550cf0..72caf40 100644
--- a/java/com/google/gitiles/dev/BUILD
+++ b/java/com/google/gitiles/dev/BUILD
@@ -9,13 +9,13 @@
         "//lib:guava",
         "//lib:guava-failureaccess",
         "//lib:html-types",
-        "//lib:servlet-api_3_1",
+        "//lib:jgit",
+        "//lib:jgit-servlet",
+        "//lib:servlet-api",
+        "//lib:slf4j-api",
+        "//lib:slf4j-simple",
         "//lib/jetty:server",
         "//lib/jetty:servlet",
-        "//lib/jgit",
-        "//lib/jgit:jgit-servlet",
-        "//lib/slf4j:slf4j-api",
-        "//lib/slf4j:slf4j-simple",
         "//lib/soy",
     ],
 )
diff --git a/java/com/google/gitiles/dev/DevServer.java b/java/com/google/gitiles/dev/DevServer.java
index 5a8fed5..028edd6 100644
--- a/java/com/google/gitiles/dev/DevServer.java
+++ b/java/com/google/gitiles/dev/DevServer.java
@@ -19,7 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.html.types.UncheckedConversions;
-import com.google.gitiles.BranchRedirectFilter;
+import com.google.gitiles.BranchRedirect;
 import com.google.gitiles.DebugRenderer;
 import com.google.gitiles.GitilesAccess;
 import com.google.gitiles.GitilesServlet;
@@ -140,7 +140,7 @@
     } else {
       servlet =
           new GitilesServlet(
-              cfg, renderer, null, null, null, null, null, null, null, new BranchRedirectFilter());
+              cfg, renderer, null, null, null, null, null, null, null, new BranchRedirect());
     }
 
     ServletContextHandler handler = new ServletContextHandler();
diff --git a/java/com/google/gitiles/doc/DocServlet.java b/java/com/google/gitiles/doc/DocServlet.java
index 0cbea50..4def176 100644
--- a/java/com/google/gitiles/doc/DocServlet.java
+++ b/java/com/google/gitiles/doc/DocServlet.java
@@ -60,7 +60,7 @@
   private static final String INDEX_MD = "index.md";
   private static final String NAVBAR_MD = "navbar.md";
   private static final String SOY_FILE = "Doc.soy";
-  private static final String SOY_TEMPLATE = "gitiles.markdownDoc";
+  private static final String SOY_TEMPLATE = "com.google.gitiles.templates.Doc.markdownDoc";
 
   // Generation of ETag logic. Bump this only if DocServlet logic changes
   // significantly enough to impact cached pages. Soy template and source
diff --git a/javatests/com/google/gitiles/BUILD b/javatests/com/google/gitiles/BUILD
index 4021114..8e90e16 100644
--- a/javatests/com/google/gitiles/BUILD
+++ b/javatests/com/google/gitiles/BUILD
@@ -5,8 +5,8 @@
     "//lib:gson",
     "//lib:guava",
     "//lib:guava-failureaccess",
-    "//lib/jgit:jgit",
-    "//lib/jgit:jgit-servlet",
+    "//lib:jgit",
+    "//lib:jgit-servlet",
     "//lib/soy:soy",
 ]
 
@@ -19,30 +19,30 @@
     deps = DEPS + [
         "//java/com/google/gitiles:servlet",
         "//lib:jsr305",
-        "//lib:servlet-api_2_5",
+        "//lib:servlet-api",
         "//lib/truth",
-        "//lib/jgit:junit",
+        "//lib:jgit-junit",
         "//lib/junit",
     ],
 )
 
 junit_tests(
     name = "servlet_tests",
+    size = "small",
     srcs = glob(
         [
             "**/*Test.java",
         ],
         exclude = ["**/ServletTest.java"],
     ),
-    size = "small",
     visibility = ["//visibility:public"],
-    runtime_deps = ["//lib/junit:hamcrest-core"],
+    runtime_deps = ["//lib/junit:hamcrest"],
     deps = DEPS + [
         "//java/com/google/gitiles:servlet",
         ":testutil",
-        "//lib:servlet-api_2_5",
+        "//lib:servlet-api",
         "//lib/truth",
-        "//lib/jgit:junit",
+        "//lib:jgit-junit",
         "//lib/junit",
     ],
 )
diff --git a/javatests/com/google/gitiles/FakeHttpServletRequest.java b/javatests/com/google/gitiles/FakeHttpServletRequest.java
index ba94c3e..14ddd59 100644
--- a/javatests/com/google/gitiles/FakeHttpServletRequest.java
+++ b/javatests/com/google/gitiles/FakeHttpServletRequest.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.security.Principal;
@@ -34,11 +35,20 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
 import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
 import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
 import org.eclipse.jgit.http.server.ServletUtils;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
 
@@ -101,6 +111,11 @@
   }
 
   @Override
+  public long getContentLengthLong() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public String getContentType() {
     return null;
   }
@@ -126,6 +141,41 @@
   }
 
   @Override
+  public ServletContext getServletContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public AsyncContext startAsync() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isAsyncStarted() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isAsyncSupported() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public AsyncContext getAsyncContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DispatcherType getDispatcherType() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public Locale getLocale() {
     return Locale.US;
   }
@@ -353,6 +403,11 @@
   }
 
   @Override
+  public String changeSessionId() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public HttpSession getSession(boolean create) {
     throw new UnsupportedOperationException();
   }
@@ -379,6 +434,37 @@
   }
 
   @Override
+  public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void login(String username, String password) throws ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void logout() throws ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Collection<Part> getParts() throws IOException, ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Part getPart(String name) throws IOException, ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T extends HttpUpgradeHandler> T upgrade(Class<T> httpUpgradeHandlerClass)
+      throws IOException, ServletException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public boolean isRequestedSessionIdValid() {
     throw new UnsupportedOperationException();
   }
diff --git a/javatests/com/google/gitiles/FakeHttpServletResponse.java b/javatests/com/google/gitiles/FakeHttpServletResponse.java
index 2cf316d..ff20cea 100644
--- a/javatests/com/google/gitiles/FakeHttpServletResponse.java
+++ b/javatests/com/google/gitiles/FakeHttpServletResponse.java
@@ -28,8 +28,10 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.nio.charset.Charset;
+import java.util.Collection;
 import java.util.Locale;
 import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -84,6 +86,16 @@
       outputStream =
           new ServletOutputStream() {
             @Override
+            public boolean isReady() {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void setWriteListener(WriteListener listener) {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
             public void write(int c) throws IOException {
               osWriter.write(c);
               osWriter.flush();
@@ -134,6 +146,11 @@
   }
 
   @Override
+  public void setContentLengthLong(long length) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public void setContentType(String type) {
     headers.removeAll(HttpHeaders.CONTENT_TYPE);
     headers.put(HttpHeaders.CONTENT_TYPE, type);
@@ -240,6 +257,7 @@
     committed = true;
   }
 
+  @Override
   public synchronized int getStatus() {
     return status;
   }
@@ -252,10 +270,21 @@
     return RawParseUtils.decode(getActualBody());
   }
 
+  @Override
   public String getHeader(String name) {
     return Iterables.getFirst(headers.get(checkNotNull(name)), null);
   }
 
+  @Override
+  public Collection<String> getHeaders(String name) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Collection<String> getHeaderNames() {
+    throw new UnsupportedOperationException();
+  }
+
   private PrintWriter newPrintWriter() {
     return new PrintWriter(new OutputStreamWriter(actualBody, UTF_8));
   }
diff --git a/javatests/com/google/gitiles/LogServletTest.java b/javatests/com/google/gitiles/LogServletTest.java
index 7232439..4ef7a4d 100644
--- a/javatests/com/google/gitiles/LogServletTest.java
+++ b/javatests/com/google/gitiles/LogServletTest.java
@@ -80,6 +80,24 @@
   }
 
   @Test
+  public void topoKeepBranchTogetherLog() throws Exception {
+    RevCommit a = repo.update("master", repo.commit().add("foo", "foo\n"));
+    RevCommit b1 = repo.update("master", repo.commit().parent(a).add("foo", "foo3\n"));
+    RevCommit c = repo.update("master", repo.commit().parent(a).add("foo", "foo2\n"));
+    RevCommit b2 = repo.update("master", repo.commit().parent(b1).add("foo", "foo4\n"));
+    RevCommit d = repo.update("master", repo.commit().parent(c).parent(b2).add("foo", "foo5\n"));
+
+    Log response = buildJson(LOG, "/repo/+log/master", "topo-order");
+    assertThat(response.log).hasSize(5);
+
+    verifyJsonCommit(response.log.get(0), d);
+    verifyJsonCommit(response.log.get(1), b2);
+    verifyJsonCommit(response.log.get(2), b1);
+    verifyJsonCommit(response.log.get(3), c);
+    verifyJsonCommit(response.log.get(4), a);
+  }
+
+  @Test
   public void follow() throws Exception {
     String contents = "contents";
     RevCommit c1 = repo.branch("master").commit().add("foo", contents).create();
diff --git a/javatests/com/google/gitiles/TreeSoyDataTest.java b/javatests/com/google/gitiles/TreeSoyDataTest.java
index cafe4e2..75ae771 100644
--- a/javatests/com/google/gitiles/TreeSoyDataTest.java
+++ b/javatests/com/google/gitiles/TreeSoyDataTest.java
@@ -17,11 +17,11 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gitiles.TreeSoyData.getTargetDisplayName;
 import static com.google.gitiles.TreeSoyData.resolveTargetUrl;
-import static com.google.gitiles.TreeSoyData.sortByType;
+import static com.google.gitiles.TreeSoyData.sortByTypeAlpha;
 
 import com.google.common.base.Strings;
-import java.util.Map;
 import java.util.HashMap;
+import java.util.Map;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,22 +70,44 @@
 
   @Test
   public void sortByTypeSortsCorrect() throws Exception {
-    Map<String, String> m1 = new HashMap<String, String>();
-    Map<String, String> m2 = new HashMap<String, String>();
-    Map<String, String> m3 = new HashMap<String, String>();
-    Map<String, String> m4 = new HashMap<String, String>();
-    Map<String, String> m5 = new HashMap<String, String>();
+    Map<String, String> m1 = new HashMap<>();
+    Map<String, String> m2 = new HashMap<>();
+    Map<String, String> m3 = new HashMap<>();
+    Map<String, String> m4 = new HashMap<>();
+    Map<String, String> m5 = new HashMap<>();
+    Map<String, String> m6 = new HashMap<>();
     m1.put("type", "TREE");
+    m1.put("name", "aa");
     m2.put("type", "TREE");
+    m2.put("name", "BB");
     m3.put("type", "SYMLINK");
     m4.put("type", "REGULAR_FILE");
     m5.put("type", "GITLINK");
-    assertThat(sortByType(m1, m2)).isEqualTo(0);
-    assertThat(sortByType(m2, m3)).isEqualTo(-1);
-    assertThat(sortByType(m3, m4)).isEqualTo(-1);
-    assertThat(sortByType(m4, m1)).isEqualTo(1);
-    assertThat(sortByType(m1, m4)).isEqualTo(-1);
-    assertThat(sortByType(m5, m2)).isEqualTo(1);
-    assertThat(sortByType(m2, m5)).isEqualTo(-1);
+    m6.put("type", "TREE");
+    m6.put("name", "AA");
+    assertThat(sortByTypeAlpha(m1, m2)).isEqualTo(-1);
+    assertThat(sortByTypeAlpha(m2, m3)).isEqualTo(-1);
+    assertThat(sortByTypeAlpha(m3, m4)).isEqualTo(-1);
+    assertThat(sortByTypeAlpha(m4, m1)).isEqualTo(1);
+    assertThat(sortByTypeAlpha(m1, m4)).isEqualTo(-1);
+    assertThat(sortByTypeAlpha(m5, m2)).isEqualTo(1);
+    assertThat(sortByTypeAlpha(m2, m5)).isEqualTo(-1);
+    assertThat(sortByTypeAlpha(m1, m6)).isEqualTo(0);
+    assertThat(sortByTypeAlpha(m2, m1)).isEqualTo(1);
+  }
+
+  @Test
+  public void sortByShortestPathFirst() throws Exception {
+    Map<String, String> p1 = new HashMap<>();
+    Map<String, String> p2 = new HashMap<>();
+    Map<String, String> p3 = new HashMap<>();
+    p1.put("type", "TREE");
+    p1.put("name", "short/");
+    p2.put("type", "TREE");
+    p2.put("name", "shortpath/");
+    p3.put("type", "TREE");
+    p3.put("name", "short.path/");
+    assertThat(sortByTypeAlpha(p1, p2)).isLessThan(0);
+    assertThat(sortByTypeAlpha(p1, p3)).isLessThan(0);
   }
 }
diff --git a/lib/BUILD b/lib/BUILD
index a6f026f..a0dfa57 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -17,8 +17,7 @@
     "gfm-tables",
     "html-types",
     "jsr305",
-    "servlet-api_2_5",
-    "servlet-api_3_1",
+    "servlet-api",
     "gson",
     "guava",
     "guava-failureaccess",
@@ -29,3 +28,58 @@
     "ow2-asm-tree",
     "ow2-asm-util",
 ]]
+
+java_library(
+    name = "slf4j-api",
+    exports = ["@log-api//jar"],
+)
+
+java_library(
+    name = "slf4j-simple",
+    runtime_deps = [
+        ":slf4j-api",
+        "@slf4j-simple//jar",
+    ],
+)
+
+java_library(
+    name = "jgit",
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit:jgit"],
+    runtime_deps = [
+        ":slf4j-api",
+        "@javaewah//jar",
+    ],
+)
+
+java_library(
+    name = "jgit-archive",
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.archive:jgit-archive"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "jgit-junit",
+    testonly = True,
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.junit:junit"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "jgit-servlet",
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.http.server:jgit-servlet"],
+    runtime_deps = [":jgit"],
+)
+
+java_library(
+    name = "tukaani-xz",
+    exports = ["@tukaani-xz//jar"],
+)
+
+java_library(
+    name = "commons-compress",
+    exports = ["@commons-compress//jar"],
+)
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index 4f58b3d..3a35ef5 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -8,7 +8,7 @@
     name = "servlet",
     exports = [
         ":security",
-        "//lib:servlet-api_3_1",  # Different from the rest of gitiles-server.
+        "//lib:servlet-api",
         "@servlet//jar",
     ],
 )
diff --git a/lib/jgit/BUILD b/lib/jgit/BUILD
deleted file mode 100644
index d813fd7..0000000
--- a/lib/jgit/BUILD
+++ /dev/null
@@ -1,44 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-package(
-    default_visibility = ["//visibility:public"],
-)
-
-java_library(
-    name = "jgit-servlet",
-    exports = ["@jgit-servlet//jar"],
-)
-
-java_library(
-    name = "jgit",
-    exports = ["@jgit-lib//jar"],
-)
-
-java_library(
-    name = "jgit-archive",
-    exports = [
-        ":commons-compress",
-        ":jgit-archive_library",
-        ":tukaani-xz",
-    ],
-)
-
-java_library(
-    name = "tukaani-xz",
-    exports = ["@tukaani-xz//jar"],
-)
-
-java_library(
-    name = "commons-compress",
-    exports = ["@commons-compress//jar"],
-)
-
-java_library(
-    name = "jgit-archive_library",
-    exports = ["@jgit-archive//jar"],
-)
-
-java_library(
-    name = "junit",
-    exports = ["@jgit-junit//jar"],
-)
diff --git a/lib/junit/BUILD b/lib/junit/BUILD
index 7c03dd3..f3e9cca 100644
--- a/lib/junit/BUILD
+++ b/lib/junit/BUILD
@@ -10,6 +10,6 @@
 )
 
 java_library(
-    name = "hamcrest-core",
-    exports = ["@hamcrest-core//jar"],
+    name = "hamcrest",
+    exports = ["@hamcrest//jar"],
 )
diff --git a/lib/slf4j/BUILD b/lib/slf4j/BUILD
deleted file mode 100644
index 2719aa8..0000000
--- a/lib/slf4j/BUILD
+++ /dev/null
@@ -1,18 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_library")
-
-package(
-    default_visibility = ["//visibility:public"],
-)
-
-java_library(
-    name = "slf4j-api",
-    exports = ["@slf4j-api//jar"],
-)
-
-java_library(
-    name = "slf4j-simple",
-    runtime_deps = [
-        ":slf4j-api",
-        "@slf4j-simple//jar",
-    ],
-)
diff --git a/modules/jgit b/modules/jgit
new file mode 160000
index 0000000..2d9ed3c
--- /dev/null
+++ b/modules/jgit
@@ -0,0 +1 @@
+Subproject commit 2d9ed3cf2aa01491e43d5108bda5dc494b53e1f8
diff --git a/resources/com/google/gitiles/static/base.css b/resources/com/google/gitiles/static/base.css
index a1d5a36..f4ef52a 100644
--- a/resources/com/google/gitiles/static/base.css
+++ b/resources/com/google/gitiles/static/base.css
@@ -116,9 +116,9 @@
   padding: 20px;
 }
 .Site-header--withNavbar .Header {
-  max-width: 980px;
+  max-width: 1020px;
   margin: 0 auto;
-  padding: 10px 0;
+  padding: 10px 20px;
 }
 .Header-title,
 .Header-image {
@@ -286,6 +286,24 @@
   font-size: 14px;
   font-style: italic;
 }
+@media (max-width: 550px) {
+  .RepoShortlog {
+    flex-direction: column;
+    -ms-flex-direction: column;
+  }
+  .RepoShortlog-refs {
+    width: auto;
+    border-bottom: 1px solid #ddd;
+    margin-bottom: 20px;
+    padding-bottom: 20px;
+  }
+  .RepoShortlog-refs > .RefList:last-child {
+    margin-bottom: 0;
+  }
+  .RepoShortlog-log {
+    width: auto;
+  }
+}
 
 /* RefList.soy */
 
@@ -302,6 +320,12 @@
 .RefList-item {
   padding: 2px 0;
 }
+@media (max-width: 550px) {
+  .RefList--responsive .RefList-item {
+    display: inline-block;
+    margin-right: 6px;
+  }
+}
 
 /* LogDetail.soy */
 
diff --git a/resources/com/google/gitiles/static/doc.css b/resources/com/google/gitiles/static/doc.css
index a531087..8a92fce 100644
--- a/resources/com/google/gitiles/static/doc.css
+++ b/resources/com/google/gitiles/static/doc.css
@@ -20,8 +20,9 @@
   padding-top: 0;
 }
 .Header-nav ul {
-  max-width: 980px;
+  max-width: 1020px;
   margin: 0 auto;
+  padding: 0 20px;
 }
 .Header-nav li {
   display: inline-block;
diff --git a/resources/com/google/gitiles/templates/BlameDetail.soy b/resources/com/google/gitiles/templates/BlameDetail.soy
index 1589056..507b00f 100644
--- a/resources/com/google/gitiles/templates/BlameDetail.soy
+++ b/resources/com/google/gitiles/templates/BlameDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.BlameDetail}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as objDetail from 'com/google/gitiles/templates/ObjectDetail.soy';
diff --git a/resources/com/google/gitiles/templates/Common.soy b/resources/com/google/gitiles/templates/Common.soy
index 3a33299..c38f727 100644
--- a/resources/com/google/gitiles/templates/Common.soy
+++ b/resources/com/google/gitiles/templates/Common.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.Common}
 
 /**
  * Common header for Gitiles.
@@ -32,6 +32,7 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>
     {$title}
     {if $repositoryName}
@@ -170,5 +171,5 @@
  * Renderer#renderStreaming(HttpServletResponse, String).
  */
 {template streamingPlaceholder}
-<br id="STREAMED_OUTPUT_BLOCK">
+<br id="STREAMED-OUTPUT-BLOCK">
 {/template}
diff --git a/resources/com/google/gitiles/templates/DiffDetail.soy b/resources/com/google/gitiles/templates/DiffDetail.soy
index 519ed77..afe63e8 100644
--- a/resources/com/google/gitiles/templates/DiffDetail.soy
+++ b/resources/com/google/gitiles/templates/DiffDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.DiffDetail}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as objDetail from 'com/google/gitiles/templates/ObjectDetail.soy';
diff --git a/resources/com/google/gitiles/templates/Doc.soy b/resources/com/google/gitiles/templates/Doc.soy
index c541033..db46994 100644
--- a/resources/com/google/gitiles/templates/Doc.soy
+++ b/resources/com/google/gitiles/templates/Doc.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.Doc}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 
@@ -51,6 +51,7 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>
     {if $siteTitle}{$siteTitle} -{sp}{/if}
     {$pageTitle}
diff --git a/resources/com/google/gitiles/templates/Error.soy b/resources/com/google/gitiles/templates/Error.soy
index 7ca5024..c8ebc9e 100644
--- a/resources/com/google/gitiles/templates/Error.soy
+++ b/resources/com/google/gitiles/templates/Error.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.Error}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 
diff --git a/resources/com/google/gitiles/templates/HostIndex.soy b/resources/com/google/gitiles/templates/HostIndex.soy
index a8d84f8..846904a 100644
--- a/resources/com/google/gitiles/templates/HostIndex.soy
+++ b/resources/com/google/gitiles/templates/HostIndex.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.HostIndex}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 
diff --git a/resources/com/google/gitiles/templates/LogDetail.soy b/resources/com/google/gitiles/templates/LogDetail.soy
index d79f25a..23ae8eb 100644
--- a/resources/com/google/gitiles/templates/LogDetail.soy
+++ b/resources/com/google/gitiles/templates/LogDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.LogDetail}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as objDetail from 'com/google/gitiles/templates/ObjectDetail.soy';
diff --git a/resources/com/google/gitiles/templates/ObjectDetail.soy b/resources/com/google/gitiles/templates/ObjectDetail.soy
index 4fc4b99..3d3c6e4 100644
--- a/resources/com/google/gitiles/templates/ObjectDetail.soy
+++ b/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.ObjectDetail}
 
 /**
  * Detailed listing of a commit.
diff --git a/resources/com/google/gitiles/templates/PathDetail.soy b/resources/com/google/gitiles/templates/PathDetail.soy
index fb0936b..3b5c9be 100644
--- a/resources/com/google/gitiles/templates/PathDetail.soy
+++ b/resources/com/google/gitiles/templates/PathDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.PathDetail}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as objDetail from 'com/google/gitiles/templates/ObjectDetail.soy';
diff --git a/resources/com/google/gitiles/templates/RefList.soy b/resources/com/google/gitiles/templates/RefList.soy
index 112573b..15b996a 100644
--- a/resources/com/google/gitiles/templates/RefList.soy
+++ b/resources/com/google/gitiles/templates/RefList.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.RefList}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 
@@ -60,7 +60,8 @@
 {template refList}
   {@param type: ?}  /** name of this type of refs, e.g. "Branches" */
   {@param refs: ?}  /** list of branch objects with url, name, and optional isHead keys. */
-  <div class="RefList">
+  {@param variant:= 'column'} /** style variant ("column" or "responsive"). **/
+  <div class="RefList RefList--{$variant}">
     <h3 class="RefList-title">{$type}</h3>
     <ul class="RefList-items">
     {for $ref in $refs}
diff --git a/resources/com/google/gitiles/templates/RepositoryIndex.soy b/resources/com/google/gitiles/templates/RepositoryIndex.soy
index ca83cab..ac1ab56 100644
--- a/resources/com/google/gitiles/templates/RepositoryIndex.soy
+++ b/resources/com/google/gitiles/templates/RepositoryIndex.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.RepositoryIndex}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as refList from 'com/google/gitiles/templates/RefList.soy';
@@ -111,6 +111,7 @@
     {call refList.refList}
       {param type: 'Branches' /}
       {param refs: $branches /}
+      {param variant: 'responsive' /}
     {/call}
     {if $moreBranchesUrl}
       <a href="{$moreBranchesUrl}">{msg desc="link to view more branches"}More...{/msg}</a>
@@ -128,6 +129,7 @@
     {call refList.refList}
       {param type: 'Tags' /}
       {param refs: $tags /}
+      {param variant: 'responsive' /}
     {/call}
     {if $moreTagsUrl}
       <a href="{$moreTagsUrl}">{msg desc="link to view more tags"}More...{/msg}</a>
diff --git a/resources/com/google/gitiles/templates/RevisionDetail.soy b/resources/com/google/gitiles/templates/RevisionDetail.soy
index e520990..7c1f1fd 100644
--- a/resources/com/google/gitiles/templates/RevisionDetail.soy
+++ b/resources/com/google/gitiles/templates/RevisionDetail.soy
@@ -11,7 +11,7 @@
 // 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.
-{namespace gitiles}
+{namespace com.google.gitiles.templates.RevisionDetail}
 
 import * as common from 'com/google/gitiles/templates/Common.soy';
 import * as objDetail from 'com/google/gitiles/templates/ObjectDetail.soy';
diff --git a/tools/BUILD b/tools/BUILD
index 5cfe48c..ec76f1b 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -1,14 +1,13 @@
 load(
     "@bazel_tools//tools/jdk:default_java_toolchain.bzl",
-    "JDK9_JVM_OPTS",
     "default_java_toolchain",
 )
 load("@rules_java//java:defs.bzl", "java_package_configuration")
 
 default_java_toolchain(
-    name = "error_prone_warnings_toolchain",
-    bootclasspath = ["@bazel_tools//tools/jdk:platformclasspath.jar"],
-    jvm_opts = JDK9_JVM_OPTS,
+    name = "error_prone_warnings_toolchain_java11",
+    source_version = "11",
+    target_version = "11",
     package_configuration = [
         ":error_prone",
     ],
diff --git a/tools/maven/mvn.sh b/tools/maven/mvn.sh
index 9b05b6f..deda565 100755
--- a/tools/maven/mvn.sh
+++ b/tools/maven/mvn.sh
@@ -58,5 +58,5 @@
 ${BAZEL_CMD} build //tools/maven:gen_${command} || \
   { echo "${BAZEL_CMD} failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; }
 
-export OUTPUT_BASE=`bazel info output_base`
+export OUTPUT_BASE=`${BAZEL_CMD} info output_base`
 ./bazel-bin/tools/maven/${command}.sh
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
deleted file mode 100755
index 9cc40e9..0000000
--- a/tools/workspace-status.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-# This script will be run by bazel when the build process starts to
-# generate key-value information that represents the status of the
-# workspace. The output should be like
-#
-# KEY1 VALUE1
-# KEY2 VALUE2
-#
-# If the script exits with non-zero code, it's considered as a failure
-# and the output will be discarded.
-
-function rev() {
-  cd $1; git describe --always --match "v[0-9].*" --dirty
-}
-
-echo STABLE_BUILD_GITILES_LABEL $(rev .)
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
new file mode 100644
index 0000000..bd0689a
--- /dev/null
+++ b/tools/workspace_status.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+from __future__ import print_function
+import os
+import subprocess
+import sys
+
+ROOT = os.path.abspath(__file__)
+while not os.path.exists(os.path.join(ROOT, 'WORKSPACE')):
+    ROOT = os.path.dirname(ROOT)
+CMD = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+
+
+def revision(directory, parent):
+    try:
+        os.chdir(directory)
+        return subprocess.check_output(CMD).strip().decode("utf-8")
+    except OSError as err:
+        print('could not invoke git: %s' % err, file=sys.stderr)
+        sys.exit(1)
+    except subprocess.CalledProcessError as err:
+        # ignore "not a git repository error" to report unknown version
+        return None
+    finally:
+        os.chdir(parent)
+
+
+print("STABLE_BUILD_GITILES_LABEL %s" % revision(ROOT, ROOT))
+for kind in ['modules']:
+    kind_dir = os.path.join(ROOT, kind)
+    for d in os.listdir(kind_dir):
+        p = os.path.join(kind_dir, d)
+        if os.path.isdir(p):
+            v = revision(p, ROOT)
+            print('STABLE_BUILD_%s_LABEL %s' % (os.path.basename(p).upper(),
+                                                v if v else 'unknown'))
diff --git a/version.bzl b/version.bzl
index 3c1285c..bf08533 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,7 +1,6 @@
-# Maven style API version (e.g. '2.x-SNAPSHOT').
+# Maven style API version (e.g. '1.0.0', '1.1.0-SNAPSHOT').
 #
-# Used by :install and :deploy when talking to the destination repository. As
-# we currently have no stable releases, we use the "build number" scheme
-# described at:
-# https://www.mojohaus.org/versions-maven-plugin/version-rules.html
-GITILES_VERSION = "0.4-1"
+# Used by :install and :deploy when talking to the destination repository.
+# Project uses semantic versioning described at:
+# https://semver.org
+GITILES_VERSION = "1.0.0"