Use HostIndex to display subtrees of repositories If a repository does not exist try to list the repositories that use that prefix, or 404 if the GitilesAccess instance returns no matches. This allows listing a subtree of repositories without needing to build up the entire HostIndex result set. Change-Id: Ie3e046101919b6bedcc26198e455dface881315b
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 82cdde0..ed9d203 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
@@ -277,7 +277,8 @@ public GitilesAccess forRequest(HttpServletRequest req) { return new GitilesAccess() { @Override - public Map<String, RepositoryDescription> listRepositories(Set<String> branches) { + public Map<String, RepositoryDescription> listRepositories( + String prefix, Set<String> branches) { return Collections.emptyMap(); }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java index d435289..5fda0df 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; @@ -105,10 +106,10 @@ } @Override - public Map<String, RepositoryDescription> listRepositories(Set<String> branches) - throws IOException { + public Map<String, RepositoryDescription> listRepositories(String prefix, + Set<String> branches) throws IOException { Map<String, RepositoryDescription> repos = Maps.newTreeMap(US_COLLATOR); - for (Repository repo : scanRepositories(basePath, req)) { + for (Repository repo : scanRepositories(basePath, prefix, req)) { repos.put(getRepositoryName(repo), buildDescription(repo, branches)); repo.close(); } @@ -219,15 +220,10 @@ return "refs/heads/" + name; } - private Collection<Repository> scanRepositories(final File basePath, final HttpServletRequest req) - throws IOException { + private Collection<Repository> scanRepositories(File basePath, String prefix, + HttpServletRequest req) throws IOException { List<Repository> repos = Lists.newArrayList(); - Queue<File> todo = Queues.newArrayDeque(); - File[] baseFiles = basePath.listFiles(); - if (baseFiles == null) { - throw new IOException("base path is not a directory: " + basePath.getPath()); - } - Collections.addAll(todo, baseFiles); + Queue<File> todo = initScan(basePath, prefix); while (!todo.isEmpty()) { File file = todo.remove(); try { @@ -243,4 +239,28 @@ } return repos; } + + private Queue<File> initScan(File basePath, String prefix) + throws IOException { + Queue<File> todo = Queues.newArrayDeque(); + File[] entries; + if (isValidPrefix(prefix)) { + entries = new File(basePath, CharMatcher.is('/').trimFrom(prefix)).listFiles(); + } else { + entries = basePath.listFiles(); + } + if (entries != null) { + Collections.addAll(todo, entries); + } else if (!basePath.isDirectory()) { + throw new IOException("base path is not a directory: " + basePath.getPath()); + } + return todo; + } + + private static boolean isValidPrefix(String prefix) { + return !Strings.isNullOrEmpty(prefix) + && !prefix.equals(".") && !prefix.equals("..") + && !prefix.contains("../") + && !prefix.endsWith("/.."); + } }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java index 4572033..6434e55 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java
@@ -22,6 +22,7 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; /** @@ -39,16 +40,22 @@ /** * List repositories on the host. * + * @param prefix repository base path to list. Trailing "/" is implicitly + * added if missing. Null or empty string will match all repositories. * @param branches branches to list along with each repository. * @return map of repository names to descriptions. * @throws ServiceNotEnabledException to trigger an HTTP 403 Forbidden - * (matching behavior in {@link org.eclipse.jgit.http.server.RepositoryFilter}). + * (matching behavior in + * {@link org.eclipse.jgit.http.server.RepositoryFilter}). * @throws ServiceNotAuthorizedException to trigger an HTTP 401 Unauthorized - * (matching behavior in {@link org.eclipse.jgit.http.server.RepositoryFilter}). + * (matching behavior in + * {@link org.eclipse.jgit.http.server.RepositoryFilter}). * @throws IOException if an error occurred. */ - public Map<String, RepositoryDescription> listRepositories(Set<String> branches) - throws ServiceNotEnabledException, ServiceNotAuthorizedException, IOException; + public Map<String, RepositoryDescription> listRepositories( + @Nullable String prefix, Set<String> branches) + throws ServiceNotEnabledException, ServiceNotAuthorizedException, + IOException; /** * @return an opaque object that uniquely identifies the end-user making the
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 2734380..d1960b8 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.gitiles.GitilesServlet.STATIC_PREFIX; -import static com.google.gitiles.ViewFilter.getRegexGroup; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.FluentIterable; @@ -33,17 +32,11 @@ import com.google.gitiles.doc.DocServlet; import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.http.server.RepositoryFilter; import org.eclipse.jgit.http.server.glue.MetaFilter; import org.eclipse.jgit.http.server.glue.ServletBinder; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.resolver.FileResolver; import org.eclipse.jgit.transport.resolver.RepositoryResolver; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import java.io.File; import java.io.IOException; @@ -195,7 +188,7 @@ this.blameCache = blameCache; this.gitwebRedirect = gitwebRedirect; if (resolver != null) { - this.resolver = wrapResolver(resolver); + this.resolver = resolver; } } @@ -285,19 +278,6 @@ checkState(!initialized, "Gitiles already initialized"); } - private static RepositoryResolver<HttpServletRequest> wrapResolver( - final RepositoryResolver<HttpServletRequest> resolver) { - checkNotNull(resolver, "resolver"); - return new RepositoryResolver<HttpServletRequest>() { - @Override - public Repository open(HttpServletRequest req, String name) - throws RepositoryNotFoundException, ServiceNotAuthorizedException, - ServiceNotEnabledException, ServiceMayNotContinueException { - return resolver.open(req, ViewFilter.trimLeadingSlash(getRegexGroup(req, 1))); - } - }; - } - private synchronized Linkifier linkifier() { if (linkifier == null) { checkState(urls != null, "GitilesUrls not yet set"); @@ -362,7 +342,7 @@ FileResolver<HttpServletRequest> fileResolver; if (resolver == null) { fileResolver = new FileResolver<>(new File(basePath), exportAll); - resolver = wrapResolver(fileResolver); + resolver = fileResolver; } else if (resolver instanceof FileResolver) { fileResolver = (FileResolver<HttpServletRequest>) resolver; } else {
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 54cf377..0e7dc59 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -23,7 +23,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -36,6 +38,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; @@ -87,6 +90,7 @@ private String hostName; private String servletPath; + private String repositoryPrefix; private String repositoryName; private Revision revision = Revision.NULL; private Revision oldRevision = Revision.NULL; @@ -106,6 +110,9 @@ hostName = other.hostName; servletPath = other.servletPath; switch (type) { + case HOST_INDEX: + repositoryPrefix = other.repositoryPrefix; + break; case LOG: case DIFF: oldRevision = other.oldRevision; @@ -161,6 +168,19 @@ return servletPath; } + public Builder setRepositoryPrefix(String prefix) { + switch (type) { + case HOST_INDEX: + this.repositoryPrefix = prefix != null + ? Strings.emptyToNull(maybeTrimLeadingAndTrailingSlash(prefix)) + : null; + return this; + default: + throw new IllegalStateException( + String.format("cannot set repository prefix on %s view", type)); + } + } + public Builder setRepositoryName(String repositoryName) { switch (type) { case HOST_INDEX: @@ -342,8 +362,9 @@ checkRootedDoc(); break; } - return new GitilesView(type, hostName, servletPath, repositoryName, revision, - oldRevision, path, extension, params, anchor); + return new GitilesView(type, hostName, servletPath, repositoryPrefix, + repositoryName, revision, oldRevision, path, extension, params, + anchor); } public String toUrl() { @@ -470,6 +491,7 @@ private final Type type; private final String hostName; private final String servletPath; + private final String repositoryPrefix; private final String repositoryName; private final Revision revision; private final Revision oldRevision; @@ -481,6 +503,7 @@ private GitilesView(Type type, String hostName, String servletPath, + String repositoryPrefix, String repositoryName, Revision revision, Revision oldRevision, @@ -491,6 +514,7 @@ this.type = type; this.hostName = hostName; this.servletPath = servletPath; + this.repositoryPrefix = repositoryPrefix; this.repositoryName = repositoryName; this.revision = firstNonNull(revision, Revision.NULL); this.oldRevision = firstNonNull(oldRevision, Revision.NULL); @@ -516,6 +540,10 @@ return servletPath; } + public String getRepositoryPrefix() { + return repositoryPrefix; + } + public String getRepositoryName() { return repositoryName; } @@ -574,6 +602,7 @@ .omitNullValues() .add("host", hostName) .add("servlet", servletPath) + .add("prefix", repositoryPrefix) .add("repo", repositoryName) .add("rev", revision) .add("old", oldRevision) @@ -592,8 +621,11 @@ ListMultimap<String, String> params = this.params; switch (type) { case HOST_INDEX: + if (repositoryPrefix != null) { + url.append(repositoryPrefix).append('/'); + } params = LinkedListMultimap.create(); - if (!this.params.containsKey("format")) { + if (repositoryPrefix == null && !this.params.containsKey("format")) { params.put("format", FormatType.HTML.toString()); } params.putAll(this.params); @@ -712,9 +744,11 @@ "hasSingleTree must be null for %s view", type); String path = this.path; ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder(); - breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this))); - if (repositoryName != null) { - breadcrumbs.add(breadcrumb(repositoryName, repositoryIndex().copyFrom(this))); + breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this).setRepositoryPrefix(null))); + if (repositoryPrefix != null) { + breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryPrefix)); + } else if (repositoryName != null) { + breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryName)); } if (type == Type.DIFF) { // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render @@ -762,6 +796,18 @@ return breadcrumbs.build(); } + private List<Map<String, String>> hostIndexBreadcrumbs(String name) { + List<String> parts = Splitter.on('/').splitToList(name); + List<Map<String, String>> r = new ArrayList<>(parts.size()); + for (int i = 0; i < parts.size(); i++) { + String prefix = Joiner.on('/').join(parts.subList(0, i + 1)); + r.add(breadcrumb( + parts.get(i), + hostIndex().copyFrom(this).setRepositoryPrefix(prefix))); + } + return r; + } + private static Map<String, String> breadcrumb(String text, Builder url) { return ImmutableMap.of("text", text, "url", url.toUrl()); }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java index 2d52359..aa2208a 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.google.gson.reflect.TypeToken; +import com.google.template.soy.data.SoyData; import com.google.template.soy.data.SoyListData; import com.google.template.soy.data.SoyMapData; @@ -37,9 +38,12 @@ import java.io.IOException; import java.io.Writer; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -56,16 +60,12 @@ this.urls = checkNotNull(urls, "urls"); } - private Map<String, RepositoryDescription> getDescriptions(HttpServletRequest req, - HttpServletResponse res) throws IOException { - return getDescriptions(req, res, parseShowBranch(req)); - } - - private Map<String, RepositoryDescription> getDescriptions(HttpServletRequest req, - HttpServletResponse res, Set<String> branches) throws IOException { + private Map<String, RepositoryDescription> list( + HttpServletRequest req, HttpServletResponse res, String prefix, + Set<String> branches) throws IOException { Map<String, RepositoryDescription> descs; try { - descs = getAccess(req).listRepositories(branches); + descs = getAccess(req).listRepositories(prefix, branches); } catch (RepositoryNotFoundException e) { res.sendError(SC_NOT_FOUND); return null; @@ -85,12 +85,17 @@ res.sendError(SC_SERVICE_UNAVAILABLE); return null; } + if (prefix != null && descs.isEmpty()) { + res.sendError(SC_NOT_FOUND); + return null; + } return descs; } - private SoyMapData toSoyMapData(RepositoryDescription desc, GitilesView view) { + private SoyMapData toSoyMapData(RepositoryDescription desc, + @Nullable String prefix, GitilesView view) { return new SoyMapData( - "name", desc.name, + "name", stripPrefix(prefix, desc.name), "description", Strings.nullToEmpty(desc.description), "url", GitilesView.repositoryIndex() .copyFrom(view) @@ -100,25 +105,37 @@ @Override protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException { - Map<String, RepositoryDescription> descs = getDescriptions(req, res); + GitilesView view = ViewFilter.getView(req); + String prefix = view.getRepositoryPrefix(); + Map<String, RepositoryDescription> descs = list(req, res, prefix, parseShowBranch(req)); if (descs == null) { return; } + SoyListData repos = new SoyListData(); for (RepositoryDescription desc : descs.values()) { - repos.add(toSoyMapData(desc, ViewFilter.getView(req))); + repos.add(toSoyMapData(desc, prefix, view)); } + String hostName = urls.getHostName(req); + List<Map<String, String>> breadcrumbs = null; + if (prefix != null) { + hostName = hostName + '/' + prefix; + breadcrumbs = view.getBreadcrumbs(); + } renderHtml(req, res, "gitiles.hostIndex", ImmutableMap.of( - "hostName", urls.getHostName(req), + "hostName", hostName, + "breadcrumbs", SoyData.createFromExistingData(breadcrumbs), "baseUrl", urls.getBaseGitUrl(req), + "prefix", prefix != null ? prefix + '/' : "", "repositories", repos)); } @Override protected void doGetText(HttpServletRequest req, HttpServletResponse res) throws IOException { + String prefix = ViewFilter.getView(req).getRepositoryPrefix(); Set<String> branches = parseShowBranch(req); - Map<String, RepositoryDescription> descs = getDescriptions(req, res, branches); + Map<String, RepositoryDescription> descs = list(req, res, prefix, branches); if (descs == null) { return; } @@ -134,7 +151,7 @@ writer.write(ref); writer.write(' '); } - writer.write(GitilesUrls.NAME_ESCAPER.apply(repo.name)); + writer.write(GitilesUrls.NAME_ESCAPER.apply(stripPrefix(prefix, repo.name))); writer.write('\n'); } writer.flush(); @@ -143,13 +160,25 @@ @Override protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException { - Map<String, RepositoryDescription> descs = getDescriptions(req, res); + String prefix = ViewFilter.getView(req).getRepositoryPrefix(); + Map<String, RepositoryDescription> descs = list(req, res, prefix, parseShowBranch(req)); if (descs == null) { return; } + if (prefix != null) { + Map<String, RepositoryDescription> r = new LinkedHashMap<>(); + for (Map.Entry<String, RepositoryDescription> e : descs.entrySet()) { + r.put(stripPrefix(prefix, e.getKey()), e.getValue()); + } + descs = r; + } renderJson(req, res, descs, new TypeToken<Map<String, RepositoryDescription>>() {}.getType()); } + private static String stripPrefix(@Nullable String prefix, String name) { + return prefix != null ? name.substring(prefix.length() + 1) : name; + } + private static Set<String> parseShowBranch(HttpServletRequest req) { // Roughly match Gerrit Code Review's /projects/ API by supporting // both show-branch and b as query parameters.
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryFilter.java new file mode 100644 index 0000000..6eb5033 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryFilter.java
@@ -0,0 +1,69 @@ +// 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 com.google.common.base.Preconditions.checkNotNull; +import static com.google.gitiles.ViewFilter.getRegexGroup; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError; +import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_REPOSITORY; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.resolver.RepositoryResolver; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +class RepositoryFilter extends AbstractHttpFilter { + private final RepositoryResolver<HttpServletRequest> resolver; + + RepositoryFilter(RepositoryResolver<HttpServletRequest> resolver) { + this.resolver = checkNotNull(resolver, "resolver"); + } + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, + FilterChain chain) throws IOException, ServletException { + try { + String repo = ViewFilter.trimLeadingSlash(getRegexGroup(req, 1)); + try (Repository git = resolver.open(req, repo)) { + req.setAttribute(ATTRIBUTE_REPOSITORY, git); + chain.doFilter(req, res); + } catch (RepositoryNotFoundException e) { + // Drop through the rest of the chain. ViewFilter will pass this + // to HostIndexServlet which will attempt to list repositories + // or send SC_NOT_FOUND there. + chain.doFilter(req, res); + } catch (ServiceMayNotContinueException e) { + sendError(req, res, SC_FORBIDDEN, e.getMessage()); + } finally { + req.removeAttribute(ATTRIBUTE_REPOSITORY); + } + } catch (ServiceNotEnabledException e) { + sendError(req, res, SC_FORBIDDEN); + } catch (ServiceNotAuthorizedException e) { + res.sendError(SC_UNAUTHORIZED); + } + } +}
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 390a271..9792270 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; +import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_REPOSITORY; import com.google.common.base.Strings; @@ -152,7 +153,7 @@ String path = getRegexGroup(req, 3); if (command.isEmpty()) { - return parseNoCommand(repoName); + return parseNoCommand(req, repoName); } else if (command.equals(CMD_ARCHIVE)) { return parseArchiveCommand(req, repoName, path); } else if (command.equals(CMD_AUTO)) { @@ -176,7 +177,11 @@ } } - private GitilesView.Builder parseNoCommand(String repoName) { + private GitilesView.Builder parseNoCommand(HttpServletRequest req, + String repoName) { + if (req.getAttribute(ATTRIBUTE_REPOSITORY) == null) { + return GitilesView.hostIndex().setRepositoryPrefix(repoName); + } return GitilesView.repositoryIndex().setRepositoryName(repoName); }
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy index e0ed398..ec9553d 100644 --- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy
@@ -19,31 +19,36 @@ * @param hostName host name. * @param? menuEntries menu entries. * @param? headerVariant variant name for custom header. + * @param? prefix prefix path for matching repositories. + * @param? breadcrumbs map of breadcrumbs for header. * @param baseUrl base URL for repositories. * @param repositories list of repository description maps with name, cloneUrl, * and optional description values. */ {template .hostIndex} {call .header} - {param title: $hostName ? $hostName + ' Git repositories' : 'Git repositories' /} + {param title: $prefix ? $prefix : $hostName ? $hostName + ' Git repositories' : 'Git repositories' /} {param menuEntries: $menuEntries /} - {param breadcrumbs: null /} + {param breadcrumbs: $breadcrumbs /} {param headerVariant: $headerVariant /} {/call} {if length($repositories)} - - <h2> - {msg desc="Git repositories available on the host"} - {$hostName} Git repositories - {/msg} - </h2> + {if not $breadcrumbs} + <h2> + {msg desc="Git repositories available on the host"} + {$hostName} Git repositories + {/msg} + </h2> + {else} + <br /> + {/if} <div class="instructions"> {msg desc="description on how to use this repository"} To clone one of these repositories, install{sp} <a href="http://www.git-scm.com/">git</a>, and run: - <pre>git clone {$baseUrl}<em>name</em></pre> + <pre>git clone {$baseUrl}{$prefix}<em>name</em></pre> {/msg} </div>
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 9197c0c..646024d 100644 --- a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java +++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
@@ -72,6 +72,35 @@ } @Test + public void hostIndexOneComponentPrefix() throws Exception { + GitilesView view = GitilesView.hostIndex() + .copyFrom(HOST) + .setRepositoryPrefix("foo") + .build(); + + assertEquals("/b/foo/", view.toUrl()); + assertEquals(ImmutableList.of( + ImmutableMap.of("text", "host", "url", "/b/?format=HTML"), + ImmutableMap.of("text", "foo", "url", "/b/foo/")), + view.getBreadcrumbs()); + } + + @Test + public void hostIndexTwoComponentPrefix() throws Exception { + GitilesView view = GitilesView.hostIndex() + .copyFrom(HOST) + .setRepositoryPrefix("foo/bar") + .build(); + + assertEquals("/b/foo/bar/", view.toUrl()); + assertEquals(ImmutableList.of( + ImmutableMap.of("text", "host", "url", "/b/?format=HTML"), + ImmutableMap.of("text", "foo", "url", "/b/foo/"), + ImmutableMap.of("text", "bar", "url", "/b/foo/bar/")), + view.getBreadcrumbs()); + } + + @Test public void queryParams() throws Exception { GitilesView view = GitilesView.log().copyFrom(HOST) .setRepositoryName("repo") @@ -133,7 +162,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/")), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/")), view.getBreadcrumbs()); } @@ -156,7 +186,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/")), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/")), view.getBreadcrumbs()); } @@ -182,7 +213,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master")), view.getBreadcrumbs()); } @@ -227,7 +259,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/")), view.getBreadcrumbs()); @@ -256,7 +289,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/"), breadcrumb("file", "/b/foo/bar/+/master/file")), @@ -368,7 +402,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/"), breadcrumb("path", "/b/foo/bar/+/master/path"), @@ -404,7 +439,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master^!", "/b/foo/bar/+/master%5E%21/"), breadcrumb(".", "/b/foo/bar/+/master%5E%21/"), breadcrumb("path", "/b/foo/bar/+/master%5E%21/path"), @@ -438,7 +474,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master^!", "/b/foo/bar/+/master%5E%21/"), breadcrumb(".", "/b/foo/bar/+/master%5E%21/"), breadcrumb("path", "/b/foo/bar/+/master%5E%21/path"), @@ -474,7 +511,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("efab5678..master", "/b/foo/bar/+/efab5678..master/"), breadcrumb(".", "/b/foo/bar/+/efab5678..master/"), breadcrumb("path", "/b/foo/bar/+/efab5678..master/path"), @@ -507,7 +545,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+log/master")), view.getBreadcrumbs()); } @@ -535,7 +574,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("abcd1234", "/b/foo/bar/+log/abcd1234")), view.getBreadcrumbs()); } @@ -564,7 +604,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+log/master"), breadcrumb("path", "/b/foo/bar/+log/master/path"), breadcrumb("to", "/b/foo/bar/+log/master/path/to"), @@ -599,7 +640,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master^..master", "/b/foo/bar/+log/master%5E..master"), breadcrumb("path", "/b/foo/bar/+log/master%5E..master/path"), breadcrumb("to", "/b/foo/bar/+log/master%5E..master/path/to"), @@ -627,7 +669,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("HEAD", "/b/foo/bar/+log")), view.getBreadcrumbs()); } @@ -700,7 +743,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/"), breadcrumb("dir", "/b/foo/bar/+/master/dir"), @@ -770,7 +814,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/"), breadcrumb("path", "/b/foo/bar/+/master/path"), @@ -781,7 +826,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/"), breadcrumb("path", "/b/foo/bar/+/master/path?autodive=0"), @@ -805,7 +851,8 @@ assertEquals( ImmutableList.of( breadcrumb("host", "/b/?format=HTML"), - breadcrumb("foo/bar", "/b/foo/bar/"), + breadcrumb("foo", "/b/foo/"), + breadcrumb("bar", "/b/foo/bar/"), breadcrumb("master", "/b/foo/bar/+/master"), breadcrumb(".", "/b/foo/bar/+/master/")), view.getBreadcrumbs(ImmutableList.<Boolean> of()));
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java new file mode 100644 index 0000000..f2e03e9 --- /dev/null +++ b/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java
@@ -0,0 +1,157 @@ +// Copyright (C) 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 com.google.gitiles.TestGitilesUrls.URLS; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; + +import com.google.gson.reflect.TypeToken; +import com.google.template.soy.data.SoyListData; +import com.google.template.soy.data.SoyMapData; +import com.google.template.soy.data.restricted.NullData; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepository; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +/** Tests for {@link HostIndexServlet}. */ +public class HostIndexServletTest extends ServletTest { + private static final String NAME = "foo/bar/repo"; + + @Override + @Before + public void setUp() throws Exception { + repo = new TestRepository<DfsRepository>( + new InMemoryRepository(new DfsRepositoryDescription(NAME))); + servlet = TestGitilesServlet.create(repo); + } + + @Test + public void rootHtml() throws Exception { + Map<String, ?> data = buildData("/"); + assertEquals(URLS.getHostName(null), data.get("hostName")); + assertSame(NullData.INSTANCE, data.get("breadcrumbs")); + assertEquals("", data.get("prefix")); + + SoyListData repos = (SoyListData) data.get("repositories"); + assertEquals(1, repos.length()); + + SoyMapData ent = (SoyMapData) repos.get(0); + assertEquals(NAME, ent.get("name").toString()); + assertEquals("/b/" + NAME + "/", ent.get("url").toString()); + } + + @Test + public void fooSubdirHtml() throws Exception { + Map<String, ?> data = buildData("/foo/"); + assertEquals(URLS.getHostName(null) + "/foo", data.get("hostName")); + assertEquals("foo/", data.get("prefix")); + + SoyListData breadcrumbs = (SoyListData) data.get("breadcrumbs"); + assertEquals(2, breadcrumbs.length()); + + SoyListData repos = (SoyListData) data.get("repositories"); + assertEquals(1, repos.length()); + + SoyMapData ent = (SoyMapData) repos.get(0); + assertEquals("bar/repo", ent.get("name").toString()); + assertEquals("/b/" + NAME + "/", ent.get("url").toString()); + } + + @Test + public void fooBarSubdirHtml() throws Exception { + Map<String, ?> data = buildData("/foo/bar/"); + assertEquals(URLS.getHostName(null) + "/foo/bar", data.get("hostName")); + assertEquals("foo/bar/", data.get("prefix")); + + SoyListData breadcrumbs = (SoyListData) data.get("breadcrumbs"); + assertEquals(3, breadcrumbs.length()); + + SoyListData repos = (SoyListData) data.get("repositories"); + assertEquals(1, repos.length()); + + SoyMapData ent = (SoyMapData) repos.get(0); + assertEquals("repo", ent.get("name").toString()); + assertEquals("/b/" + NAME + "/", ent.get("url").toString()); + } + + @Test + public void rootText() throws Exception { + String name = repo.getRepository().getDescription().getRepositoryName(); + FakeHttpServletResponse res = buildText("/"); + assertEquals(name + "\n", new String(res.getActualBody(), UTF_8)); + } + + @Test + public void fooSubdirText() throws Exception { + FakeHttpServletResponse res = buildText("/foo/"); + assertEquals("bar/repo\n", new String(res.getActualBody(), UTF_8)); + } + + @Test + public void fooBarSubdirText() throws Exception { + FakeHttpServletResponse res = buildText("/foo/bar/"); + assertEquals("repo\n", new String(res.getActualBody(), UTF_8)); + } + + @Test + public void rootJson() throws Exception { + String name = repo.getRepository().getDescription().getRepositoryName(); + Map<String, RepositoryDescription> res = buildJson( + "/", + new TypeToken<Map<String, RepositoryDescription>>() {}.getType()); + + assertEquals(1, res.size()); + RepositoryDescription d = res.get(name); + assertNotNull(name + " exists", d); + assertEquals(name, d.name); + } + + @Test + public void fooSubdirJson() throws Exception { + Map<String, RepositoryDescription> res = buildJson( + "/foo/", + new TypeToken<Map<String, RepositoryDescription>>() {}.getType()); + + assertEquals(1, res.size()); + RepositoryDescription d = res.get("bar/repo"); + assertNotNull("bar/repo exists", d); + assertEquals(repo.getRepository().getDescription().getRepositoryName(), d.name); + } + + @Test + public void fooBarSubdirJson() throws Exception { + Map<String, RepositoryDescription> res = buildJson( + "/foo/bar/", + new TypeToken<Map<String, RepositoryDescription>>() {}.getType()); + + assertEquals(1, res.size()); + RepositoryDescription d = res.get("repo"); + assertNotNull("repo exists", d); + assertEquals(repo.getRepository().getDescription().getRepositoryName(), d.name); + } + + @Test + public void emptySubdirectoryList() throws Exception { + assertNotFound("/no.repos/", null); + } +}
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java index 50877ba..2a6eb87 100644 --- a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java +++ b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
@@ -16,11 +16,13 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableMap; import org.eclipse.jgit.internal.storage.dfs.DfsRepository; import org.eclipse.jgit.lib.Config; +import java.util.Collections; import java.util.Map; import java.util.Set; @@ -38,12 +40,20 @@ public GitilesAccess forRequest(final HttpServletRequest req) { return new GitilesAccess() { @Override - public Map<String, RepositoryDescription> listRepositories(Set<String> branches) { + public Map<String, RepositoryDescription> listRepositories(String prefix, + Set<String> branches) { + String name = repo.getDescription().getRepositoryName(); + if (prefix != null) { + String pattern = CharMatcher.is('/').trimFrom(prefix) + '/'; + if (!name.startsWith(pattern)) { + return Collections.emptyMap(); + } + } if (branches != null && !branches.isEmpty()) { throw new UnsupportedOperationException("branches set not yet supported"); } RepositoryDescription desc = new RepositoryDescription(); - desc.name = repo.getDescription().getRepositoryName(); + desc.name = name; desc.cloneUrl = TestGitilesUrls.URLS.getBaseGitUrl(req) + "/" + desc.name; return ImmutableMap.of(desc.name, desc); }