From 4f0f65fed0613cd69eba1cb8a80d0d18d0a79983 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Thu, 19 Sep 2013 08:13:48 -0400 Subject: [PATCH] Reverse line links in blob view (issue-309) --- src/main/java/com/gitblit/GitBlit.java | 248 +++++++++++++++++++++++++++++++++++------------- 1 files changed, 179 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index ca21717..95da669 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -32,6 +32,7 @@ import java.nio.charset.Charset; import java.security.Principal; import java.text.MessageFormat; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -84,8 +85,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 +124,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; @@ -229,6 +235,33 @@ new GitBlit(); } return gitblit; + } + + /** + * 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; } /** @@ -694,12 +727,12 @@ 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(); } } @@ -1214,8 +1247,8 @@ // 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 @@ -1468,22 +1501,13 @@ } 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 @@ -1528,6 +1552,10 @@ * @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; @@ -1574,23 +1602,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)); @@ -1627,13 +1638,17 @@ * @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 @@ -1669,11 +1684,7 @@ 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(); @@ -1846,8 +1857,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"; @@ -1972,14 +1983,22 @@ // 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", ""))); @@ -1998,6 +2017,8 @@ 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,6 +2056,8 @@ 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://")) { @@ -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) { @@ -2108,7 +2134,7 @@ * @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 + "/"; @@ -2227,21 +2253,31 @@ } /** - * 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; } @@ -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)) { @@ -2477,6 +2514,15 @@ // 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)) { @@ -2491,7 +2537,13 @@ // close the repository object r.close(); } - + + // 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")); + // update repository cache removeFromCachedRepositoryList(repositoryName); // model will actually be replaced on next load because config is stale @@ -2549,6 +2601,16 @@ 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); @@ -2638,12 +2700,56 @@ * 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 (ParseException 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,14 +2773,14 @@ 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; } /** @@ -3402,6 +3508,10 @@ 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()); @@ -3566,7 +3676,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)) { @@ -3781,7 +3891,7 @@ * @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 -- Gitblit v1.9.1