From 16e4747d3cb2c2a53a6bef554bca306d8594a080 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 14 Sep 2012 17:51:04 -0400 Subject: [PATCH] Mostly complete blob view line links feature, DOM offset bug remains (issue 130) --- src/com/gitblit/GitBlit.java | 275 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 files changed, 251 insertions(+), 24 deletions(-) diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 0940c76..c758654 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -37,9 +37,9 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; +import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -58,6 +58,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.WindowCache; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.transport.ServiceMayNotContinueException; @@ -79,6 +80,7 @@ import com.gitblit.models.FederationProposal; import com.gitblit.models.FederationSet; import com.gitblit.models.Metric; +import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.SearchResult; import com.gitblit.models.ServerSettings; @@ -88,6 +90,8 @@ import com.gitblit.models.UserModel; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.ByteFormat; +import com.gitblit.utils.ContainerUtils; +import com.gitblit.utils.DeepCopier; import com.gitblit.utils.FederationUtils; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.JsonUtils; @@ -128,7 +132,9 @@ private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>(); - private final List<String> repositoryListCache = new CopyOnWriteArrayList<String>(); + private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>(); + + private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>(); private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>(""); @@ -151,6 +157,8 @@ private LuceneExecutor luceneExecutor; private TimeZone timezone; + + private FileBasedConfig projectConfigs; public GitBlit() { if (gitblit == null) { @@ -736,25 +744,32 @@ * * @param name */ - private void addToCachedRepositoryList(String name) { + private void addToCachedRepositoryList(String name, RepositoryModel model) { if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { - repositoryListCache.add(name); + repositoryListCache.put(name, model); } + } + + /** + * Removes the repository from the list of cached repositories. + * + * @param name + */ + private void removeFromCachedRepositoryList(String name) { + if (StringUtils.isEmpty(name)) { + return; + } + repositoryListCache.remove(name); } /** - * Clears all the cached data for the specified repository. + * Clears all the cached metadata for the specified repository. * * @param repositoryName - * @param isDeleted */ - private void clearRepositoryCache(String repositoryName, boolean isDeleted) { + private void clearRepositoryMetadataCache(String repositoryName) { repositorySizeCache.remove(repositoryName); repositoryMetricsCache.remove(repositoryName); - - if (isDeleted) { - repositoryListCache.remove(repositoryName); - } } /** @@ -831,10 +846,12 @@ calculateSize(model); } } + } else { + // update cache + for (String repository : repositories) { + getRepositoryModel(repository); + } } - - // update cache - repositoryListCache.addAll(repositories); long duration = System.currentTimeMillis() - startTime; logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration)); @@ -842,7 +859,7 @@ } // return sorted copy of cached list - List<String> list = new ArrayList<String>(repositoryListCache); + List<String> list = new ArrayList<String>(repositoryListCache.keySet()); StringUtils.sortRepositorynames(list); return list; } @@ -966,6 +983,203 @@ * @return repository model or null */ public RepositoryModel getRepositoryModel(String repositoryName) { + if (!repositoryListCache.containsKey(repositoryName)) { + RepositoryModel model = loadRepositoryModel(repositoryName); + if (model == null) { + return null; + } + addToCachedRepositoryList(repositoryName, model); + return model; + } + + // cached model + RepositoryModel model = repositoryListCache.get(repositoryName); + + // check for updates + Repository r = getRepository(repositoryName); + if (r == null) { + // repository is missing + removeFromCachedRepositoryList(repositoryName); + logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName)); + return null; + } + + FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r); + if (config.isOutdated()) { + // reload model + logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName)); + model = loadRepositoryModel(repositoryName); + removeFromCachedRepositoryList(repositoryName); + addToCachedRepositoryList(repositoryName, model); + } else { + // update a few repository parameters + if (!model.hasCommits) { + // update hasCommits, assume a repository only gains commits :) + model.hasCommits = JGitUtils.hasCommits(r); + } + + model.lastChange = JGitUtils.getLastChange(r); + } + r.close(); + + // return a copy of the cached model + return DeepCopier.copy(model); + } + + + /** + * Returns the map of project config. This map is cached and reloaded if + * the underlying projects.conf file changes. + * + * @return project config map + */ + private Map<String, ProjectModel> getProjectConfigs() { + if (projectConfigs.isOutdated()) { + + try { + projectConfigs.load(); + } catch (Exception e) { + } + + // project configs + String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main"); + ProjectModel rootProject = new ProjectModel(rootName, true); + + Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>(); + // cache the root project under its alias and an empty path + configs.put("", rootProject); + configs.put(rootProject.name.toLowerCase(), rootProject); + + for (String name : projectConfigs.getSubsections("project")) { + ProjectModel project; + if (name.equalsIgnoreCase(rootName)) { + project = rootProject; + } else { + project = new ProjectModel(name); + } + project.title = projectConfigs.getString("project", name, "title"); + project.description = projectConfigs.getString("project", name, "description"); + // TODO add more interesting metadata + // project manager? + // commit message regex? + // RW+ + // RW + // R + configs.put(name.toLowerCase(), project); + } + projectCache.clear(); + projectCache.putAll(configs); + } + return projectCache; + } + + /** + * Returns a list of project models for the user. + * + * @param user + * @return list of projects that are accessible to the user + */ + public List<ProjectModel> getProjectModels(UserModel user) { + Map<String, ProjectModel> configs = getProjectConfigs(); + + // per-user project lists, this accounts for security and visibility + Map<String, ProjectModel> map = new TreeMap<String, ProjectModel>(); + // root project + map.put("", configs.get("")); + + for (RepositoryModel model : getRepositoryModels(user)) { + String rootPath = StringUtils.getRootPath(model.name).toLowerCase(); + if (!map.containsKey(rootPath)) { + ProjectModel project; + if (configs.containsKey(rootPath)) { + // clone the project model because it's repository list will + // be tailored for the requesting user + project = DeepCopier.copy(configs.get(rootPath)); + } else { + project = new ProjectModel(rootPath); + } + map.put(rootPath, project); + } + map.get(rootPath).addRepository(model); + } + + // sort projects, root project first + List<ProjectModel> projects = new ArrayList<ProjectModel>(map.values()); + Collections.sort(projects); + projects.remove(map.get("")); + projects.add(0, map.get("")); + return projects; + } + + /** + * Returns the project model for the specified user. + * + * @param name + * @param user + * @return a project model, or null if it does not exist + */ + public ProjectModel getProjectModel(String name, UserModel user) { + for (ProjectModel project : getProjectModels(user)) { + if (project.name.equalsIgnoreCase(name)) { + return project; + } + } + return null; + } + + /** + * Returns a project model for the Gitblit/system user. + * + * @param name a project name + * @return a project model or null if the project does not exist + */ + public ProjectModel getProjectModel(String name) { + Map<String, ProjectModel> configs = getProjectConfigs(); + ProjectModel project = configs.get(name.toLowerCase()); + if (project == null) { + return null; + } + // clone the object + project = DeepCopier.copy(project); + String folder = name.toLowerCase() + "/"; + for (String repository : getRepositoryList()) { + if (repository.toLowerCase().startsWith(folder)) { + project.addRepository(repository); + } + } + return project; + } + + /** + * Workaround JGit. I need to access the raw config object directly in order + * to see if the config is dirty so that I can reload a repository model. + * If I use the stock JGit method to get the config it already reloads the + * config. If the config changes are made within Gitblit this is fine as + * the returned config will still be flagged as dirty. BUT... if the config + * is manipulated outside Gitblit then it fails to recognize this as dirty. + * + * @param r + * @return a config + */ + private StoredConfig getRepositoryConfig(Repository r) { + try { + Field f = r.getClass().getDeclaredField("repoConfig"); + f.setAccessible(true); + StoredConfig config = (StoredConfig) f.get(r); + return config; + } catch (Exception e) { + logger.error("Failed to retrieve \"repoConfig\" via reflection", e); + } + return r.getConfig(); + } + + /** + * Create a repository model from the configuration and repository data. + * + * @param repositoryName + * @return a repositoryModel or null if the repository does not exist + */ + private RepositoryModel loadRepositoryModel(String repositoryName) { Repository r = getRepository(repositoryName); if (r == null) { return null; @@ -1025,6 +1239,11 @@ * @return true if the repository exists */ public boolean hasRepository(String repositoryName) { + if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { + // if we are caching use the cache to determine availability + // otherwise we end up adding a phantom repository to the cache + return repositoryListCache.containsKey(repositoryName); + } Repository r = getRepository(repositoryName, false); if (r == null) { return false; @@ -1169,9 +1388,6 @@ // create repository logger.info("create repository " + repository.name); r = JGitUtils.createRepository(repositoriesFolder, repository.name); - - // add name to cache - addToCachedRepositoryList(repository.name); } else { // rename repository if (!repositoryName.equalsIgnoreCase(repository.name)) { @@ -1211,10 +1427,7 @@ } // clear the cache - clearRepositoryCache(repositoryName, true); - - // add new name to repository list cache - addToCachedRepositoryList(repository.name); + clearRepositoryMetadataCache(repositoryName); } // load repository @@ -1242,13 +1455,18 @@ repository.name, currentRef, repository.HEAD)); if (JGitUtils.setHEADtoRef(r, repository.HEAD)) { // clear the cache - clearRepositoryCache(repository.name, false); + clearRepositoryMetadataCache(repository.name); } } // close the repository object r.close(); } + + // update repository cache + removeFromCachedRepositoryList(repositoryName); + // model will actually be replaced on next load because config is stale + addToCachedRepositoryList(repository.name, repository); } /** @@ -1339,12 +1557,14 @@ try { closeRepository(repositoryName); // clear the repository cache - clearRepositoryCache(repositoryName, true); + clearRepositoryMetadataCache(repositoryName); + removeFromCachedRepositoryList(repositoryName); File folder = new File(repositoriesFolder, repositoryName); if (folder.exists() && folder.isDirectory()) { FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY); if (userService.deleteRepositoryRole(repositoryName)) { + logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName)); return true; } } @@ -2090,6 +2310,11 @@ loginService = new GitblitUserService(); } setUserService(loginService); + + // load and cache the project metadata + projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect()); + getProjectConfigs(); + mailExecutor = new MailExecutor(settings); if (mailExecutor.isReady()) { logger.info("Mail executor is scheduled to process the message queue every 2 minutes."); @@ -2125,6 +2350,8 @@ } catch (IllegalArgumentException e) { logger.error("Failed to configure JGit parameters!", e); } + + ContainerUtils.CVE_2007_0450.test(); } private void logTimezone(String type, TimeZone zone) { -- Gitblit v1.9.1