James Moger
2013-09-18 0250286553ca93406bfaa976d6aae61e76a9d55f
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;
@@ -102,6 +105,7 @@
import com.gitblit.models.GitClientApplication;
import com.gitblit.models.Metric;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RefModel;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.RepositoryUrl;
@@ -120,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 +236,33 @@
      }
      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;
   }
   /**
    * Determine if this is the GO variant of Gitblit.
@@ -274,6 +308,15 @@
         self().timezone = TimeZone.getTimeZone(tzid);
      }
      return self().timezone;
   }
   /**
    * Returns the active settings.
    *
    * @return the active settings
    */
   public static IStoredSettings getSettings() {
      return self().settings;
   }
   
   /**
@@ -684,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();
      }
   }
@@ -724,6 +767,18 @@
   }
   /**
    * Returns true if the username represents an internal account
    *
    * @param username
    * @return true if the specified username represents an internal account
    */
   protected boolean isInternalAccount(String username) {
      return !StringUtils.isEmpty(username)
            && (username.equalsIgnoreCase(Constants.FEDERATION_USER)
                  || username.equalsIgnoreCase(UserModel.ANONYMOUS.username));
   }
   /**
    * Authenticate a user based on a username and password.
    * 
    * @see IUserService.authenticate(String, char[])
@@ -748,10 +803,7 @@
         if (usernameDecoded.equalsIgnoreCase(Constants.FEDERATION_USER)) {
            List<String> tokens = getFederationTokens();
            if (tokens.contains(pw)) {
               // the federation user is an administrator
               UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
               federationUser.canAdmin = true;
               return federationUser;
               return getFederationUser();
            }
         }
      }
@@ -838,6 +890,7 @@
      if (principal != null) {
         String username = principal.getName();
         if (!StringUtils.isEmpty(username)) {
            boolean internalAccount = isInternalAccount(username);
            UserModel user = getUserModel(username);
            if (user != null) {
               // existing user
@@ -845,7 +898,8 @@
               logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}",
                     user.username, httpRequest.getRemoteAddr()));
               return user;
            } else if (settings.getBoolean(Keys.realm.container.autoCreateAccounts, true)) {
            } else if (settings.getBoolean(Keys.realm.container.autoCreateAccounts, false)
                  && !internalAccount) {
               // auto-create user from an authenticated container principal
               user = new UserModel(username.toLowerCase());
               user.displayName = username;
@@ -855,7 +909,7 @@
               logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}",
                     user.username, httpRequest.getRemoteAddr()));
               return user;
            } else {
            } else if (!internalAccount) {
               logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}",
                     principal.getName(), httpRequest.getRemoteAddr()));
            }
@@ -1024,6 +1078,13 @@
      String usernameDecoded = decodeUsername(username);
      return userService.deleteUser(usernameDecoded);
   }
   protected UserModel getFederationUser() {
      // the federation user is an administrator
      UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
      federationUser.canAdmin = true;
      return federationUser;
   }
   /**
    * Retrieve the user object for the specified username.
@@ -1186,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
@@ -1440,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
@@ -1474,7 +1526,10 @@
      }
      
      // return sorted copy of cached list
      List<String> list = new ArrayList<String>(repositoryListCache.keySet());
      List<String> list = new ArrayList<String>();
      for (RepositoryModel model : repositoryListCache.values()) {
         list.add(model.name);
      }
      StringUtils.sortRepositorynames(list);
      return list;
   }
@@ -1497,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;
@@ -1543,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));
@@ -1596,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
@@ -1638,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();
      
@@ -1664,6 +1706,30 @@
         }
      }
      return count;
   }
   private void reloadProjectMarkdown(ProjectModel project) {
      // project markdown
      File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : project.name) + "/project.mkd");
      if (pmkd.exists()) {
         Date lm = new Date(pmkd.lastModified());
         if (!projectMarkdownCache.hasCurrent(project.name, lm)) {
            String mkd = com.gitblit.utils.FileUtils.readContent(pmkd,  "\n");
            projectMarkdownCache.updateObject(project.name, lm, mkd);
         }
         project.projectMarkdown = projectMarkdownCache.getObject(project.name);
      }
      // project repositories markdown
      File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : project.name) + "/repositories.mkd");
      if (rmkd.exists()) {
         Date lm = new Date(rmkd.lastModified());
         if (!projectRepositoriesMarkdownCache.hasCurrent(project.name, lm)) {
            String mkd = com.gitblit.utils.FileUtils.readContent(rmkd,  "\n");
            projectRepositoriesMarkdownCache.updateObject(project.name, lm, mkd);
         }
         project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(project.name);
      }
   }
   
   
@@ -1700,27 +1766,7 @@
            project.title = projectConfigs.getString("project", name, "title");
            project.description = projectConfigs.getString("project", name, "description");
            
            // project markdown
            File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/project.mkd");
            if (pmkd.exists()) {
               Date lm = new Date(pmkd.lastModified());
               if (!projectMarkdownCache.hasCurrent(name, lm)) {
                  String mkd = com.gitblit.utils.FileUtils.readContent(pmkd,  "\n");
                  projectMarkdownCache.updateObject(name, lm, mkd);
               }
               project.projectMarkdown = projectMarkdownCache.getObject(name);
            }
            // project repositories markdown
            File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/repositories.mkd");
            if (rmkd.exists()) {
               Date lm = new Date(rmkd.lastModified());
               if (!projectRepositoriesMarkdownCache.hasCurrent(name, lm)) {
                  String mkd = com.gitblit.utils.FileUtils.readContent(rmkd,  "\n");
                  projectRepositoriesMarkdownCache.updateObject(name, lm, mkd);
               }
               project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(name);
            }
            reloadProjectMarkdown(project);
            
            configs.put(name.toLowerCase(), project);
         }
@@ -1811,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";
@@ -1842,6 +1888,8 @@
         // no repositories == no project
         return null;
      }
      reloadProjectMarkdown(project);
      return project;
   }
   
@@ -1935,15 +1983,24 @@
         // 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);
@@ -1960,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(
@@ -1987,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)) {
@@ -1997,9 +2056,11 @@
      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 (model.origin != null && model.origin.startsWith("file://")) {
      if (StringUtils.isEmpty(model.originRepository) && model.origin != null && model.origin.startsWith("file://")) {
         // repository was cloned locally... perhaps as a fork
         try {
            File folder = new File(new URI(model.origin));
@@ -2009,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) {
@@ -2070,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 + "/";
@@ -2189,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;
   }
@@ -2360,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)) {
@@ -2408,6 +2483,7 @@
                     String origin = config.getString("remote", "origin", "url");
                     origin = origin.replace(repositoryName, repository.name);
                     config.setString("remote", "origin", "url", origin);
                     config.setString(Constants.CONFIG_GITBLIT, null, "originRepository", repository.name);
                     config.save();
                  } catch (Exception e) {
                     logger.error("Failed to update repository fork config for " + fork, e);
@@ -2416,11 +2492,12 @@
               }
            }
            
            // remove this repository from any origin model's fork list
            // update this repository's origin's fork list
            if (!StringUtils.isEmpty(repository.originRepository)) {
               RepositoryModel origin = repositoryListCache.get(repository.originRepository);
               if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
                  origin.forks.remove(repositoryName);
                  origin.forks.add(repository.name);
               }
            }
@@ -2437,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)) {
@@ -2451,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
@@ -2469,6 +2561,7 @@
   public void updateConfiguration(Repository r, RepositoryModel repository) {
      StoredConfig config = r.getConfig();
      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);
@@ -2508,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);
@@ -2597,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)) {
@@ -2626,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;
   }
   /**
@@ -2920,8 +3067,7 @@
      String cloneUrl = sb.toString();
      // Retrieve all available repositories
      UserModel user = new UserModel(Constants.FEDERATION_USER);
      user.canAdmin = true;
      UserModel user = getFederationUser();
      List<RepositoryModel> list = getRepositoryModels(user);
      // create the [cloneurl, repositoryModel] map
@@ -3362,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());
@@ -3402,9 +3552,8 @@
      configureJGit();
      configureFanout();
      configureGitDaemon();
      CommitCache.instance().setCacheDays(settings.getInteger(Keys.web.activityCacheDays, 14));
      configureCommitCache();
      ContainerUtils.CVE_2007_0450.test();
   }
   
@@ -3511,6 +3660,42 @@
            gitDaemon = null;
            logger.error(MessageFormat.format("Failed to start Git daemon on {0}:{1,number,0}", bindInterface, port), e);
         }
      }
   }
   protected void configureCommitCache() {
      int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
      if (daysToCache <= 0) {
         logger.info("commit cache disabled");
      } else {
         long start = System.nanoTime();
         long repoCount = 0;
         long commitCount = 0;
         logger.info(MessageFormat.format("preparing {0} day commit cache. please wait...", daysToCache));
         CommitCache.instance().setCacheDays(daysToCache);
         Date cutoff = CommitCache.instance().getCutoffDate();
         for (String repositoryName : getRepositoryList()) {
            RepositoryModel model = getRepositoryModel(repositoryName);
            if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
               repoCount++;
               Repository repository = getRepository(repositoryName);
               for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
                  if (!ref.getDate().after(cutoff)) {
                     // branch not recently updated
                     continue;
                  }
                  List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
                  if (commits.size() > 0) {
                     logger.info(MessageFormat.format("  cached {0} commits for {1}:{2}",
                           commits.size(), repositoryName, ref.getName()));
                     commitCount += commits.size();
                  }
               }
               repository.close();
            }
         }
         logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
               daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
      }
   }
   
@@ -3706,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