| // Copyright 2012 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 java.nio.charset.StandardCharsets.UTF_8; |
| |
| 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; |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.Collator; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| import javax.servlet.http.HttpServletRequest; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.http.server.ServletUtils; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.transport.resolver.FileResolver; |
| import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; |
| import org.eclipse.jgit.util.IO; |
| |
| /** |
| * Default implementation of {@link GitilesAccess} with local repositories. |
| * |
| * <p>Repositories are scanned on-demand under the given path, configured by default from {@code |
| * gitiles.basePath}. There is no access control beyond what user the JVM is running under. |
| */ |
| public class DefaultAccess implements GitilesAccess { |
| private static final String ANONYMOUS_USER_KEY = "anonymous user"; |
| |
| private static final String DEFAULT_DESCRIPTION = |
| "Unnamed repository; edit this file 'description' to name the repository."; |
| |
| private static final Collator US_COLLATOR = Collator.getInstance(Locale.US); |
| |
| public static class Factory implements GitilesAccess.Factory { |
| private final File basePath; |
| private final String canonicalBasePath; |
| private final String baseGitUrl; |
| private final Config baseConfig; |
| private final FileResolver<HttpServletRequest> resolver; |
| |
| Factory( |
| File basePath, |
| String baseGitUrl, |
| Config baseConfig, |
| FileResolver<HttpServletRequest> resolver) |
| throws IOException { |
| this.basePath = checkNotNull(basePath, "basePath"); |
| this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); |
| this.baseConfig = checkNotNull(baseConfig, "baseConfig"); |
| this.resolver = checkNotNull(resolver, "resolver"); |
| this.canonicalBasePath = basePath.getCanonicalPath(); |
| } |
| |
| @Override |
| public GitilesAccess forRequest(HttpServletRequest req) { |
| return newAccess(basePath, canonicalBasePath, baseGitUrl, resolver, req); |
| } |
| |
| protected DefaultAccess newAccess( |
| File basePath, |
| String canonicalBasePath, |
| String baseGitUrl, |
| FileResolver<HttpServletRequest> resolver, |
| HttpServletRequest req) { |
| return new DefaultAccess(basePath, canonicalBasePath, baseGitUrl, baseConfig, resolver, req); |
| } |
| } |
| |
| protected final File basePath; |
| protected final String canonicalBasePath; |
| protected final String baseGitUrl; |
| protected final Config baseConfig; |
| protected final FileResolver<HttpServletRequest> resolver; |
| protected final HttpServletRequest req; |
| |
| protected DefaultAccess( |
| File basePath, |
| String canonicalBasePath, |
| String baseGitUrl, |
| Config baseConfig, |
| FileResolver<HttpServletRequest> resolver, |
| HttpServletRequest req) { |
| this.basePath = checkNotNull(basePath, "basePath"); |
| this.canonicalBasePath = checkNotNull(canonicalBasePath, "canonicalBasePath"); |
| this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); |
| this.baseConfig = checkNotNull(baseConfig, "baseConfig"); |
| this.resolver = checkNotNull(resolver, "resolver"); |
| this.req = checkNotNull(req, "req"); |
| } |
| |
| @Override |
| 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, prefix, req)) { |
| repos.put(getRepositoryName(repo), buildDescription(repo, branches)); |
| repo.close(); |
| } |
| return repos; |
| } |
| |
| @Override |
| public Object getUserKey() { |
| // Always return the same anonymous user key (effectively running with the |
| // same user permissions as the JVM). Subclasses may override this behavior. |
| return ANONYMOUS_USER_KEY; |
| } |
| |
| @Override |
| public String getRepositoryName() { |
| return getRepositoryName(ServletUtils.getRepository(req)); |
| } |
| |
| @Override |
| public RepositoryDescription getRepositoryDescription() throws IOException { |
| return buildDescription(ServletUtils.getRepository(req), Collections.<String>emptySet()); |
| } |
| |
| @Override |
| public Config getConfig() { |
| return baseConfig; |
| } |
| |
| private String getRepositoryName(Repository repo) { |
| String path = getRelativePath(repo); |
| if (repo.isBare() && path.endsWith(".git")) { |
| path = path.substring(0, path.length() - 4); |
| } |
| return path; |
| } |
| |
| private String getRelativePath(Repository repo) { |
| String path = repo.isBare() ? repo.getDirectory().getPath() : repo.getDirectory().getParent(); |
| if (repo.isBare()) { |
| path = repo.getDirectory().getPath(); |
| if (path.endsWith(".git")) { |
| path = path.substring(0, path.length() - 4); |
| } |
| } else { |
| path = repo.getDirectory().getParent(); |
| } |
| return getRelativePath(path); |
| } |
| |
| private String getRelativePath(String path) { |
| String base = basePath.getPath(); |
| if (path.equals(base)) { |
| return ""; |
| } |
| if (path.startsWith(base)) { |
| return path.substring(base.length() + 1); |
| } |
| if (path.startsWith(canonicalBasePath)) { |
| return path.substring(canonicalBasePath.length() + 1); |
| } |
| throw new IllegalStateException( |
| String.format("Repository path %s is outside base path %s", path, base)); |
| } |
| |
| private String loadDescriptionText(Repository repo) throws IOException { |
| String desc = null; |
| StoredConfig config = repo.getConfig(); |
| IOException configError = null; |
| try { |
| config.load(); |
| desc = config.getString("gitweb", null, "description"); |
| } catch (ConfigInvalidException e) { |
| configError = new IOException(e); |
| } |
| if (desc == null) { |
| File descFile = new File(repo.getDirectory(), "description"); |
| if (descFile.exists()) { |
| desc = new String(IO.readFully(descFile), UTF_8); |
| if (DEFAULT_DESCRIPTION.equals(CharMatcher.whitespace().trimFrom(desc))) { |
| desc = null; |
| } |
| } else if (configError != null) { |
| throw configError; |
| } |
| } |
| return desc; |
| } |
| |
| private RepositoryDescription buildDescription(Repository repo, Set<String> branches) |
| throws IOException { |
| RepositoryDescription desc = new RepositoryDescription(); |
| desc.name = getRepositoryName(repo); |
| desc.cloneUrl = baseGitUrl + getRelativePath(repo); |
| desc.description = loadDescriptionText(repo); |
| if (!branches.isEmpty()) { |
| desc.branches = Maps.newLinkedHashMap(); |
| for (String name : branches) { |
| Ref ref = repo.exactRef(normalizeRefName(name)); |
| if ((ref != null) && (ref.getObjectId() != null)) { |
| desc.branches.put(name, ref.getObjectId().name()); |
| } |
| } |
| } |
| return desc; |
| } |
| |
| private static String normalizeRefName(String name) { |
| if (name.startsWith("refs/")) { |
| return name; |
| } |
| return "refs/heads/" + name; |
| } |
| |
| private Collection<Repository> scanRepositories( |
| File basePath, String prefix, HttpServletRequest req) throws IOException { |
| List<Repository> repos = Lists.newArrayList(); |
| Queue<File> todo = initScan(basePath, prefix); |
| while (!todo.isEmpty()) { |
| File file = todo.remove(); |
| try { |
| repos.add(resolver.open(req, getRelativePath(file.getPath()))); |
| } catch (RepositoryNotFoundException e) { |
| File[] children = file.listFiles(); |
| if (children != null) { |
| Collections.addAll(todo, children); |
| } |
| } catch (ServiceNotEnabledException e) { |
| throw new IOException(e); |
| } |
| } |
| 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("/.."); |
| } |
| } |