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);
}