From b1dba764c201f4708b82767b2d91edb6e189ce6f Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 22 Jul 2011 10:09:18 -0400 Subject: [PATCH] Fixed (again) empty repository check (issue 13) --- src/com/gitblit/GitBlit.java | 530 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 files changed, 479 insertions(+), 51 deletions(-) diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 7182d9b..c54fbe1 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -17,19 +17,30 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import javax.servlet.http.Cookie; +import org.apache.wicket.protocol.http.WebResponse; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.lib.StoredConfig; 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 org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,111 +51,330 @@ import com.gitblit.utils.JGitUtils; import com.gitblit.utils.StringUtils; +/** + * GitBlit is the servlet context listener singleton that acts as the core for + * the web ui and the servlets. This class is either directly instantiated by + * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the + * definition in the web.xml file (Gitblit WAR). + * + * This class is the central logic processor for Gitblit. All settings, user + * object, and repository object operations pass through this class. + * + * Repository Resolution. There are two pathways for finding repositories. One + * pathway, for web ui display and repository authentication & authorization, is + * within this class. The other pathway is through the standard GitServlet. + * + * @author James Moger + * + */ public class GitBlit implements ServletContextListener { - private static final GitBlit GITBLIT; + private static GitBlit gitblit; private final Logger logger = LoggerFactory.getLogger(GitBlit.class); - private FileResolver<Void> repositoryResolver; + private RepositoryResolver<Void> repositoryResolver; private File repositoriesFolder; - private boolean exportAll; + private boolean exportAll = true; - private ILoginService loginService; + private IUserService userService; - private IStoredSettings storedSettings; + private IStoredSettings settings; - static { - GITBLIT = new GitBlit(); + public GitBlit() { + if (gitblit == null) { + // set the static singleton reference + gitblit = this; + } } - private GitBlit() { - } - + /** + * Returns the Gitblit singleton. + * + * @return gitblit singleton + */ public static GitBlit self() { - return GITBLIT; + if (gitblit == null) { + new GitBlit(); + } + return gitblit; } + /** + * Returns the boolean value for the specified key. If the key does not + * exist or the value for the key can not be interpreted as a boolean, the + * defaultValue is returned. + * + * @see IStoredSettings.getBoolean(String, boolean) + * @param key + * @param defaultValue + * @return key value or defaultValue + */ public static boolean getBoolean(String key, boolean defaultValue) { - return GITBLIT.storedSettings.getBoolean(key, defaultValue); + return self().settings.getBoolean(key, defaultValue); } + /** + * Returns the integer value for the specified key. If the key does not + * exist or the value for the key can not be interpreted as an integer, the + * defaultValue is returned. + * + * @see IStoredSettings.getInteger(String key, int defaultValue) + * @param key + * @param defaultValue + * @return key value or defaultValue + */ public static int getInteger(String key, int defaultValue) { - return GITBLIT.storedSettings.getInteger(key, defaultValue); + return self().settings.getInteger(key, defaultValue); } + /** + * Returns the char value for the specified key. If the key does not exist + * or the value for the key can not be interpreted as a character, the + * defaultValue is returned. + * + * @see IStoredSettings.getChar(String key, char defaultValue) + * @param key + * @param defaultValue + * @return key value or defaultValue + */ + public static char getChar(String key, char defaultValue) { + return self().settings.getChar(key, defaultValue); + } + + /** + * Returns the string value for the specified key. If the key does not exist + * or the value for the key can not be interpreted as a string, the + * defaultValue is returned. + * + * @see IStoredSettings.getString(String key, String defaultValue) + * @param key + * @param defaultValue + * @return key value or defaultValue + */ public static String getString(String key, String defaultValue) { - return GITBLIT.storedSettings.getString(key, defaultValue); + return self().settings.getString(key, defaultValue); } + /** + * Returns a list of space-separated strings from the specified key. + * + * @see IStoredSettings.getStrings(String key) + * @param name + * @return list of strings + */ public static List<String> getStrings(String key) { - return GITBLIT.storedSettings.getStrings(key); + return self().settings.getStrings(key); } + + /** + * Returns the list of keys whose name starts with the specified prefix. If + * the prefix is null or empty, all key names are returned. + * + * @see IStoredSettings.getAllKeys(String key) + * @param startingWith + * @return list of keys + */ public static List<String> getAllKeys(String startingWith) { - return GITBLIT.storedSettings.getAllKeys(startingWith); + return self().settings.getAllKeys(startingWith); } - public boolean isDebugMode() { - return storedSettings.getBoolean(Keys.web.debugMode, false); + /** + * Is Gitblit running in debug mode? + * + * @return true if Gitblit is running in debug mode + */ + public static boolean isDebugMode() { + return self().settings.getBoolean(Keys.web.debugMode, false); } + /** + * Returns the list of non-Gitblit clone urls. This allows Gitblit to + * advertise alternative urls for Git client repository access. + * + * @param repositoryName + * @return list of non-gitblit clone urls + */ public List<String> getOtherCloneUrls(String repositoryName) { List<String> cloneUrls = new ArrayList<String>(); - for (String url : storedSettings.getStrings(Keys.web.otherUrls)) { + for (String url : settings.getStrings(Keys.web.otherUrls)) { cloneUrls.add(MessageFormat.format(url, repositoryName)); } return cloneUrls; } - public void setLoginService(ILoginService loginService) { - this.loginService = loginService; + /** + * Set the user service. The user service authenticates all users and is + * responsible for managing user permissions. + * + * @param userService + */ + public void setUserService(IUserService userService) { + logger.info("Setting up user service " + userService.toString()); + this.userService = userService; } + /** + * Authenticate a user based on a username and password. + * + * @see IUserService.authenticate(String, char[]) + * @param username + * @param password + * @return a user object or null + */ public UserModel authenticate(String username, char[] password) { - if (loginService == null) { + if (userService == null) { return null; } - return loginService.authenticate(username, password); + return userService.authenticate(username, password); } + /** + * Authenticate a user based on their cookie. + * + * @param cookies + * @return a user object or null + */ + public UserModel authenticate(Cookie[] cookies) { + if (userService == null) { + return null; + } + if (userService.supportsCookies()) { + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Constants.NAME)) { + String value = cookie.getValue(); + return userService.authenticate(value.toCharArray()); + } + } + } + } + return null; + } + + /** + * Sets a cookie for the specified user. + * + * @param response + * @param user + */ + public void setCookie(WebResponse response, UserModel user) { + if (userService == null) { + return; + } + if (userService.supportsCookies()) { + Cookie userCookie; + if (user == null) { + // clear cookie for logout + userCookie = new Cookie(Constants.NAME, ""); + } else { + // set cookie for login + char[] cookie = userService.getCookie(user); + userCookie = new Cookie(Constants.NAME, new String(cookie)); + userCookie.setMaxAge(Integer.MAX_VALUE); + } + userCookie.setPath("/"); + response.addCookie(userCookie); + } + } + + /** + * Returns the list of all users available to the login service. + * + * @see IUserService.getAllUsernames() + * @return list of all usernames + */ public List<String> getAllUsernames() { - List<String> names = new ArrayList<String>(loginService.getAllUsernames()); + List<String> names = new ArrayList<String>(userService.getAllUsernames()); Collections.sort(names); return names; } + /** + * Delete the user object with the specified username + * + * @see IUserService.deleteUser(String) + * @param username + * @return true if successful + */ public boolean deleteUser(String username) { - return loginService.deleteUser(username); + return userService.deleteUser(username); } + /** + * Retrieve the user object for the specified username. + * + * @see IUserService.getUserModel(String) + * @param username + * @return a user object or null + */ public UserModel getUserModel(String username) { - UserModel user = loginService.getUserModel(username); + UserModel user = userService.getUserModel(username); return user; } + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @see IUserService.getUsernamesForRepositoryRole(String) + * @param repository + * @return list of all usernames that can bypass the access restriction + */ public List<String> getRepositoryUsers(RepositoryModel repository) { - return loginService.getUsernamesForRole(repository.name); + return userService.getUsernamesForRepositoryRole(repository.name); } + /** + * Sets the list of all uses who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @see IUserService.setUsernamesForRepositoryRole(String, List<String>) + * @param repository + * @param usernames + * @return true if successful + */ public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) { - return loginService.setUsernamesForRole(repository.name, repositoryUsers); + return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers); } - public void editUserModel(String username, UserModel user, boolean isCreate) + /** + * Adds/updates a complete user object keyed by username. This method allows + * for renaming a user. + * + * @see IUserService.updateUserModel(String, UserModel) + * @param username + * @param user + * @param isCreate + * @throws GitBlitException + */ + public void updateUserModel(String username, UserModel user, boolean isCreate) throws GitBlitException { - if (!loginService.updateUserModel(username, user)) { + if (!userService.updateUserModel(username, user)) { throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!"); } } + /** + * Returns the list of all repositories available to Gitblit. This method + * does not consider user access permissions. + * + * @return list of all repositories + */ public List<String> getRepositoryList() { return JGitUtils.getRepositoryList(repositoriesFolder, exportAll, - storedSettings.getBoolean(Keys.git.nestedRepositories, true)); + settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true)); } + /** + * Returns the JGit repository for the specified name. + * + * @param repositoryName + * @return repository or null + */ public Repository getRepository(String repositoryName) { Repository r = null; try { @@ -153,13 +383,24 @@ r = null; logger.error("GitBlit.getRepository(String) failed to find " + new File(repositoriesFolder, repositoryName).getAbsolutePath()); + } catch (ServiceNotAuthorizedException e) { + r = null; + logger.error("GitBlit.getRepository(String) failed to find " + + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e); } catch (ServiceNotEnabledException e) { r = null; - e.printStackTrace(); + logger.error("GitBlit.getRepository(String) failed to find " + + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e); } return r; } + /** + * Returns the list of repository models that are accessible to the user. + * + * @param user + * @return list of repository models accessible to user + */ public List<RepositoryModel> getRepositoryModels(UserModel user) { List<String> list = getRepositoryList(); List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(); @@ -172,8 +413,19 @@ return repositories; } + /** + * Returns a repository model if the repository exists and the user may + * access the repository. + * + * @param user + * @param repositoryName + * @return repository model or null + */ public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) { RepositoryModel model = getRepositoryModel(repositoryName); + if (model == null) { + return null; + } if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { if (user != null && user.canAccessRepository(model.name)) { return model; @@ -184,6 +436,13 @@ } } + /** + * Returns the repository model for the specified repository. This method + * does not consider user access permissions. + * + * @param repositoryName + * @return repository model or null + */ public RepositoryModel getRepositoryModel(String repositoryName) { Repository r = getRepository(repositoryName); if (r == null) { @@ -192,7 +451,7 @@ RepositoryModel model = new RepositoryModel(); model.name = repositoryName; model.hasCommits = JGitUtils.hasCommits(r); - model.lastChange = JGitUtils.getLastChange(r); + model.lastChange = JGitUtils.getLastChange(r, null); StoredConfig config = JGitUtils.readConfig(r); if (config != null) { model.description = getConfig(config, "description", ""); @@ -209,6 +468,61 @@ return model; } + /** + * Returns the size in bytes of the repository. + * + * @param model + * @return size in bytes + */ + public long calculateSize(RepositoryModel model) { + File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED); + return com.gitblit.utils.FileUtils.folderSize(gitDir); + } + + /** + * Ensure that a cached repository is completely closed and its resources + * are properly released. + * + * @param repositoryName + */ + private void closeRepository(String repositoryName) { + Repository repository = getRepository(repositoryName); + // assume 2 uses in case reflection fails + int uses = 2; + try { + // The FileResolver caches repositories which is very useful + // for performance until you want to delete a repository. + // I have to use reflection to call close() the correct + // number of times to ensure that the object and ref databases + // are properly closed before I can delete the repository from + // the filesystem. + Field useCnt = Repository.class.getDeclaredField("useCnt"); + useCnt.setAccessible(true); + uses = ((AtomicInteger) useCnt.get(repository)).get(); + } catch (Exception e) { + logger.warn(MessageFormat + .format("Failed to reflectively determine use count for repository {0}", + repositoryName), e); + } + if (uses > 0) { + logger.info(MessageFormat + .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases", + repositoryName, uses, uses)); + for (int i = 0; i < uses; i++) { + repository.close(); + } + } + } + + /** + * Returns the gitblit string vlaue for the specified key. If key is not + * set, returns defaultValue. + * + * @param config + * @param field + * @param defaultValue + * @return field value or defaultValue + */ private String getConfig(StoredConfig config, String field, String defaultValue) { String value = config.getString("gitblit", null, field); if (StringUtils.isEmpty(value)) { @@ -217,16 +531,39 @@ return value; } + /** + * Returns the gitblit boolean vlaue for the specified key. If key is not + * set, returns defaultValue. + * + * @param config + * @param field + * @param defaultValue + * @return field value or defaultValue + */ private boolean getConfig(StoredConfig config, String field, boolean defaultValue) { return config.getBoolean("gitblit", field, defaultValue); } - public void editRepositoryModel(String repositoryName, RepositoryModel repository, + /** + * Creates/updates the repository model keyed by reopsitoryName. Saves all + * repository settings in .git/config. This method allows for renaming + * repositories and will update user access permissions accordingly. + * + * All repositories created by this method are bare and automatically have + * .git appended to their names, which is the standard convention for bare + * repositories. + * + * @param repositoryName + * @param repository + * @param isCreate + * @throws GitBlitException + */ + public void updateRepositoryModel(String repositoryName, RepositoryModel repository, boolean isCreate) throws GitBlitException { Repository r = null; if (isCreate) { // ensure created repository name ends with .git - if (!repository.name.endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) { + if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) { repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT; } if (new File(repositoriesFolder, repository.name).exists()) { @@ -236,10 +573,11 @@ } // create repository logger.info("create repository " + repository.name); - r = JGitUtils.createRepository(repositoriesFolder, repository.name, true); + r = JGitUtils.createRepository(repositoriesFolder, repository.name); } else { // rename repository if (!repositoryName.equalsIgnoreCase(repository.name)) { + closeRepository(repositoryName); File folder = new File(repositoriesFolder, repositoryName); File destFolder = new File(repositoriesFolder, repository.name); if (destFolder.exists()) { @@ -254,7 +592,7 @@ repository.name)); } // rename the roles - if (!loginService.renameRole(repositoryName, repository.name)) { + if (!userService.renameRepositoryRole(repositoryName, repository.name)) { throw new GitBlitException(MessageFormat.format( "Failed to rename repository permissions ''{0}'' to ''{1}''.", repositoryName, repository.name)); @@ -267,6 +605,8 @@ r = repositoryResolver.open(null, repository.name); } catch (RepositoryNotFoundException e) { logger.error("Repository not found", e); + } catch (ServiceNotAuthorizedException e) { + logger.error("Service not authorized", e); } catch (ServiceNotEnabledException e) { logger.error("Service not enabled", e); } @@ -293,16 +633,31 @@ } } + /** + * Deletes the repository from the file system and removes the repository + * permission from all repository users. + * + * @param model + * @return true if successful + */ public boolean deleteRepositoryModel(RepositoryModel model) { return deleteRepository(model.name); } + /** + * Deletes the repository from the file system and removes the repository + * permission from all repository users. + * + * @param repositoryName + * @return true if successful + */ public boolean deleteRepository(String repositoryName) { try { + closeRepository(repositoryName); File folder = new File(repositoriesFolder, repositoryName); if (folder.exists() && folder.isDirectory()) { - FileUtils.delete(folder, FileUtils.RECURSIVE); - if (loginService.deleteRole(repositoryName)) { + FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY); + if (userService.deleteRepositoryRole(repositoryName)) { return true; } } @@ -312,35 +667,108 @@ return false; } - public boolean renameRepository(RepositoryModel model, String newName) { - File folder = new File(repositoriesFolder, model.name); - if (folder.exists() && folder.isDirectory()) { - File newFolder = new File(repositoriesFolder, newName); - if (folder.renameTo(newFolder)) { - return loginService.renameRole(model.name, newName); + /** + * Returns an html version of the commit message with any global or + * repository-specific regular expression substitution applied. + * + * @param repositoryName + * @param text + * @return html version of the commit message + */ + public String processCommitMessage(String repositoryName, String text) { + String html = StringUtils.breakLinesForHtml(text); + Map<String, String> map = new HashMap<String, String>(); + // global regex keys + if (settings.getBoolean(Keys.regex.global, false)) { + for (String key : settings.getAllKeys(Keys.regex.global)) { + if (!key.equals(Keys.regex.global)) { + String subKey = key.substring(key.lastIndexOf('.') + 1); + map.put(subKey, settings.getString(key, "")); + } } } - return false; + + // repository-specific regex keys + List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "." + + repositoryName.toLowerCase()); + for (String key : keys) { + String subKey = key.substring(key.lastIndexOf('.') + 1); + map.put(subKey, settings.getString(key, "")); + } + + for (Entry<String, String> entry : map.entrySet()) { + String definition = entry.getValue().trim(); + String[] chunks = definition.split("!!!"); + if (chunks.length == 2) { + html = html.replaceAll(chunks[0], chunks[1]); + } else { + logger.warn(entry.getKey() + + " improperly formatted. Use !!! to separate match from replacement: " + + definition); + } + } + return html; } + /** + * Configure the Gitblit singleton with the specified settings source. This + * source may be file settings (Gitblit GO) or may be web.xml settings + * (Gitblit WAR). + * + * @param settings + */ public void configureContext(IStoredSettings settings) { logger.info("Reading configuration from " + settings.toString()); - this.storedSettings = settings; - repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "repos")); - exportAll = settings.getBoolean(Keys.git.exportAll, true); + this.settings = settings; + repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git")); + logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath()); repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll); + String realm = settings.getString(Keys.realm.userService, "users.properties"); + IUserService loginService = null; + try { + // check to see if this "file" is a login service class + Class<?> realmClass = Class.forName(realm); + if (IUserService.class.isAssignableFrom(realmClass)) { + loginService = (IUserService) realmClass.newInstance(); + } + } catch (Throwable t) { + // not a login service class or class could not be instantiated. + // try to use default file login service + File realmFile = new File(realm); + if (!realmFile.exists()) { + try { + realmFile.createNewFile(); + } catch (IOException x) { + logger.error( + MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x); + } + } + loginService = new FileUserService(realmFile); + } + setUserService(loginService); } + /** + * Configure Gitblit from the web.xml, if no configuration has already been + * specified. + * + * @see ServletContextListener.contextInitialize(ServletContextEvent) + */ @Override public void contextInitialized(ServletContextEvent contextEvent) { - if (storedSettings == null) { + if (settings == null) { + // Gitblit WAR is running in a servlet container WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext()); configureContext(webxmlSettings); } } + /** + * Gitblit is being shutdown either because the servlet container is + * shutting down or because the servlet container is re-deploying Gitblit. + */ @Override public void contextDestroyed(ServletContextEvent contextEvent) { - logger.info("GitBlit context destroyed by servlet container."); + logger.info("Gitblit context destroyed by servlet container."); } } -- Gitblit v1.9.1