From c05da657ec71c46d0e5bc32b074ddcd9d8b76353 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Tue, 12 Nov 2013 17:56:46 -0500 Subject: [PATCH] Add markup support for confluence, mediawiki, textile, trac, and twiki --- src/main/java/com/gitblit/GitBlit.java | 830 +++++++++++++++++++++++++++++++++------------------------- 1 files changed, 475 insertions(+), 355 deletions(-) diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index ca21717..f313b6e 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -61,6 +61,9 @@ import javax.mail.MessagingException; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMultipart; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @@ -84,8 +87,10 @@ import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.Constants.AccountType; import com.gitblit.Constants.AuthenticationType; import com.gitblit.Constants.AuthorizationControl; +import com.gitblit.Constants.CommitMessageRenderer; import com.gitblit.Constants.FederationRequest; import com.gitblit.Constants.FederationStrategy; import com.gitblit.Constants.FederationToken; @@ -121,8 +126,11 @@ import com.gitblit.utils.FederationUtils; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JGitUtils; +import com.gitblit.utils.JGitUtils.LastChange; import com.gitblit.utils.JsonUtils; +import com.gitblit.utils.MarkdownUtils; import com.gitblit.utils.MetricUtils; +import com.gitblit.utils.ModelUtils; import com.gitblit.utils.ObjectCache; import com.gitblit.utils.StringUtils; import com.gitblit.utils.TimeUtils; @@ -139,28 +147,28 @@ * 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 GitBlit gitblit; - + private final Logger logger = LoggerFactory.getLogger(GitBlit.class); private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5); private final List<FederationModel> federationRegistrations = Collections .synchronizedList(new ArrayList<FederationModel>()); - + private final ObjectCache<Collection<GitClientApplication>> clientApplications = new ObjectCache<Collection<GitClientApplication>>(); private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>(); @@ -168,19 +176,19 @@ private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>(); private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>(); - + 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>(""); - + private final ObjectCache<String> projectMarkdownCache = new ObjectCache<String>(); - + private final ObjectCache<String> projectRepositoriesMarkdownCache = new ObjectCache<String>(); private ServletContext servletContext; - + private File baseFolder; private File repositoriesFolder; @@ -194,15 +202,15 @@ private ServerStatus serverStatus; private MailExecutor mailExecutor; - + private LuceneExecutor luceneExecutor; - + private GCExecutor gcExecutor; - + private TimeZone timezone; - + private FileBasedConfig projectConfigs; - + private FanoutService fanoutService; private GitDaemon gitDaemon; @@ -221,7 +229,7 @@ /** * Returns the Gitblit singleton. - * + * * @return gitblit singleton */ public static GitBlit self() { @@ -232,18 +240,45 @@ } /** + * Returns the boot date of the Gitblit server. + * + * @return the boot date of Gitblit + */ + public static Date getBootDate() { + return self().serverStatus.bootDate; + } + + /** + * Returns the most recent change date of any repository served by Gitblit. + * + * @return a date + */ + public static Date getLastActivityDate() { + Date date = null; + for (String name : self().getRepositoryList()) { + Repository r = self().getRepository(name); + Date lastChange = JGitUtils.getLastChange(r).when; + r.close(); + if (lastChange != null && (date == null || lastChange.after(date))) { + date = lastChange; + } + } + return date; + } + + /** * Determine if this is the GO variant of Gitblit. - * + * * @return true if this is the GO variant of Gitblit. */ public static boolean isGO() { return self().settings instanceof FileSettings; } - + /** * Determine if this Gitblit instance is actively serving git repositories * or if it is merely a repository viewer. - * + * * @return true if Gitblit is serving repositories */ public static boolean isServingRepositories() { @@ -253,7 +288,7 @@ /** * Determine if this Gitblit instance is actively serving git repositories * or if it is merely a repository viewer. - * + * * @return true if Gitblit is serving repositories */ public static boolean isSendingMail() { @@ -262,7 +297,7 @@ /** * Returns the preferred timezone for the Gitblit instance. - * + * * @return a timezone */ public static TimeZone getTimezone() { @@ -276,31 +311,31 @@ } return self().timezone; } - + /** * Returns the active settings. - * + * * @return the active settings */ public static IStoredSettings getSettings() { return self().settings; } - + /** * Returns the user-defined blob encodings. - * + * * @return an array of encodings, may be empty */ public static String [] getEncodings() { return getStrings(Keys.web.blobEncodings).toArray(new String[0]); } - + /** * 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 @@ -314,7 +349,7 @@ * 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 @@ -328,7 +363,7 @@ * Returns the integer list for the specified key. If the key does not * exist or the value for the key can not be interpreted as an integer, an * empty list is returned. - * + * * @see IStoredSettings.getIntegers(String key) * @param key * @return key value or defaultValue @@ -336,12 +371,12 @@ public static List<Integer> getIntegers(String key) { return self().settings.getIntegers(key); } - + /** * Returns the value in bytes 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.getFilesize(String key, int defaultValue) * @param key * @param defaultValue @@ -355,7 +390,7 @@ * Returns the value in bytes for the specified key. If the key does not * exist or the value for the key can not be interpreted as a long, the * defaultValue is returned. - * + * * @see IStoredSettings.getFilesize(String key, long defaultValue) * @param key * @param defaultValue @@ -369,7 +404,7 @@ * 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 @@ -383,7 +418,7 @@ * 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 @@ -395,7 +430,7 @@ /** * Returns a list of space-separated strings from the specified key. - * + * * @see IStoredSettings.getStrings(String key) * @param n * @return list of strings @@ -406,7 +441,7 @@ /** * Returns a map of space-separated key-value pairs from the specified key. - * + * * @see IStoredSettings.getStrings(String key) * @param n * @return map of string, string @@ -418,7 +453,7 @@ /** * 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 @@ -430,7 +465,7 @@ /** * Is Gitblit running in debug mode? - * + * * @return true if Gitblit is running in debug mode */ public static boolean isDebugMode() { @@ -439,7 +474,7 @@ /** * Returns the file object for the specified configuration key. - * + * * @return the file */ public static File getFileOrFolder(String key, String defaultFileOrFolder) { @@ -453,7 +488,7 @@ * file or folder retrievals are (at least initially) funneled through this * method so it is the correct point to globally override/alter filesystem * access based on environment or some other indicator. - * + * * @return the file */ public static File getFileOrFolder(String fileOrFolder) { @@ -464,7 +499,7 @@ /** * Returns the path of the repositories folder. This method checks to see if * Gitblit is running on a cloud service and may return an adjusted path. - * + * * @return the repositories folder path */ public static File getRepositoriesFolder() { @@ -474,7 +509,7 @@ /** * Returns the path of the proposals folder. This method checks to see if * Gitblit is running on a cloud service and may return an adjusted path. - * + * * @return the proposals folder path */ public static File getProposalsFolder() { @@ -484,16 +519,16 @@ /** * Returns the path of the Groovy folder. This method checks to see if * Gitblit is running on a cloud service and may return an adjusted path. - * + * * @return the Groovy scripts folder path */ public static File getGroovyScriptsFolder() { return getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy"); } - + /** * Updates the list of server settings. - * + * * @param settings * @return true if the update succeeded */ @@ -507,10 +542,10 @@ serverStatus.heapFree = Runtime.getRuntime().freeMemory(); return serverStatus; } - + /** * Returns a list of repository URLs and the user access permission. - * + * * @param request * @param user * @param repository @@ -556,13 +591,13 @@ } return list; } - + protected String getRepositoryUrl(HttpServletRequest request, String username, RepositoryModel repository) { StringBuilder sb = new StringBuilder(); sb.append(HttpUtils.getGitblitURL(request)); sb.append(Constants.GIT_PATH); sb.append(repository.name); - + // inject username into repository url if authentication is required if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE) && !StringUtils.isEmpty(username)) { @@ -570,7 +605,7 @@ } return sb.toString(); } - + protected String getGitDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) { if (gitDaemon != null) { String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost"); @@ -587,7 +622,7 @@ } return null; } - + protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) { if (gitDaemon != null && user.canClone(repository)) { AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission; @@ -610,7 +645,7 @@ /** * Returns the list of custom client applications to be used for the * repository url panel; - * + * * @return a collection of client applications */ public Collection<GitClientApplication> getClientApplications() { @@ -629,13 +664,13 @@ if (clients != null) { clientApplications.updateObject("user", lastModified, clients); return clients; - } + } } catch (IOException e) { logger.error("Failed to deserialize " + userDefs.getAbsolutePath(), e); } } } - + // no user definitions, use system definitions if (!clientApplications.hasCurrent("system", new Date(0))) { try { @@ -649,10 +684,10 @@ logger.error("Failed to deserialize clientapps.json resource!", e); } } - + return clientApplications.getObject("system"); } - + private Collection<GitClientApplication> readClientApplications(InputStream is) { try { Type type = new TypeToken<Collection<GitClientApplication>>() { @@ -672,7 +707,7 @@ /** * Set the user service. The user service authenticates all users and is * responsible for managing user permissions. - * + * * @param userService */ public void setUserService(IUserService userService) { @@ -680,32 +715,32 @@ this.userService = userService; this.userService.setup(settings); } - + public boolean supportsAddUser() { return supportsCredentialChanges(new UserModel("")); } - + /** * Returns true if the user's credentials can be changed. - * + * * @param user * @return true if the user service supports credential changes */ public boolean supportsCredentialChanges(UserModel user) { if (user == null) { return false; - } else if (!Constants.EXTERNAL_ACCOUNT.equals(user.password)) { - // credentials likely maintained by Gitblit - return userService.supportsCredentialChanges(); + } else if (AccountType.LOCAL.equals(user.accountType)) { + // local account, we can change credentials + return true; } else { - // credentials are externally maintained - return false; + // external account, ask user service + return userService.supportsCredentialChanges(); } } /** * Returns true if the user's display name can be changed. - * + * * @param user * @return true if the user service supports display name changes */ @@ -715,7 +750,7 @@ /** * Returns true if the user's email address can be changed. - * + * * @param user * @return true if the user service supports email address changes */ @@ -725,7 +760,7 @@ /** * Returns true if the user's team memberships can be changed. - * + * * @param user * @return true if the user service supports team membership changes */ @@ -735,7 +770,7 @@ /** * Returns true if the username represents an internal account - * + * * @param username * @return true if the specified username represents an internal account */ @@ -747,7 +782,7 @@ /** * Authenticate a user based on a username and password. - * + * * @see IUserService.authenticate(String, char[]) * @param username * @param password @@ -784,7 +819,7 @@ /** * Authenticate a user based on their cookie. - * + * * @param cookies * @return a user object or null */ @@ -807,22 +842,22 @@ /** * Authenticate a user based on HTTP request parameters. - * + * * Authentication by X509Certificate is tried first and then by cookie. - * + * * @param httpRequest * @return a user object or null */ public UserModel authenticate(HttpServletRequest httpRequest) { return authenticate(httpRequest, false); } - + /** * Authenticate a user based on HTTP request parameters. - * + * * Authentication by X509Certificate, servlet container principal, cookie, * and BASIC header. - * + * * @param httpRequest * @param requiresCertificate * @return a user object or null @@ -846,12 +881,12 @@ model.username, metadata.serialNumber, httpRequest.getRemoteAddr())); } } - + if (requiresCertificate) { // caller requires client certificate authentication (e.g. git servlet) return null; } - + // try to authenticate by servlet container principal Principal principal = httpRequest.getUserPrincipal(); if (principal != null) { @@ -882,7 +917,7 @@ } } } - + // try to authenticate by cookie if (allowCookieAuthentication()) { UserModel user = authenticate(httpRequest.getCookies()); @@ -893,7 +928,7 @@ return user; } } - + // try to authenticate by BASIC final String authorization = httpRequest.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Basic")) { @@ -914,14 +949,14 @@ user.username, httpRequest.getRemoteAddr())); return user; } else { - logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials ({1}) from {2}", - username, credentials, httpRequest.getRemoteAddr())); + logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}", + username, httpRequest.getRemoteAddr())); } } } return null; } - + protected void flagWicketSession(AuthenticationType authenticationType) { RequestCycle requestCycle = RequestCycle.get(); if (requestCycle != null) { @@ -944,7 +979,7 @@ /** * Sets a cookie for the specified user. - * + * * @param response * @param user */ @@ -976,10 +1011,10 @@ response.addCookie(userCookie); } } - + /** * Logout a user. - * + * * @param user */ public void logout(UserModel user) { @@ -991,27 +1026,27 @@ /** * Encode the username for user in an url. - * + * * @param name * @return the encoded name */ protected String encodeUsername(String name) { - return name.replace("@", "%40").replace(" ", "%20").replace("\\", "%5C"); + return name.replace("@", "%40").replace(" ", "%20").replace("\\", "%5C"); } /** * Decode a username from an encoded url. - * + * * @param name * @return the decoded name */ protected String decodeUsername(String name) { return name.replace("%40", "@").replace("%20", " ").replace("%5C", "\\"); } - + /** * Returns the list of all users available to the login service. - * + * * @see IUserService.getAllUsernames() * @return list of all usernames */ @@ -1022,7 +1057,7 @@ /** * Returns the list of all users available to the login service. - * + * * @see IUserService.getAllUsernames() * @return list of all usernames */ @@ -1033,7 +1068,7 @@ /** * Delete the user object with the specified username - * + * * @see IUserService.deleteUser(String) * @param username * @return true if successful @@ -1045,7 +1080,7 @@ String usernameDecoded = decodeUsername(username); return userService.deleteUser(usernameDecoded); } - + protected UserModel getFederationUser() { // the federation user is an administrator UserModel federationUser = new UserModel(Constants.FEDERATION_USER); @@ -1055,7 +1090,7 @@ /** * Retrieve the user object for the specified username. - * + * * @see IUserService.getUserModel(String) * @param username * @return a user object or null @@ -1065,14 +1100,14 @@ return null; } String usernameDecoded = decodeUsername(username); - UserModel user = userService.getUserModel(usernameDecoded); + UserModel user = userService.getUserModel(usernameDecoded); return user; } - + /** * Returns the effective list of permissions for this user, taking into account * team memberships, ownerships. - * + * * @param user * @return the effective list of permissions for the user */ @@ -1107,7 +1142,7 @@ set.add(rp); } } - + List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set); Collections.sort(list); return list; @@ -1117,7 +1152,7 @@ * Returns the list of users and their access permissions for the specified * repository including permission source information such as the team or * regular expression which sets the permission. - * + * * @param repository * @return a list of RegistrantAccessPermissions */ @@ -1140,10 +1175,10 @@ } return list; } - + /** * Sets the access permissions to the specified repository for the specified users. - * + * * @param repository * @param permissions * @return true if the user models have been updated @@ -1160,11 +1195,11 @@ } return userService.updateUserModels(users); } - + /** * Returns the list of all users who have an explicit access permission * for the specified repository. - * + * * @see IUserService.getUsernamesForRepositoryRole(String) * @param repository * @return list of all usernames that have an access permission for the repository @@ -1176,7 +1211,7 @@ /** * 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 @@ -1192,7 +1227,7 @@ /** * 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 @@ -1207,15 +1242,15 @@ "Failed to rename ''{0}'' because ''{1}'' already exists.", username, user.username)); } - + // rename repositories and owner fields for all repositories for (RepositoryModel model : getRepositoryModels(user)) { if (model.isUsersPersonalRepository(username)) { // personal repository model.addOwner(user.username); String oldRepositoryName = model.name; - model.name = "~" + user.username + model.name.substring(model.projectPath.length()); - model.projectPath = "~" + user.username; + model.name = user.getPersonalPath() + model.name.substring(model.projectPath.length()); + model.projectPath = user.getPersonalPath(); updateRepositoryModel(oldRepositoryName, model, false); } else if (model.isOwner(username)) { // common/shared repo @@ -1232,7 +1267,7 @@ /** * Returns the list of available teams that a user or repository may be * assigned to. - * + * * @return the list of teams */ public List<String> getAllTeamnames() { @@ -1243,7 +1278,7 @@ /** * Returns the list of available teams that a user or repository may be * assigned to. - * + * * @return the list of teams */ public List<TeamModel> getAllTeams() { @@ -1253,19 +1288,19 @@ /** * Returns the TeamModel object for the specified name. - * + * * @param teamname * @return a TeamModel object or null */ public TeamModel getTeamModel(String teamname) { return userService.getTeamModel(teamname); } - + /** * Returns the list of teams and their access permissions for the specified * repository including the source of the permission such as the admin flag * or a regular expression. - * + * * @param repository * @return a list of RegistrantAccessPermissions */ @@ -1280,10 +1315,10 @@ Collections.sort(list); return list; } - + /** * Sets the access permissions to the specified repository for the specified teams. - * + * * @param repository * @param permissions * @return true if the team models have been updated @@ -1300,11 +1335,11 @@ } return userService.updateTeamModels(teams); } - + /** * Returns the list of all teams who have an explicit access permission for * the specified repository. - * + * * @see IUserService.getTeamnamesForRepositoryRole(String) * @param repository * @return list of all teamnames with explicit access permissions to the repository @@ -1316,7 +1351,7 @@ /** * Sets the list of all uses who are allowed to bypass the access * restriction placed on the specified repository. - * + * * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>) * @param repository * @param teamnames @@ -1331,7 +1366,7 @@ /** * Updates the TeamModel object for the specified name. - * + * * @param teamname * @param team * @param isCreate @@ -1352,7 +1387,7 @@ /** * Delete the team object with the specified teamname - * + * * @see IUserService.deleteTeam(String) * @param teamname * @return true if successful @@ -1360,17 +1395,17 @@ public boolean deleteTeam(String teamname) { return userService.deleteTeam(teamname); } - + /** * Adds the repository to the list of cached repositories if Gitblit is * configured to cache the repository list. - * + * * @param model */ private void addToCachedRepositoryList(RepositoryModel model) { if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { repositoryListCache.put(model.name.toLowerCase(), model); - + // update the fork origin repository with this repository clone if (!StringUtils.isEmpty(model.originRepository)) { if (repositoryListCache.containsKey(model.originRepository)) { @@ -1380,10 +1415,10 @@ } } } - + /** * Removes the repository from the list of cached repositories. - * + * * @param name * @return the model being removed */ @@ -1396,23 +1431,23 @@ /** * Clears all the cached metadata for the specified repository. - * + * * @param repositoryName */ private void clearRepositoryMetadataCache(String repositoryName) { repositorySizeCache.remove(repositoryName); repositoryMetricsCache.remove(repositoryName); } - + /** * Resets the repository list cache. - * + * */ public void resetRepositoryListCache() { logger.info("Repository cache manually reset"); repositoryListCache.clear(); } - + /** * Calculate the checksum of settings that affect the repository list cache. * @return a checksum @@ -1427,11 +1462,11 @@ String checksum = StringUtils.getSHA1(ns.toString()); return checksum; } - + /** * Compare the last repository list setting checksum to the current checksum. * If different then clear the cache so that it may be rebuilt. - * + * * @return true if the cached repository list is valid since the last check */ private boolean isValidRepositoryList() { @@ -1448,14 +1483,14 @@ /** * 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() { if (repositoryListCache.size() == 0 || !isValidRepositoryList()) { // we are not caching OR we have not yet cached OR the cached list is invalid long startTime = System.currentTimeMillis(); - List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder, + List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder, settings.getBoolean(Keys.git.onlyAccessBareRepositories, false), settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true), settings.getInteger(Keys.git.searchRecursionDepth, -1), @@ -1468,24 +1503,15 @@ } else { // we are caching this list String msg = "{0} repositories identified in {1} msecs"; - - // optionally (re)calculate repository sizes if (getBoolean(Keys.web.showRepositorySizes, true)) { - ByteFormat byteFormat = new ByteFormat(); + // optionally (re)calculate repository sizes msg = "{0} repositories identified with calculated folder sizes in {1} msecs"; - for (String repository : repositories) { - RepositoryModel model = getRepositoryModel(repository); - if (!model.skipSizeCalculation) { - model.size = byteFormat.format(calculateSize(model)); - } - } - } else { - // update cache - for (String repository : repositories) { - getRepositoryModel(repository); - } } - + + for (String repository : repositories) { + getRepositoryModel(repository); + } + // rebuild fork networks for (RepositoryModel model : repositoryListCache.values()) { if (!StringUtils.isEmpty(model.originRepository)) { @@ -1495,12 +1521,12 @@ } } } - + long duration = System.currentTimeMillis() - startTime; logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration)); } } - + // return sorted copy of cached list List<String> list = new ArrayList<String>(); for (RepositoryModel model : repositoryListCache.values()) { @@ -1512,7 +1538,7 @@ /** * Returns the JGit repository for the specified name. - * + * * @param repositoryName * @return repository or null */ @@ -1522,12 +1548,16 @@ /** * Returns the JGit repository for the specified name. - * + * * @param repositoryName * @param logError * @return repository or null */ public Repository getRepository(String repositoryName, boolean logError) { + // Decode url-encoded repository name (issue-278) + // http://stackoverflow.com/questions/17183110 + repositoryName = repositoryName.replace("%7E", "~").replace("%7e", "~"); + if (isCollectingGarbage(repositoryName)) { logger.warn(MessageFormat.format("Rejecting request for {0}, busy collecting garbage!", repositoryName)); return null; @@ -1536,7 +1566,7 @@ File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED); if (dir == null) return null; - + Repository r = null; try { FileKey key = FileKey.exact(dir, FS.DETECTED); @@ -1552,7 +1582,7 @@ /** * Returns the list of repository models that are accessible to the user. - * + * * @param user * @return list of repository models accessible to user */ @@ -1574,23 +1604,6 @@ } } } - if (getBoolean(Keys.web.showRepositorySizes, true)) { - int repoCount = 0; - long startTime = System.currentTimeMillis(); - ByteFormat byteFormat = new ByteFormat(); - for (RepositoryModel model : repositories) { - if (!model.skipSizeCalculation) { - repoCount++; - model.size = byteFormat.format(calculateSize(model)); - } - } - long duration = System.currentTimeMillis() - startTime; - if (duration > 250) { - // only log calcualtion time if > 250 msecs - logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs", - repoCount, duration)); - } - } long duration = System.currentTimeMillis() - methodStart; logger.info(MessageFormat.format("{0} repository models loaded for {1} in {2} msecs", repositories.size(), user == null ? "anonymous" : user.username, duration)); @@ -1600,7 +1613,7 @@ /** * Returns a repository model if the repository exists and the user may * access the repository. - * + * * @param user * @param repositoryName * @return repository model or null @@ -1622,20 +1635,24 @@ /** * 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) { + // Decode url-encoded repository name (issue-278) + // http://stackoverflow.com/questions/17183110 + repositoryName = repositoryName.replace("%7E", "~").replace("%7e", "~"); + if (!repositoryListCache.containsKey(repositoryName)) { RepositoryModel model = loadRepositoryModel(repositoryName); if (model == null) { return null; } addToCachedRepositoryList(model); - return model; + return DeepCopier.copy(model); } - + // cached model RepositoryModel model = repositoryListCache.get(repositoryName.toLowerCase()); @@ -1654,7 +1671,7 @@ logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName)); return null; } - + FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r); if (config.isOutdated()) { // reload model @@ -1663,27 +1680,23 @@ removeFromCachedRepositoryList(model.name); addToCachedRepositoryList(model); } else { - // update a few repository parameters + // 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); - if (!model.skipSizeCalculation) { - ByteFormat byteFormat = new ByteFormat(); - model.size = byteFormat.format(calculateSize(model)); - } + updateLastChangeFields(r, model); } r.close(); - + // return a copy of the cached model return DeepCopier.copy(model); } - + /** * Returns the star count of the repository. - * + * * @param repository * @return the star count */ @@ -1696,7 +1709,7 @@ } return count; } - + private void reloadProjectMarkdown(ProjectModel project) { // project markdown File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : project.name) + "/project.mkd"); @@ -1708,7 +1721,7 @@ } project.projectMarkdown = projectMarkdownCache.getObject(project.name); } - + // project repositories markdown File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : project.name) + "/repositories.mkd"); if (rmkd.exists()) { @@ -1720,17 +1733,17 @@ project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(project.name); } } - - + + /** * 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 (projectCache.isEmpty() || projectConfigs.isOutdated()) { - + try { projectConfigs.load(); } catch (Exception e) { @@ -1754,9 +1767,9 @@ } project.title = projectConfigs.getString("project", name, "title"); project.description = projectConfigs.getString("project", name, "description"); - + reloadProjectMarkdown(project); - + configs.put(name.toLowerCase(), project); } projectCache.clear(); @@ -1764,10 +1777,10 @@ } return projectCache; } - + /** * Returns a list of project models for the user. - * + * * @param user * @param includeUsers * @return list of projects that are accessible to the user @@ -1779,9 +1792,9 @@ 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(); + String rootPath = StringUtils.getRootPath(model.name).toLowerCase(); if (!map.containsKey(rootPath)) { ProjectModel project; if (configs.containsKey(rootPath)) { @@ -1795,7 +1808,7 @@ } map.get(rootPath).addRepository(model); } - + // sort projects, root project first List<ProjectModel> projects; if (includeUsers) { @@ -1818,10 +1831,10 @@ } 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 @@ -1834,10 +1847,10 @@ } 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 */ @@ -1846,8 +1859,8 @@ ProjectModel project = configs.get(name.toLowerCase()); if (project == null) { project = new ProjectModel(name); - if (name.length() > 0 && name.charAt(0) == '~') { - UserModel user = getUserModel(name.substring(1)); + if (ModelUtils.isPersonalRepository(name)) { + UserModel user = getUserModel(ModelUtils.getUserNameFromRepoPath(name)); if (user != null) { project.title = user.getDisplayName(); project.description = "personal repositories"; @@ -1877,16 +1890,16 @@ // no repositories == no project return null; } - + reloadProjectMarkdown(project); return project; } - + /** * Returns the list of project models that are referenced by the supplied * repository model list. This is an alternative method exists to ensure * Gitblit does not call getRepositoryModels(UserModel) twice in a request. - * + * * @param repositoryModels * @param includeUsers * @return a list of project models @@ -1924,7 +1937,7 @@ } return new ArrayList<ProjectModel>(projects.values()); } - + /** * 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. @@ -1932,7 +1945,7 @@ * 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 */ @@ -1947,10 +1960,10 @@ } 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 */ @@ -1972,32 +1985,40 @@ // is symlinked. Use the provided repository name. model.name = repositoryName; } - model.hasCommits = JGitUtils.hasCommits(r); - model.lastChange = JGitUtils.getLastChange(r); model.projectPath = StringUtils.getFirstPathElement(repositoryName); - + StoredConfig config = r.getConfig(); boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url")); - + if (config != null) { + // Initialize description from description file + if (getConfig(config,"description", null) == null) { + File descFile = new File(r.getDirectory(), "description"); + if (descFile.exists()) { + String desc = com.gitblit.utils.FileUtils.readContent(descFile, System.getProperty("line.separator")); + if (!desc.toLowerCase().startsWith("unnamed repository")) { + config.setString(Constants.CONFIG_GITBLIT, null, "description", desc); + } + } + } model.description = getConfig(config, "description", ""); model.originRepository = getConfig(config, "originRepository", null); model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", ""))); - model.useTickets = getConfig(config, "useTickets", false); model.useDocs = getConfig(config, "useDocs", false); model.useIncrementalPushTags = getConfig(config, "useIncrementalPushTags", false); model.incrementalPushTagPrefix = getConfig(config, "incrementalPushTagPrefix", null); model.allowForks = getConfig(config, "allowForks", true); model.accessRestriction = AccessRestrictionType.fromName(getConfig(config, - "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null))); + "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, "PUSH"))); model.authorizationControl = AuthorizationControl.fromName(getConfig(config, "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null))); model.verifyCommitter = getConfig(config, "verifyCommitter", false); model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin); model.isFrozen = getConfig(config, "isFrozen", false); - model.showReadme = getConfig(config, "showReadme", false); model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false); model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false); + model.commitMessageRenderer = CommitMessageRenderer.fromName(getConfig(config, "commitMessageRenderer", + settings.getString(Keys.web.commitMessageRenderer, null))); model.federationStrategy = FederationStrategy.fromName(getConfig(config, "federationStrategy", null)); model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList( @@ -2025,7 +2046,7 @@ Constants.CONFIG_GITBLIT, null, "indexBranch"))); model.metricAuthorExclusions = new ArrayList<String>(Arrays.asList(config.getStringList( Constants.CONFIG_GITBLIT, null, "metricAuthorExclusions"))); - + // Custom defined properties model.customFields = new LinkedHashMap<String, String>(); for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) { @@ -2035,8 +2056,10 @@ model.HEAD = JGitUtils.getHEADRef(r); model.availableRefs = JGitUtils.getAvailableHeadTargets(r); model.sparkleshareId = JGitUtils.getSparkleshareId(r); + model.hasCommits = JGitUtils.hasCommits(r); + updateLastChangeFields(r, model); r.close(); - + if (StringUtils.isEmpty(model.originRepository) && model.origin != null && model.origin.startsWith("file://")) { // repository was cloned locally... perhaps as a fork try { @@ -2047,6 +2070,9 @@ File repoFolder = new File(getRepositoriesFolder(), originRepo); if (repoFolder.exists()) { model.originRepository = originRepo.toLowerCase(); + + // persist the fork origin + updateConfiguration(r, model); } } } catch (URISyntaxException e) { @@ -2055,20 +2081,20 @@ } return model; } - + /** * Determines if this server has the requested repository. - * + * * @param n * @return true if the repository exists */ public boolean hasRepository(String repositoryName) { return hasRepository(repositoryName, false); } - + /** * Determines if this server has the requested repository. - * + * * @param n * @param caseInsensitive * @return true if the repository exists @@ -2078,7 +2104,7 @@ // 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.toLowerCase()); - } + } Repository r = getRepository(repositoryName, false); if (r == null) { return false; @@ -2086,11 +2112,11 @@ r.close(); return true; } - + /** * Determines if the specified user has a fork of the specified origin * repository. - * + * * @param username * @param origin * @return true the if the user has a fork @@ -2098,17 +2124,17 @@ public boolean hasFork(String username, String origin) { return getFork(username, origin) != null; } - + /** * Gets the name of a user's fork of the specified origin * repository. - * + * * @param username * @param origin * @return the name of the user's fork, null otherwise */ public String getFork(String username, String origin) { - String userProject = "~" + username.toLowerCase(); + String userProject = ModelUtils.getPersonalPath(username); if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { String userPath = userProject + "/"; @@ -2124,7 +2150,7 @@ } } } - + if (originModel.originRepository != null) { roots.add(originModel.originRepository); originModel = repositoryListCache.get(originModel.originRepository); @@ -2133,7 +2159,7 @@ originModel = null; } } - + for (String repository : repositoryListCache.keySet()) { if (repository.startsWith(userPath)) { RepositoryModel model = repositoryListCache.get(repository); @@ -2164,11 +2190,11 @@ // user does not have a fork return null; } - + /** * Returns the fork network for a repository by traversing up the fork graph * to discover the root and then down through all children of the root node. - * + * * @param repository * @return a ForkModel */ @@ -2191,7 +2217,7 @@ return root; } } - + private ForkModel getForkModelFromCache(String repository) { RepositoryModel model = repositoryListCache.get(repository.toLowerCase()); if (model == null) { @@ -2208,7 +2234,7 @@ } return fork; } - + private ForkModel getForkModel(String repository) { RepositoryModel model = getRepositoryModel(repository.toLowerCase()); if (model == null) { @@ -2227,28 +2253,38 @@ } /** - * Returns the size in bytes of the repository. Gitblit caches the - * repository sizes to reduce the performance penalty of recursive - * calculation. The cache is updated if the repository has been changed - * since the last calculation. - * + * Updates the last changed fields and optionally calculates the size of the + * repository. Gitblit caches the repository sizes to reduce the performance + * penalty of recursive calculation. The cache is updated if the repository + * has been changed since the last calculation. + * * @param model - * @return size in bytes + * @return size in bytes of the repository */ - public long calculateSize(RepositoryModel model) { - if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) { - return repositorySizeCache.getObject(model.name); + public long updateLastChangeFields(Repository r, RepositoryModel model) { + LastChange lc = JGitUtils.getLastChange(r); + model.lastChange = lc.when; + model.lastChangeAuthor = lc.who; + + if (!getBoolean(Keys.web.showRepositorySizes, true) || model.skipSizeCalculation) { + model.size = null; + return 0L; } - File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED); - long size = com.gitblit.utils.FileUtils.folderSize(gitDir); - repositorySizeCache.updateObject(model.name, model.lastChange, size); + if (!repositorySizeCache.hasCurrent(model.name, model.lastChange)) { + File gitDir = r.getDirectory(); + long sz = com.gitblit.utils.FileUtils.folderSize(gitDir); + repositorySizeCache.updateObject(model.name, model.lastChange, sz); + } + long size = repositorySizeCache.getObject(model.name); + ByteFormat byteFormat = new ByteFormat(); + model.size = byteFormat.format(size); return size; } /** * Ensure that a cached repository is completely closed and its resources * are properly released. - * + * * @param repositoryName */ private void closeRepository(String repositoryName) { @@ -2283,7 +2319,7 @@ repository.close(); } } - + // close any open index writer/searcher in the Lucene executor luceneExecutor.close(repositoryName); } @@ -2293,7 +2329,7 @@ * This method builds a metrics cache. The cache is updated if the * repository is updated. A new copy of the metrics list is returned on each * call so that modifications to the list are non-destructive. - * + * * @param model * @param repository * @return a new array list of metrics @@ -2310,7 +2346,7 @@ /** * Returns the gitblit string value for the specified key. If key is not * set, returns defaultValue. - * + * * @param config * @param field * @param defaultValue @@ -2327,7 +2363,7 @@ /** * Returns the gitblit boolean value for the specified key. If key is not * set, returns defaultValue. - * + * * @param config * @param field * @param defaultValue @@ -2336,11 +2372,11 @@ private boolean getConfig(StoredConfig config, String field, boolean defaultValue) { return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue); } - + /** * Returns the gitblit string value for the specified key. If key is not * set, returns defaultValue. - * + * * @param config * @param field * @param defaultValue @@ -2362,11 +2398,11 @@ * 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 @@ -2398,7 +2434,8 @@ } // create repository logger.info("create repository " + repository.name); - r = JGitUtils.createRepository(repositoriesFolder, repository.name); + String shared = getString(Keys.git.createRepositoriesShared, "FALSE"); + r = JGitUtils.createRepository(repositoriesFolder, repository.name, shared); } else { // rename repository if (!repositoryName.equalsIgnoreCase(repository.name)) { @@ -2436,7 +2473,7 @@ "Failed to rename repository permissions ''{0}'' to ''{1}''.", repositoryName, repository.name)); } - + // rename fork origins in their configs if (!ArrayUtils.isEmpty(repository.forks)) { for (String fork : repository.forks) { @@ -2454,7 +2491,7 @@ rf.close(); } } - + // update this repository's origin's fork list if (!StringUtils.isEmpty(repository.originRepository)) { RepositoryModel origin = repositoryListCache.get(repository.originRepository); @@ -2477,10 +2514,19 @@ // update settings if (r != null) { updateConfiguration(r, repository); + // Update the description file + File descFile = new File(r.getDirectory(), "description"); + if (repository.description != null) + { + com.gitblit.utils.FileUtils.writeContent(descFile, repository.description); + } + else if (descFile.exists() && !descFile.isDirectory()) { + descFile.delete(); + } // only update symbolic head if it changes String currentRef = JGitUtils.getHEADRef(r); if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) { - logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}", + logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}", repository.name, currentRef, repository.HEAD)); if (JGitUtils.setHEADtoRef(r, repository.HEAD)) { // clear the cache @@ -2488,19 +2534,25 @@ } } + // Adjust permissions in case we updated the config files + JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "config"), + getString(Keys.git.createRepositoriesShared, "FALSE")); + JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "HEAD"), + getString(Keys.git.createRepositoriesShared, "FALSE")); + // 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); } - + /** * Updates the Gitblit configuration for the specified repository. - * + * * @param r * the Git repository * @param repository @@ -2511,7 +2563,6 @@ config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description); config.setString(Constants.CONFIG_GITBLIT, null, "originRepository", repository.originRepository); config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners)); - config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets); config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs); config.setBoolean(Constants.CONFIG_GITBLIT, null, "useIncrementalPushTags", repository.useIncrementalPushTags); if (StringUtils.isEmpty(repository.incrementalPushTagPrefix) || @@ -2526,7 +2577,6 @@ config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter); config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches); config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen); - config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme); config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSizeCalculation", repository.skipSizeCalculation); config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSummaryMetrics", repository.skipSummaryMetrics); config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy", @@ -2549,13 +2599,23 @@ config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits); } + CommitMessageRenderer defaultRenderer = CommitMessageRenderer.fromName(settings.getString(Keys.web.commitMessageRenderer, null)); + if (repository.commitMessageRenderer == null || repository.commitMessageRenderer == defaultRenderer) { + // use default from config + config.unset(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer"); + } else { + // repository overrides default + config.setString(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer", + repository.commitMessageRenderer.name()); + } + updateList(config, "federationSets", repository.federationSets); updateList(config, "preReceiveScript", repository.preReceiveScripts); updateList(config, "postReceiveScript", repository.postReceiveScripts); updateList(config, "mailingList", repository.mailingLists); updateList(config, "indexBranch", repository.indexedBranches); updateList(config, "metricAuthorExclusions", repository.metricAuthorExclusions); - + // User Defined Properties if (repository.customFields != null) { if (repository.customFields.size() == 0) { @@ -2577,7 +2637,7 @@ logger.error("Failed to save repository config!", e); } } - + private void updateList(StoredConfig config, String field, List<String> list) { // a null list is skipped, not cleared // this is for RPC administration where an older manager might be used @@ -2594,7 +2654,7 @@ /** * Deletes the repository from the file system and removes the repository * permission from all repository users. - * + * * @param model * @return true if successful */ @@ -2605,7 +2665,7 @@ /** * Deletes the repository from the file system and removes the repository * permission from all repository users. - * + * * @param repositoryName * @return true if successful */ @@ -2614,7 +2674,7 @@ closeRepository(repositoryName); // clear the repository cache clearRepositoryMetadataCache(repositoryName); - + RepositoryModel model = removeFromCachedRepositoryList(repositoryName); if (model != null && !ArrayUtils.isEmpty(model.forks)) { resetRepositoryListCache(); @@ -2637,13 +2697,57 @@ /** * Returns an html version of the commit message with any global or * repository-specific regular expression substitution applied. - * + * + * This method uses the preferred renderer to transform the commit message. + * + * @param repository + * @param text + * @return html version of the commit message + */ + public String processCommitMessage(RepositoryModel repository, String text) { + switch (repository.commitMessageRenderer) { + case MARKDOWN: + try { + String prepared = processCommitMessageRegex(repository.name, text); + return MarkdownUtils.transformMarkdown(prepared); + } catch (Exception e) { + logger.error("Failed to render commit message as markdown", e); + } + break; + default: + // noop + break; + } + + return processPlainCommitMessage(repository.name, text); + } + + /** + * Returns an html version of the commit message with any global or + * repository-specific regular expression substitution applied. + * + * This method assumes the commit message is plain text. + * * @param repositoryName * @param text * @return html version of the commit message */ - public String processCommitMessage(String repositoryName, String text) { - String html = StringUtils.breakLinesForHtml(text); + public String processPlainCommitMessage(String repositoryName, String text) { + String html = StringUtils.escapeForHtml(text, false); + html = processCommitMessageRegex(repositoryName, html); + return StringUtils.breakLinesForHtml(html); + + } + + /** + * Apply globally or per-repository specified regex substitutions to the + * commit message. + * + * @param repositoryName + * @param text + * @return the processed commit message + */ + protected String processCommitMessageRegex(String repositoryName, String text) { Map<String, String> map = new HashMap<String, String>(); // global regex keys if (settings.getBoolean(Keys.regex.global, false)) { @@ -2667,19 +2771,19 @@ String definition = entry.getValue().trim(); String[] chunks = definition.split("!!!"); if (chunks.length == 2) { - html = html.replaceAll(chunks[0], chunks[1]); + text = text.replaceAll(chunks[0], chunks[1]); } else { logger.warn(entry.getKey() + " improperly formatted. Use !!! to separate match from replacement: " + definition); } } - return html; + return text; } /** * Returns Gitblit's scheduled executor service for scheduling tasks. - * + * * @return scheduledExecutor */ public ScheduledExecutorService executor() { @@ -2727,7 +2831,7 @@ /** * Returns the list of federated gitblit instances that this instance will * try to pull. - * + * * @return list of registered gitblit instances */ public List<FederationModel> getFederationRegistrations() { @@ -2739,7 +2843,7 @@ /** * Retrieve the specified federation registration. - * + * * @param name * the name of the registration * @return a federation registration @@ -2763,7 +2867,7 @@ /** * Returns the list of federation sets. - * + * * @return list of federation sets */ public List<FederationSet> getFederationSets(String gitblitUrl) { @@ -2786,7 +2890,7 @@ /** * Returns the list of possible federation tokens for this Gitblit instance. - * + * * @return list of federation tokens */ public List<String> getFederationTokens() { @@ -2804,7 +2908,7 @@ /** * Returns the specified federation token for this Gitblit instance. - * + * * @param type * @return a federation token */ @@ -2814,7 +2918,7 @@ /** * Returns the specified federation token for this Gitblit instance. - * + * * @param value * @return a federation token */ @@ -2826,7 +2930,7 @@ /** * Compares the provided token with this Gitblit instance's tokens and * determines if the requested permission may be granted to the token. - * + * * @param req * @param token * @return true if the request can be executed @@ -2852,7 +2956,7 @@ /** * Acknowledge and cache the status of a remote Gitblit instance. - * + * * @param identification * the identification of the pulling Gitblit instance * @param registration @@ -2872,7 +2976,7 @@ /** * Returns the list of registration results. - * + * * @return the list of registration results */ public List<FederationModel> getFederationResultRegistrations() { @@ -2882,7 +2986,7 @@ /** * Submit a federation proposal. The proposal is cached locally and the * Gitblit administrator(s) are notified via email. - * + * * @param proposal * the proposal * @param gitblitUrl @@ -2914,7 +3018,7 @@ /** * Returns the list of pending federation proposals - * + * * @return list of federation proposals */ public List<FederationProposal> getPendingFederationProposals() { @@ -2940,7 +3044,7 @@ /** * Get repositories for the specified token. - * + * * @param gitblitUrl * the base url of this gitblit instance * @param token @@ -2999,7 +3103,7 @@ /** * Creates a proposal from the token. - * + * * @param gitblitUrl * the url of this Gitblit instance * @param token @@ -3021,7 +3125,7 @@ /** * Returns the proposal identified by the supplied token. - * + * * @param token * @return the specified proposal or null */ @@ -3037,7 +3141,7 @@ /** * Deletes a pending federation proposal. - * + * * @param a * proposal * @return true if the proposal was deleted @@ -3051,7 +3155,7 @@ /** * Returns the list of all Groovy push hook scripts. Script files must have * .groovy extension - * + * * @return list of available hook scripts */ public List<String> getAllScripts() { @@ -3075,7 +3179,7 @@ /** * Returns the list of pre-receive scripts the repository inherited from the * global settings and team affiliations. - * + * * @param repository * if null only the globally specified scripts are returned * @return a list of scripts @@ -3107,7 +3211,7 @@ * Returns the list of all available Groovy pre-receive push hook scripts * that are not already inherited by the repository. Script files must have * .groovy extension - * + * * @param repository * optional parameter * @return list of available hook scripts @@ -3128,7 +3232,7 @@ /** * Returns the list of post-receive scripts the repository inherited from * the global settings and team affiliations. - * + * * @param repository * if null only the globally specified scripts are returned * @return a list of scripts @@ -3159,7 +3263,7 @@ * Returns the list of unused Groovy post-receive push hook scripts that are * not already inherited by the repository. Script files must have .groovy * extension - * + * * @param repository * optional parameter * @return list of available hook scripts @@ -3176,24 +3280,24 @@ } return scripts; } - + /** * Search the specified repositories using the Lucene query. - * + * * @param query * @param page * @param pageSize * @param repositories * @return */ - public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) { + public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) { List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories); return srs; } /** * Notify the administrators by email. - * + * * @param subject * @param message */ @@ -3204,7 +3308,7 @@ /** * Notify users by email of something. - * + * * @param subject * @param message * @param toAddresses @@ -3215,7 +3319,7 @@ /** * Notify users by email of something. - * + * * @param subject * @param message * @param toAddresses @@ -3229,16 +3333,16 @@ Message mail = mailExecutor.createMessage(toAddresses); if (mail != null) { mail.setSubject(subject); - - MimeBodyPart messagePart = new MimeBodyPart(); + + MimeBodyPart messagePart = new MimeBodyPart(); messagePart.setText(message, "utf-8"); messagePart.setHeader("Content-Type", "text/plain; charset=\"utf-8\""); messagePart.setHeader("Content-Transfer-Encoding", "quoted-printable"); - + MimeMultipart multiPart = new MimeMultipart(); multiPart.addBodyPart(messagePart); mail.setContent(multiPart); - + mailExecutor.queue(mail); } } catch (MessagingException e) { @@ -3248,7 +3352,7 @@ /** * Notify users by email of something. - * + * * @param subject * @param message * @param toAddresses @@ -3259,7 +3363,7 @@ /** * Notify users by email of something. - * + * * @param subject * @param message * @param toAddresses @@ -3273,12 +3377,12 @@ Message mail = mailExecutor.createMessage(toAddresses); if (mail != null) { mail.setSubject(subject); - - MimeBodyPart messagePart = new MimeBodyPart(); + + MimeBodyPart messagePart = new MimeBodyPart(); messagePart.setText(message, "utf-8"); messagePart.setHeader("Content-Type", "text/html; charset=\"utf-8\""); messagePart.setHeader("Content-Transfer-Encoding", "quoted-printable"); - + MimeMultipart multiPart = new MimeMultipart(); multiPart.addBodyPart(messagePart); mail.setContent(multiPart); @@ -3292,7 +3396,7 @@ /** * Returns the descriptions/comments of the Gitblit config settings. - * + * * @return SettingsModel */ public ServerSettings getSettingsModel() { @@ -3305,7 +3409,7 @@ setting.name = key; settingsModel.add(setting); } - setting.currentValue = settings.getString(key, ""); + setting.currentValue = settings.getString(key, ""); } settingsModel.pushScripts = getAllScripts(); return settingsModel; @@ -3315,7 +3419,7 @@ * Parse the properties file and aggregate all the comments by the setting * key. A setting model tracks the current value, the default value, the * description of the setting and and directives about the setting. - * + * * @return Map<String, SettingModel> */ private ServerSettings loadSettingModels() { @@ -3384,7 +3488,7 @@ * 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, File folder, boolean startFederation) { @@ -3401,7 +3505,11 @@ mailExecutor = new MailExecutor(settings); luceneExecutor = new LuceneExecutor(settings, repositoriesFolder); gcExecutor = new GCExecutor(settings); - + + // initialize utilities + String prefix = settings.getString(Keys.git.userRepositoryPrefix, "~"); + ModelUtils.setUserRepoPrefix(prefix); + // calculate repository list settings checksum for future config changes repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum()); @@ -3410,7 +3518,7 @@ logger.info("Identifying available repositories..."); getRepositoryList(); } - + logTimezone("JVM", TimeZone.getDefault()); logTimezone(Constants.NAME, getTimezone()); @@ -3428,12 +3536,12 @@ } setUserService(loginService); } - + // load and cache the project metadata projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "${baseFolder}/projects.conf"), FS.detect()); getProjectConfigs(); - - configureMailExecutor(); + + configureMailExecutor(); configureLuceneIndexing(); configureGarbageCollector(); if (startFederation) { @@ -3446,7 +3554,7 @@ ContainerUtils.CVE_2007_0450.test(); } - + protected void configureMailExecutor() { if (mailExecutor.isReady()) { logger.info("Mail executor is scheduled to process the message queue every 2 minutes."); @@ -3455,12 +3563,12 @@ logger.warn("Mail server is not properly configured. Mail services disabled."); } } - + protected void configureLuceneIndexing() { scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2, TimeUnit.MINUTES); logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes."); } - + protected void configureGarbageCollector() { // schedule gc engine if (gcExecutor.isReady()) { @@ -3480,13 +3588,13 @@ delay = (int) ((cd.getTime() - now.getTime())/TimeUtils.MIN); String when = delay + " mins"; if (delay > 60) { - when = MessageFormat.format("{0,number,0.0} hours", ((float)delay)/60f); + when = MessageFormat.format("{0,number,0.0} hours", (delay)/60f); } logger.info(MessageFormat.format("Next scheculed GC scan is in {0}", when)); scheduledExecutor.scheduleAtFixedRate(gcExecutor, delay, 60*24, TimeUnit.MINUTES); } } - + protected void configureJGit() { // Configure JGit WindowCacheConfig cfg = new WindowCacheConfig(); @@ -3510,7 +3618,7 @@ logger.error("Failed to configure JGit parameters!", e); } } - + protected void configureFanout() { // startup Fanout PubSub service if (settings.getInteger(Keys.fanout.port, 0) > 0) { @@ -3538,7 +3646,7 @@ fanoutService.start(); } } - + protected void configureGitDaemon() { int port = settings.getInteger(Keys.git.daemonPort, 0); String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost"); @@ -3552,7 +3660,7 @@ } } } - + protected void configureCommitCache() { int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14); if (daysToCache <= 0) { @@ -3566,7 +3674,7 @@ Date cutoff = CommitCache.instance().getCutoffDate(); for (String repositoryName : getRepositoryList()) { RepositoryModel model = getRepositoryModel(repositoryName); - if (model.hasCommits && model.lastChange.after(cutoff)) { + if (model != null && model.hasCommits && model.lastChange.after(cutoff)) { repoCount++; Repository repository = getRepository(repositoryName); for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) { @@ -3588,11 +3696,11 @@ daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start))); } } - + protected final Logger getLogger() { return logger; } - + protected final ScheduledExecutorService getScheduledExecutor() { return scheduledExecutor; } @@ -3600,7 +3708,7 @@ protected final LuceneExecutor getLuceneExecutor() { return luceneExecutor; } - + private void logTimezone(String type, TimeZone zone) { SimpleDateFormat df = new SimpleDateFormat("z Z"); df.setTimeZone(zone); @@ -3611,7 +3719,7 @@ /** * Configure Gitblit from the web.xml, if no configuration has already been * specified. - * + * * @see ServletContextListener.contextInitialize(ServletContextEvent) */ @Override @@ -3624,7 +3732,7 @@ String contextRealPath = context.getRealPath("/"); File contextFolder = (contextRealPath != null) ? new File(contextRealPath) : null; String openShift = System.getenv("OPENSHIFT_DATA_DIR"); - + if (!StringUtils.isEmpty(openShift)) { // Gitblit is running in OpenShift/JBoss File base = new File(openShift); @@ -3633,7 +3741,7 @@ // gitblit.properties setting overrides File overrideFile = new File(base, "gitblit.properties"); webxmlSettings.applyOverrides(overrideFile); - + // Copy the included scripts to the configured groovy folder String path = webxmlSettings.getString(Keys.groovy.scriptsFolder, "groovy"); File localScripts = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, base, path); @@ -3649,15 +3757,15 @@ } } } - + // configure context using the web.xml configureContext(webxmlSettings, base, true); } else { // Gitblit is running in a standard servlet container logger.info("WAR contextFolder is " + ((contextFolder != null) ? contextFolder.getAbsolutePath() : "<empty>")); - + String path = webxmlSettings.getString(Constants.baseFolder, Constants.contextFolder$ + "/WEB-INF/data"); - + if (path.contains(Constants.contextFolder$) && contextFolder == null) { // warn about null contextFolder (issue-199) logger.error(""); @@ -3667,7 +3775,19 @@ logger.error(MessageFormat.format("OR configure your servlet container to specify a \"{0}\" parameter in the context configuration!!", Constants.baseFolder)); logger.error(""); } - + + try { + // try to lookup JNDI env-entry for the baseFolder + InitialContext ic = new InitialContext(); + Context env = (Context) ic.lookup("java:comp/env"); + String val = (String) env.lookup("baseFolder"); + if (!StringUtils.isEmpty(val)) { + path = val; + } + } catch (NamingException n) { + logger.error("Failed to get JNDI env-entry: " + n.getExplanation()); + } + File base = com.gitblit.utils.FileUtils.resolveParameter(Constants.contextFolder$, contextFolder, path); base.mkdirs(); @@ -3678,15 +3798,15 @@ } // delegate all config to baseFolder/gitblit.properties file - FileSettings settings = new FileSettings(localSettings.getAbsolutePath()); + FileSettings settings = new FileSettings(localSettings.getAbsolutePath()); configureContext(settings, base, true); } } - + settingsModel = loadSettingModels(); serverStatus.servletContainer = servletContext.getServerInfo(); } - + protected void extractResources(ServletContext context, String path, File toDir) { for (String resource : context.getResourcePaths(path)) { // extract the resource to the directory if it does not exist @@ -3751,18 +3871,18 @@ gitDaemon.stop(); } } - + /** - * + * * @return true if we are running the gc executor */ public boolean isCollectingGarbage() { return gcExecutor.isRunning(); } - + /** * Returns true if Gitblit is actively collecting garbage in this repository. - * + * * @param repositoryName * @return true if actively collecting garbage */ @@ -3773,15 +3893,15 @@ /** * Creates a personal fork of the specified repository. The clone is view * restricted by default and the owner of the source repository is given - * access to the clone. - * + * access to the clone. + * * @param repository * @param user * @return the repository model of the fork, if successful * @throws GitBlitException */ public RepositoryModel fork(RepositoryModel repository, UserModel user) throws GitBlitException { - String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name))); + String cloneName = MessageFormat.format("{0}/{1}.git", user.getPersonalPath(), StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name))); String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name); // clone the repository @@ -3834,7 +3954,7 @@ } cloneTeams.add(cloneTeam); } - userService.updateTeamModels(cloneTeams); + userService.updateTeamModels(cloneTeams); // add this clone to the cached model addToCachedRepositoryList(cloneModel); @@ -3844,7 +3964,7 @@ /** * Allow to understand if GitBlit supports and is configured to allow * cookie-based authentication. - * + * * @return status of Cookie authentication enablement. */ public boolean allowCookieAuthentication() { -- Gitblit v1.9.1