blob: aabda3dd7187739c8b9ab379a2ae553e17ae2889 [file] [log] [blame]
// 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("/..");
}
}