| // Copyright (C) 2008 The Android Open Source Project |
| // |
| // 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.gerrit.server.git; |
| |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.Project.NameKey; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.events.LifecycleListener; |
| import com.google.gerrit.lifecycle.LifecycleModule; |
| import com.google.gerrit.server.cache.PerThreadRefDbCache; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.FileVisitOption; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.Map; |
| import java.util.NavigableSet; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ConfigConstants; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.RepositoryCache; |
| import org.eclipse.jgit.lib.RepositoryCache.FileKey; |
| import org.eclipse.jgit.lib.RepositoryCacheConfig; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.storage.file.WindowCacheConfig; |
| import org.eclipse.jgit.util.FS; |
| |
| /** Manages Git repositories stored on the local filesystem. */ |
| @Singleton |
| public class LocalDiskRepositoryManager implements GitRepositoryManager { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public static class LocalDiskRepositoryManagerModule extends LifecycleModule { |
| @Override |
| protected void configure() { |
| listener().to(LocalDiskRepositoryManager.Lifecycle.class); |
| } |
| } |
| |
| public static class Lifecycle implements LifecycleListener { |
| private final Config serverConfig; |
| |
| @Inject |
| Lifecycle(@GerritServerConfig Config cfg) { |
| this.serverConfig = cfg; |
| } |
| |
| @Override |
| public void start() { |
| RepositoryCacheConfig repoCacheCfg = new RepositoryCacheConfig(); |
| repoCacheCfg.fromConfig(serverConfig); |
| repoCacheCfg.install(); |
| |
| WindowCacheConfig cfg = new WindowCacheConfig(); |
| cfg.fromConfig(serverConfig); |
| if (serverConfig.getString("core", null, "streamFileThreshold") == null) { |
| long mx = Runtime.getRuntime().maxMemory(); |
| int limit = |
| (int) |
| Math.min( |
| mx / 4, // don't use more than 1/4 of the heap. |
| 2047 << 20); // cannot exceed array length |
| if ((5 << 20) < limit && limit % (1 << 20) != 0) { |
| // If the limit is at least 5 MiB but is not a whole multiple |
| // of MiB round up to the next one full megabyte. This is a very |
| // tiny memory increase in exchange for nice round units. |
| limit = ((limit / (1 << 20)) + 1) << 20; |
| } |
| |
| String desc; |
| if (limit % (1 << 20) == 0) { |
| desc = String.format("%dm", limit / (1 << 20)); |
| } else if (limit % (1 << 10) == 0) { |
| desc = String.format("%dk", limit / (1 << 10)); |
| } else { |
| desc = String.format("%d", limit); |
| } |
| logger.atInfo().log("Defaulting core.streamFileThreshold to %s", desc); |
| cfg.setStreamFileThreshold(limit); |
| } |
| cfg.install(); |
| } |
| |
| @Override |
| public void stop() {} |
| } |
| |
| private final Path basePath; |
| private final Map<Project.NameKey, FileKey> fileKeyByProject = new ConcurrentHashMap<>(); |
| private final boolean usePerRequestRefCache; |
| |
| @Inject |
| LocalDiskRepositoryManager(SitePaths site, @GerritServerConfig Config cfg) { |
| basePath = site.resolve(cfg.getString("gerrit", null, "basePath")); |
| if (basePath == null) { |
| throw new IllegalStateException("gerrit.basePath must be configured"); |
| } |
| usePerRequestRefCache = cfg.getBoolean("core", null, "usePerRequestRefCache", true); |
| } |
| |
| /** |
| * Return the basePath under which the specified project is stored. |
| * |
| * @param name the name of the project |
| * @return base directory |
| */ |
| public Path getBasePath(Project.NameKey name) { |
| return basePath; |
| } |
| |
| @Override |
| public Status getRepositoryStatus(NameKey name) { |
| if (isUnreasonableName(name)) { |
| return Status.NON_EXISTENT; |
| } |
| Path path = getBasePath(name); |
| File dir = FileKey.resolve(path.resolve(name.get()).toFile(), FS.DETECTED); |
| if (dir == null) { |
| return Status.NON_EXISTENT; |
| } |
| Repository repo; |
| try { |
| // Try to open with mustExist, so that it does not attempt to create a repository. |
| repo = RepositoryCache.open(FileKey.lenient(dir, FS.DETECTED), /*mustExist=*/ true); |
| } catch (RepositoryNotFoundException e) { |
| return Status.NON_EXISTENT; |
| } catch (IOException e) { |
| return Status.UNAVAILABLE; |
| } |
| // If object database does not exist, the repository is unusable |
| return repo.getObjectDatabase().exists() ? Status.ACTIVE : Status.UNAVAILABLE; |
| } |
| |
| @Override |
| public Repository openRepository(Project.NameKey name) throws RepositoryNotFoundException { |
| FileKey cachedLocation = fileKeyByProject.get(name); |
| if (cachedLocation != null) { |
| try { |
| return RepositoryCache.open(cachedLocation); |
| } catch (IOException e) { |
| fileKeyByProject.remove(name, cachedLocation); |
| } |
| } |
| |
| if (isUnreasonableName(name)) { |
| throw new RepositoryNotFoundException("Invalid name: " + name); |
| } |
| FileKey location = |
| usePerRequestRefCache |
| ? DynamicRefDbRepository.FileKey.lenient( |
| getBasePath(name).resolve(name.get()).toFile(), |
| FS.DETECTED, |
| (path, refDb) -> PerThreadRefDbCache.getRefDatabase(path, refDb)) |
| : FileKey.lenient(getBasePath(name).resolve(name.get()).toFile(), FS.DETECTED); |
| try { |
| Repository repo = RepositoryCache.open(location); |
| fileKeyByProject.put(name, location); |
| return repo; |
| } catch (IOException e) { |
| throw new RepositoryNotFoundException("Cannot open repository " + name, e); |
| } |
| } |
| |
| @Override |
| public Repository createRepository(Project.NameKey name) |
| throws RepositoryNotFoundException, RepositoryExistsException, IOException { |
| if (isUnreasonableName(name)) { |
| throw new RepositoryNotFoundException("Invalid name: " + name); |
| } |
| |
| Path path = getBasePath(name); |
| File dir = FileKey.resolve(path.resolve(name.get()).toFile(), FS.DETECTED); |
| if (dir != null) { |
| // Already exists on disk, use the repository we found. |
| // |
| Project.NameKey onDiskName = getProjectName(path, dir.getCanonicalFile().toPath()); |
| |
| if (!onDiskName.equals(name)) { |
| throw new RepositoryCaseMismatchException(name); |
| } |
| throw new RepositoryExistsException(name); |
| } |
| |
| // It doesn't exist under any of the standard permutations |
| // of the repository name, so prefer the standard bare name. |
| // |
| String n = name.get() + Constants.DOT_GIT_EXT; |
| FileKey loc = FileKey.exact(path.resolve(n).toFile(), FS.DETECTED); |
| |
| try { |
| Repository db = RepositoryCache.open(loc, false); |
| db.create(true /* bare */); |
| |
| StoredConfig config = db.getConfig(); |
| config.setBoolean( |
| ConfigConstants.CONFIG_CORE_SECTION, |
| null, |
| ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, |
| true); |
| config.save(); |
| |
| // JGit only writes to the reflog for refs/meta/config if the log file |
| // already exists. |
| // |
| File metaConfigLog = new File(db.getDirectory(), "logs/" + RefNames.REFS_CONFIG); |
| if (!metaConfigLog.getParentFile().mkdirs() || !metaConfigLog.createNewFile()) { |
| logger.atSevere().log( |
| "Failed to create ref log for %s in repository %s", RefNames.REFS_CONFIG, name); |
| } |
| |
| return db; |
| } catch (IOException e) { |
| throw new RepositoryNotFoundException("Cannot create repository " + name, e); |
| } |
| } |
| |
| @Override |
| public Boolean canPerformGC() { |
| return true; |
| } |
| |
| private boolean isUnreasonableName(Project.NameKey nameKey) { |
| final String name = nameKey.get(); |
| |
| return name.length() == 0 // no empty paths |
| || name.charAt(name.length() - 1) == '/' // no suffix |
| || name.indexOf('\\') >= 0 // no windows/dos style paths |
| || name.charAt(0) == '/' // no absolute paths |
| || new File(name).isAbsolute() // no absolute paths |
| || name.startsWith("../") // no "l../etc/passwd" |
| || name.contains("/../") // no "foo/../etc/passwd" |
| || name.contains("/./") // "foo/./foo" is insane to ask |
| || name.contains("//") // windows UNC path can be "//..." |
| || name.contains(".git/") // no path segments that end with '.git' as "foo.git/bar" |
| || name.contains("?") // common unix wildcard |
| || name.contains("%") // wildcard or string parameter |
| || name.contains("*") // wildcard |
| || name.contains(":") // Could be used for absolute paths in windows? |
| || name.contains("<") // redirect input |
| || name.contains(">") // redirect output |
| || name.contains("|") // pipe |
| || name.contains("$") // dollar sign |
| || name.contains("\r") // carriage return |
| || name.contains("/+") // delimiter in /changes/ |
| || name.contains("~"); // delimiter in /changes/ |
| } |
| |
| @Override |
| public NavigableSet<Project.NameKey> list() { |
| ProjectVisitor visitor = new ProjectVisitor(basePath); |
| scanProjects(visitor); |
| return Collections.unmodifiableNavigableSet(visitor.found); |
| } |
| |
| protected void scanProjects(ProjectVisitor visitor) { |
| try { |
| Files.walkFileTree( |
| visitor.startFolder, |
| EnumSet.of(FileVisitOption.FOLLOW_LINKS), |
| Integer.MAX_VALUE, |
| visitor); |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log( |
| "Error walking repository tree %s", visitor.startFolder.toAbsolutePath()); |
| } |
| } |
| |
| private static Project.NameKey getProjectName(Path startFolder, Path p) { |
| String projectName = startFolder.relativize(p).toString(); |
| if (File.separatorChar != '/') { |
| projectName = projectName.replace(File.separatorChar, '/'); |
| } |
| if (projectName.endsWith(Constants.DOT_GIT_EXT)) { |
| int newLen = projectName.length() - Constants.DOT_GIT_EXT.length(); |
| projectName = projectName.substring(0, newLen); |
| } |
| return Project.nameKey(projectName); |
| } |
| |
| protected class ProjectVisitor extends SimpleFileVisitor<Path> { |
| private final NavigableSet<Project.NameKey> found = new TreeSet<>(); |
| private Path startFolder; |
| |
| public ProjectVisitor(Path startFolder) { |
| setStartFolder(startFolder); |
| } |
| |
| public void setStartFolder(Path startFolder) { |
| this.startFolder = startFolder; |
| } |
| |
| @Override |
| public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) |
| throws IOException { |
| if (!dir.equals(startFolder) && isRepo(dir)) { |
| addProject(dir); |
| return FileVisitResult.SKIP_SUBTREE; |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult visitFileFailed(Path file, IOException e) { |
| logger.atWarning().log("%s", e.getMessage()); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| private boolean isRepo(Path p) { |
| String name = p.getFileName().toString(); |
| return !name.equals(Constants.DOT_GIT) |
| && (name.endsWith(Constants.DOT_GIT_EXT) |
| || FileKey.isGitRepository(p.toFile(), FS.DETECTED)); |
| } |
| |
| private void addProject(Path p) { |
| Project.NameKey nameKey = getProjectName(startFolder, p); |
| if (getBasePath(nameKey).equals(startFolder)) { |
| if (isUnreasonableName(nameKey)) { |
| logger.atWarning().log("Ignoring unreasonably named repository %s", p.toAbsolutePath()); |
| } else { |
| found.add(nameKey); |
| } |
| } |
| } |
| } |
| } |