James Moger
2013-11-17 db4f6b5740c6ea45d9e2209dc569bc18904a8b4d
src/main/java/com/gitblit/GitBlit.java
@@ -61,14 +61,17 @@
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;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.resource.ContextRelativeResource;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.eclipse.jgit.lib.Repository;
@@ -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;
@@ -95,6 +100,14 @@
import com.gitblit.fanout.FanoutService;
import com.gitblit.fanout.FanoutSocketService;
import com.gitblit.git.GitDaemon;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblitManager;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IProjectManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.ISessionManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.FederationSet;
@@ -121,8 +134,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 +155,38 @@
 * 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 {
public class GitBlit implements ServletContextListener,
                        IRuntimeManager,
                        INotificationManager,
                        IUserManager,
                        ISessionManager,
                        IRepositoryManager,
                        IProjectManager,
                        IFederationManager,
                        IGitblitManager {
   private static GitBlit gitblit;
   private final IStoredSettings goSettings;
   private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
   private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
   private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10);
   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 +194,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,256 +220,140 @@
   private ServerStatus serverStatus;
   private MailExecutor mailExecutor;
   private LuceneExecutor luceneExecutor;
   private GCExecutor gcExecutor;
   private MirrorExecutor mirrorExecutor;
   private TimeZone timezone;
   private FileBasedConfig projectConfigs;
   private FanoutService fanoutService;
   private GitDaemon gitDaemon;
   public GitBlit() {
      if (gitblit == null) {
         // set the static singleton reference
         gitblit = this;
      }
      this.goSettings = null;
   }
   public GitBlit(final IUserService userService) {
   protected GitBlit(final IUserService userService) {
      this.goSettings = null;
      this.userService = userService;
      gitblit = this;
   }
   public GitBlit(IStoredSettings settings, File baseFolder) {
      this.goSettings = settings;
      this.baseFolder = baseFolder;
      gitblit = this;
   }
   /**
    * Returns the Gitblit singleton.
    *
    *
    * @return gitblit singleton
    */
   public static GitBlit self() {
      if (gitblit == null) {
         new GitBlit();
      }
      return gitblit;
   }
   /**
    * 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;
   @SuppressWarnings("unchecked")
   public static <X> X getManager(Class<X> managerClass) {
      if (managerClass.isAssignableFrom(GitBlit.class)) {
         return (X) gitblit;
      }
      return null;
   }
   @Override
   public File getBaseFolder() {
      return baseFolder;
   }
   @Override
   public void setBaseFolder(File folder) {
      this.baseFolder = folder;
   }
   /**
    * 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
    * Returns the boot date of the Gitblit server.
    *
    * @return the boot date of Gitblit
    */
   public static boolean isServingRepositories() {
      return getBoolean(Keys.git.enableGitServlet, true) || (getInteger(Keys.git.daemonPort, 0) > 0);
   @Override
   public Date getBootDate() {
      return serverStatus.bootDate;
   }
   /**
    * Returns the most recent change date of any repository served by Gitblit.
    *
    * @return a date
    */
   @Override
   public Date getLastActivityDate() {
      Date date = null;
      for (String name : getRepositoryList()) {
         Repository r = 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 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() {
      return self().mailExecutor.isReady();
   @Override
   public boolean isServingRepositories() {
      return settings.getBoolean(Keys.git.enableGitServlet, true) || (settings.getInteger(Keys.git.daemonPort, 0) > 0);
   }
   /**
    * Returns the preferred timezone for the Gitblit instance.
    *
    *
    * @return a timezone
    */
   public static TimeZone getTimezone() {
      if (self().timezone == null) {
         String tzid = getString("web.timezone", null);
   @Override
   public TimeZone getTimezone() {
      if (timezone == null) {
         String tzid = settings.getString("web.timezone", null);
         if (StringUtils.isEmpty(tzid)) {
            self().timezone = TimeZone.getDefault();
            return self().timezone;
            timezone = TimeZone.getDefault();
            return timezone;
         }
         self().timezone = TimeZone.getTimeZone(tzid);
         timezone = TimeZone.getTimeZone(tzid);
      }
      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
    * @return key value or defaultValue
    */
   public static boolean getBoolean(String key, boolean defaultValue) {
      return self().settings.getBoolean(key, defaultValue);
   }
   /**
    * Returns the integer value for the specified key. If the key does not
    * exist or the value for the key can not be interpreted as an integer, the
    * defaultValue is returned.
    *
    * @see IStoredSettings.getInteger(String key, int defaultValue)
    * @param key
    * @param defaultValue
    * @return key value or defaultValue
    */
   public static int getInteger(String key, int defaultValue) {
      return self().settings.getInteger(key, defaultValue);
   }
   /**
    * 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
    */
   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
    * @return key value or defaultValue
    */
   public static int getFilesize(String key, int defaultValue) {
      return self().settings.getFilesize(key, defaultValue);
   }
   /**
    * 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
    * @return key value or defaultValue
    */
   public static long getFilesize(String key, long defaultValue) {
      return self().settings.getFilesize(key, defaultValue);
   }
   /**
    * Returns the char value for the specified key. If the key does not exist
    * or the value for the key can not be interpreted as a character, the
    * defaultValue is returned.
    *
    * @see IStoredSettings.getChar(String key, char defaultValue)
    * @param key
    * @param defaultValue
    * @return key value or defaultValue
    */
   public static char getChar(String key, char defaultValue) {
      return self().settings.getChar(key, defaultValue);
   }
   /**
    * Returns the string value for the specified key. If the key does not exist
    * or the value for the key can not be interpreted as a string, the
    * defaultValue is returned.
    *
    * @see IStoredSettings.getString(String key, String defaultValue)
    * @param key
    * @param defaultValue
    * @return key value or defaultValue
    */
   public static String getString(String key, String defaultValue) {
      return self().settings.getString(key, defaultValue);
   }
   /**
    * Returns a list of space-separated strings from the specified key.
    *
    * @see IStoredSettings.getStrings(String key)
    * @param n
    * @return list of strings
    */
   public static List<String> getStrings(String key) {
      return self().settings.getStrings(key);
   }
   /**
    * 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
    */
   public static Map<String, String> getMap(String key) {
      return self().settings.getMap(key);
   }
   /**
    * Returns the list of keys whose name starts with the specified prefix. If
    * the prefix is null or empty, all key names are returned.
    *
    * @see IStoredSettings.getAllKeys(String key)
    * @param startingWith
    * @return list of keys
    */
   public static List<String> getAllKeys(String startingWith) {
      return self().settings.getAllKeys(startingWith);
      return timezone;
   }
   /**
    * Is Gitblit running in debug mode?
    *
    *
    * @return true if Gitblit is running in debug mode
    */
   public static boolean isDebugMode() {
      return self().settings.getBoolean(Keys.web.debugMode, false);
   @Override
   public boolean isDebugMode() {
      return settings.getBoolean(Keys.web.debugMode, false);
   }
   /**
    * Returns the file object for the specified configuration key.
    *
    *
    * @return the file
    */
   public static File getFileOrFolder(String key, String defaultFileOrFolder) {
      String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
   @Override
   public File getFileOrFolder(String key, String defaultFileOrFolder) {
      String fileOrFolder = settings.getString(key, defaultFileOrFolder);
      return getFileOrFolder(fileOrFolder);
   }
@@ -453,69 +363,97 @@
    * 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) {
   @Override
   public File getFileOrFolder(String fileOrFolder) {
      return com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$,
            self().baseFolder, fileOrFolder);
            baseFolder, fileOrFolder);
   }
   /**
    * 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() {
   @Override
   public File getRepositoriesFolder() {
      return getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
   }
   /**
    * 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() {
   @Override
   public File getProposalsFolder() {
      return getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
   }
   /**
    * 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() {
   @Override
   public File getHooksFolder() {
      return getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy");
   }
   /**
    * Updates the list of server settings.
    *
    * Returns the path of the Groovy Grape folder. This method checks to see if
    * Gitblit is running on a cloud service and may return an adjusted path.
    *
    * @return the Groovy Grape folder path
    */
   @Override
   public File getGrapesFolder() {
      return getFileOrFolder(Keys.groovy.grapeFolder, "${baseFolder}/groovy/grape");
   }
   /**
    * Returns the runtime settings.
    *
    * @return runtime settings
    */
   @Override
   public IStoredSettings getSettings() {
      return settings;
   }
   /**
    * Updates the runtime settings.
    *
    * @param settings
    * @return true if the update succeeded
    */
   @Override
   public boolean updateSettings(Map<String, String> updatedSettings) {
      return settings.saveSettings(updatedSettings);
   }
   @Override
   public ServerStatus getStatus() {
      // update heap memory status
      serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
      serverStatus.heapFree = Runtime.getRuntime().freeMemory();
      return serverStatus;
   }
   /**
    * Returns a list of repository URLs and the user access permission.
    *
    *
    * @param request
    * @param user
    * @param repository
    * @return a list of repository urls
    */
   @Override
   public List<RepositoryUrl> getRepositoryUrls(HttpServletRequest request, UserModel user, RepositoryModel repository) {
      if (user == null) {
         user = UserModel.ANONYMOUS;
@@ -556,13 +494,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 +508,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 +525,7 @@
      }
      return null;
   }
   protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {
      if (gitDaemon != null && user.canClone(repository)) {
         AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;
@@ -610,9 +548,10 @@
   /**
    * Returns the list of custom client applications to be used for the
    * repository url panel;
    *
    *
    * @return a collection of client applications
    */
   @Override
   public Collection<GitClientApplication> getClientApplications() {
      // prefer user definitions, if they exist
      File userDefs = new File(baseFolder, "clientapps.json");
@@ -629,13 +568,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 +588,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 +611,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,62 +619,67 @@
      this.userService = userService;
      this.userService.setup(settings);
   }
   @Override
   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
    */
   @Override
   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
    */
   @Override
   public boolean supportsDisplayNameChanges(UserModel user) {
      return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();
   }
   /**
    * Returns true if the user's email address can be changed.
    *
    *
    * @param user
    * @return true if the user service supports email address changes
    */
   @Override
   public boolean supportsEmailAddressChanges(UserModel user) {
      return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();
   }
   /**
    * Returns true if the user's team memberships can be changed.
    *
    *
    * @param user
    * @return true if the user service supports team membership changes
    */
   @Override
   public boolean supportsTeamMembershipChanges(UserModel user) {
      return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
   }
   /**
    * Returns true if the username represents an internal account
    *
    *
    * @param username
    * @return true if the specified username represents an internal account
    */
@@ -747,12 +691,13 @@
   /**
    * Authenticate a user based on a username and password.
    *
    *
    * @see IUserService.authenticate(String, char[])
    * @param username
    * @param password
    * @return a user object or null
    */
   @Override
   public UserModel authenticate(String username, char[] password) {
      if (StringUtils.isEmpty(username)) {
         // can not authenticate empty username
@@ -784,7 +729,7 @@
   /**
    * Authenticate a user based on their cookie.
    *
    *
    * @param cookies
    * @return a user object or null
    */
@@ -807,30 +752,32 @@
   /**
    * 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
    */
   @Override
   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
    */
   @Override
   public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) {
      // try to authenticate by certificate
      boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true);
      String [] oids = getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]);
      String [] oids = settings.getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]);
      UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids);
      if (model != null) {
         // grab real user model and preserve certificate serial number
@@ -846,12 +793,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,9 +829,9 @@
            }
         }
      }
      // try to authenticate by cookie
      if (allowCookieAuthentication()) {
      if (supportsCookies()) {
         UserModel user = authenticate(httpRequest.getCookies());
         if (user != null) {
            flagWicketSession(AuthenticationType.COOKIE);
@@ -893,7 +840,7 @@
            return user;
         }
      }
      // try to authenticate by BASIC
      final String authorization = httpRequest.getHeader("Authorization");
      if (authorization != null && authorization.startsWith("Basic")) {
@@ -914,14 +861,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,11 +891,12 @@
   /**
    * Sets a cookie for the specified user.
    *
    *
    * @param response
    * @param user
    */
   public void setCookie(WebResponse response, UserModel user) {
   @Override
   public void setCookie(HttpServletResponse response, UserModel user) {
      if (userService == null) {
         return;
      }
@@ -976,12 +924,13 @@
         response.addCookie(userCookie);
      }
   }
   /**
    * Logout a user.
    *
    *
    * @param user
    */
   @Override
   public void logout(UserModel user) {
      if (userService == null) {
         return;
@@ -991,30 +940,31 @@
   /**
    * 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
    */
   @Override
   public List<String> getAllUsernames() {
      List<String> names = new ArrayList<String>(userService.getAllUsernames());
      return names;
@@ -1022,10 +972,11 @@
   /**
    * Returns the list of all users available to the login service.
    *
    *
    * @see IUserService.getAllUsernames()
    * @return list of all usernames
    */
   @Override
   public List<UserModel> getAllUsers() {
      List<UserModel> users = userService.getAllUsers();
      return users;
@@ -1033,11 +984,12 @@
   /**
    * Delete the user object with the specified username
    *
    *
    * @see IUserService.deleteUser(String)
    * @param username
    * @return true if successful
    */
   @Override
   public boolean deleteUser(String username) {
      if (StringUtils.isEmpty(username)) {
         return false;
@@ -1045,8 +997,9 @@
      String usernameDecoded = decodeUsername(username);
      return userService.deleteUser(usernameDecoded);
   }
   protected UserModel getFederationUser() {
   @Override
   public UserModel getFederationUser() {
      // the federation user is an administrator
      UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
      federationUser.canAdmin = true;
@@ -1055,27 +1008,29 @@
   /**
    * Retrieve the user object for the specified username.
    *
    *
    * @see IUserService.getUserModel(String)
    * @param username
    * @return a user object or null
    */
   @Override
   public UserModel getUserModel(String username) {
      if (StringUtils.isEmpty(username)) {
         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
    */
   @Override
   public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
      if (StringUtils.isEmpty(user.username)) {
         // new user
@@ -1086,7 +1041,7 @@
      // Flag missing repositories
      for (RegistrantAccessPermission permission : set) {
         if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) {
            RepositoryModel rm = GitBlit.self().getRepositoryModel(permission.registrant);
            RepositoryModel rm = getRepositoryModel(permission.registrant);
            if (rm == null) {
               permission.permissionType = PermissionType.MISSING;
               permission.mutable = false;
@@ -1107,7 +1062,7 @@
            set.add(rp);
         }
      }
      List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set);
      Collections.sort(list);
      return list;
@@ -1117,10 +1072,11 @@
    * 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
    */
   @Override
   public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
      List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
      if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
@@ -1140,14 +1096,15 @@
      }
      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
    */
   @Override
   public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
      List<UserModel> users = new ArrayList<UserModel>();
      for (RegistrantAccessPermission up : permissions) {
@@ -1160,15 +1117,16 @@
      }
      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
    */
   @Override
   public List<String> getRepositoryUsers(RepositoryModel repository) {
      return userService.getUsernamesForRepositoryRole(repository.name);
   }
@@ -1176,7 +1134,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,13 +1150,14 @@
   /**
    * Adds/updates a complete user object keyed by username. This method allows
    * for renaming a user.
    *
    *
    * @see IUserService.updateUserModel(String, UserModel)
    * @param username
    * @param user
    * @param isCreate
    * @throws GitBlitException
    */
   @Override
   public void updateUserModel(String username, UserModel user, boolean isCreate)
         throws GitBlitException {
      if (!username.equalsIgnoreCase(user.username)) {
@@ -1207,15 +1166,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 +1191,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,9 +1202,10 @@
   /**
    * Returns the list of available teams that a user or repository may be
    * assigned to.
    *
    *
    * @return the list of teams
    */
   @Override
   public List<TeamModel> getAllTeams() {
      List<TeamModel> teams = userService.getAllTeams();
      return teams;
@@ -1253,22 +1213,24 @@
   /**
    * Returns the TeamModel object for the specified name.
    *
    *
    * @param teamname
    * @return a TeamModel object or null
    */
   @Override
   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
    */
   @Override
   public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
      List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
      for (TeamModel team : userService.getAllTeams()) {
@@ -1280,14 +1242,15 @@
      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
    */
   @Override
   public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
      List<TeamModel> teams = new ArrayList<TeamModel>();
      for (RegistrantAccessPermission tp : permissions) {
@@ -1300,15 +1263,16 @@
      }
      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
    */
   @Override
   public List<String> getRepositoryTeams(RepositoryModel repository) {
      return userService.getTeamnamesForRepositoryRole(repository.name);
   }
@@ -1316,7 +1280,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,11 +1295,12 @@
   /**
    * Updates the TeamModel object for the specified name.
    *
    *
    * @param teamname
    * @param team
    * @param isCreate
    */
   @Override
   public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
         throws GitBlitException {
      if (!teamname.equalsIgnoreCase(team.name)) {
@@ -1352,25 +1317,27 @@
   /**
    * Delete the team object with the specified teamname
    *
    *
    * @see IUserService.deleteTeam(String)
    * @param teamname
    * @return true if successful
    */
   @Override
   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) {
   @Override
   public 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 +1347,10 @@
         }
      }
   }
   /**
    * Removes the repository from the list of cached repositories.
    *
    *
    * @param name
    * @return the model being removed
    */
@@ -1396,23 +1363,24 @@
   /**
    * 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.
    *
    *
    */
   @Override
   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 +1395,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 +1416,15 @@
   /**
    * Returns the list of all repositories available to Gitblit. This method
    * does not consider user access permissions.
    *
    *
    * @return list of all repositories
    */
   @Override
   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 +1437,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();
            if (settings.getBoolean(Keys.web.showRepositorySizes, true)) {
               // 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 +1455,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,22 +1472,28 @@
   /**
    * Returns the JGit repository for the specified name.
    *
    *
    * @param repositoryName
    * @return repository or null
    */
   @Override
   public Repository getRepository(String repositoryName) {
      return getRepository(repositoryName, true);
   }
   /**
    * Returns the JGit repository for the specified name.
    *
    *
    * @param repositoryName
    * @param logError
    * @return repository or null
    */
   @Override
   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 +1502,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,10 +1518,11 @@
   /**
    * Returns the list of repository models that are accessible to the user.
    *
    *
    * @param user
    * @return list of repository models accessible to user
    */
   @Override
   public List<RepositoryModel> getRepositoryModels(UserModel user) {
      long methodStart = System.currentTimeMillis();
      List<String> list = getRepositoryList();
@@ -1574,23 +1541,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,11 +1550,12 @@
   /**
    * Returns a repository model if the repository exists and the user may
    * access the repository.
    *
    *
    * @param user
    * @param repositoryName
    * @return repository model or null
    */
   @Override
   public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
      RepositoryModel model = getRepositoryModel(repositoryName);
      if (model == null) {
@@ -1622,20 +1573,25 @@
   /**
    * Returns the repository model for the specified repository. This method
    * does not consider user access permissions.
    *
    *
    * @param repositoryName
    * @return repository model or null
    */
   @Override
   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 +1610,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,30 +1619,27 @@
         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
    */
   @Override
   public long getStarCount(RepositoryModel repository) {
      long count = 0;
      for (UserModel user : getAllUsers()) {
@@ -1696,7 +1649,7 @@
      }
      return count;
   }
   private void reloadProjectMarkdown(ProjectModel project) {
      // project markdown
      File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : project.name) + "/project.mkd");
@@ -1708,7 +1661,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,24 +1673,24 @@
         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) {
         }
         // project configs
         String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
         String rootName = settings.getString(Keys.web.repositoryRootGroupName, "main");
         ProjectModel rootProject = new ProjectModel(rootName, true);
         Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>();
@@ -1754,9 +1707,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,14 +1717,15 @@
      }
      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
    */
   @Override
   public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
      Map<String, ProjectModel> configs = getProjectConfigs();
@@ -1779,9 +1733,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 +1749,7 @@
         }
         map.get(rootPath).addRepository(model);
      }
      // sort projects, root project first
      List<ProjectModel> projects;
      if (includeUsers) {
@@ -1818,14 +1772,15 @@
      }
      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
    */
   @Override
   public ProjectModel getProjectModel(String name, UserModel user) {
      for (ProjectModel project : getProjectModels(user, true)) {
         if (project.name.equalsIgnoreCase(name)) {
@@ -1834,20 +1789,21 @@
      }
      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
    */
   @Override
   public ProjectModel getProjectModel(String name) {
      Map<String, ProjectModel> configs = getProjectConfigs();
      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,20 +1833,21 @@
         // 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
    */
   @Override
   public List<ProjectModel> getProjectModels(List<RepositoryModel> repositoryModels, boolean includeUsers) {
      Map<String, ProjectModel> projects = new LinkedHashMap<String, ProjectModel>();
      for (RepositoryModel repository : repositoryModels) {
@@ -1924,7 +1881,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 +1889,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 +1904,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 +1929,39 @@
         // 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(
@@ -2014,6 +1978,7 @@
         model.origin = config.getString("remote", "origin", "url");
         if (model.origin != null) {
            model.origin = model.origin.replace('\\', '/');
            model.isMirror = config.getBoolean("remote", "origin", "mirror", false);
         }
         model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
               Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
@@ -2025,7 +1990,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 +2000,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 +2014,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,30 +2025,32 @@
      }
      return model;
   }
   /**
    * Determines if this server has the requested repository.
    *
    *
    * @param n
    * @return true if the repository exists
    */
   @Override
   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
    */
   @Override
   public boolean hasRepository(String repositoryName, boolean caseSensitiveCheck) {
      if (!caseSensitiveCheck && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
         // if we are caching use the cache to determine availability
         // otherwise we end up adding a phantom repository to the cache
         return repositoryListCache.containsKey(repositoryName.toLowerCase());
      }
      }
      Repository r = getRepository(repositoryName, false);
      if (r == null) {
         return false;
@@ -2086,29 +2058,31 @@
      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
    */
   @Override
   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
    */
   @Override
   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 +2098,7 @@
                  }
               }
            }
            if (originModel.originRepository != null) {
               roots.add(originModel.originRepository);
               originModel = repositoryListCache.get(originModel.originRepository);
@@ -2133,7 +2107,7 @@
               originModel = null;
            }
         }
         for (String repository : repositoryListCache.keySet()) {
            if (repository.startsWith(userPath)) {
               RepositoryModel model = repositoryListCache.get(repository);
@@ -2164,14 +2138,15 @@
      // 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
    */
   @Override
   public ForkModel getForkNetwork(String repository) {
      if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
         // find the root, cached
@@ -2191,7 +2166,7 @@
         return root;
      }
   }
   private ForkModel getForkModelFromCache(String repository) {
      RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
      if (model == null) {
@@ -2208,7 +2183,7 @@
      }
      return fork;
   }
   private ForkModel getForkModel(String repository) {
      RepositoryModel model = getRepositoryModel(repository.toLowerCase());
      if (model == null) {
@@ -2227,28 +2202,39 @@
   }
   /**
    * 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);
   @Override
   public long updateLastChangeFields(Repository r, RepositoryModel model) {
      LastChange lc = JGitUtils.getLastChange(r);
      model.lastChange = lc.when;
      model.lastChangeAuthor = lc.who;
      if (!settings.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 +2269,7 @@
            repository.close();
         }
      }
      // close any open index writer/searcher in the Lucene executor
      luceneExecutor.close(repositoryName);
   }
@@ -2293,11 +2279,12 @@
    * 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
    */
   @Override
   public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
      if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
         return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
@@ -2310,7 +2297,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 +2314,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 +2323,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,16 +2349,17 @@
    * Creates/updates the repository model keyed by reopsitoryName. Saves all
    * repository settings in .git/config. This method allows for renaming
    * repositories and will update user access permissions accordingly.
    *
    *
    * All repositories created by this method are bare and automatically have
    * .git appended to their names, which is the standard convention for bare
    * repositories.
    *
    *
    * @param repositoryName
    * @param repository
    * @param isCreate
    * @throws GitBlitException
    */
   @Override
   public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
         boolean isCreate) throws GitBlitException {
      if (gcExecutor.isCollectingGarbage(repositoryName)) {
@@ -2381,7 +2369,7 @@
      Repository r = null;
      String projectPath = StringUtils.getFirstPathElement(repository.name);
      if (!StringUtils.isEmpty(projectPath)) {
         if (projectPath.equalsIgnoreCase(getString(Keys.web.repositoryRootGroupName, "main"))) {
         if (projectPath.equalsIgnoreCase(settings.getString(Keys.web.repositoryRootGroupName, "main"))) {
            // strip leading group name
            repository.name = repository.name.substring(projectPath.length() + 1);
         }
@@ -2398,7 +2386,8 @@
         }
         // create repository
         logger.info("create repository " + repository.name);
         r = JGitUtils.createRepository(repositoriesFolder, repository.name);
         String shared = settings.getString(Keys.git.createRepositoriesShared, "FALSE");
         r = JGitUtils.createRepository(repositoriesFolder, repository.name, shared);
      } else {
         // rename repository
         if (!repositoryName.equalsIgnoreCase(repository.name)) {
@@ -2436,7 +2425,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 +2443,7 @@
                  rf.close();
               }
            }
            // update this repository's origin's fork list
            if (!StringUtils.isEmpty(repository.originRepository)) {
               RepositoryModel origin = repositoryListCache.get(repository.originRepository);
@@ -2477,10 +2466,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,31 +2486,36 @@
            }
         }
         // Adjust permissions in case we updated the config files
         JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "config"),
               settings.getString(Keys.git.createRepositoriesShared, "FALSE"));
         JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "HEAD"),
               settings.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
    *            the Gitblit repository model
    */
   @Override
   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);
      config.setBoolean(Constants.CONFIG_GITBLIT, null, "useIncrementalPushTags", repository.useIncrementalPushTags);
      if (StringUtils.isEmpty(repository.incrementalPushTagPrefix) ||
            repository.incrementalPushTagPrefix.equals(settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r"))) {
@@ -2526,7 +2529,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 +2551,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 +2589,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,10 +2606,11 @@
   /**
    * Deletes the repository from the file system and removes the repository
    * permission from all repository users.
    *
    *
    * @param model
    * @return true if successful
    */
   @Override
   public boolean deleteRepositoryModel(RepositoryModel model) {
      return deleteRepository(model.name);
   }
@@ -2605,16 +2618,17 @@
   /**
    * Deletes the repository from the file system and removes the repository
    * permission from all repository users.
    *
    *
    * @param repositoryName
    * @return true if successful
    */
   @Override
   public boolean deleteRepository(String repositoryName) {
      try {
         closeRepository(repositoryName);
         // clear the repository cache
         clearRepositoryMetadataCache(repositoryName);
         RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
         if (model != null && !ArrayUtils.isEmpty(model.forks)) {
            resetRepositoryListCache();
@@ -2637,13 +2651,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,27 +2725,28 @@
         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() {
      return scheduledExecutor;
   }
   public static boolean canFederate() {
      String passphrase = getString(Keys.federation.passphrase, "");
   @Override
   public boolean canFederate() {
      String passphrase = settings.getString(Keys.federation.passphrase, "");
      return !StringUtils.isEmpty(passphrase);
   }
@@ -2727,9 +2786,10 @@
   /**
    * Returns the list of federated gitblit instances that this instance will
    * try to pull.
    *
    *
    * @return list of registered gitblit instances
    */
   @Override
   public List<FederationModel> getFederationRegistrations() {
      if (federationRegistrations.isEmpty()) {
         federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
@@ -2739,11 +2799,12 @@
   /**
    * Retrieve the specified federation registration.
    *
    *
    * @param name
    *            the name of the registration
    * @return a federation registration
    */
   @Override
   public FederationModel getFederationRegistration(String url, String name) {
      // check registrations
      for (FederationModel r : getFederationRegistrations()) {
@@ -2763,9 +2824,10 @@
   /**
    * Returns the list of federation sets.
    *
    *
    * @return list of federation sets
    */
   @Override
   public List<FederationSet> getFederationSets(String gitblitUrl) {
      List<FederationSet> list = new ArrayList<FederationSet>();
      // generate standard tokens
@@ -2786,9 +2848,10 @@
   /**
    * Returns the list of possible federation tokens for this Gitblit instance.
    *
    *
    * @return list of federation tokens
    */
   @Override
   public List<String> getFederationTokens() {
      List<String> tokens = new ArrayList<String>();
      // generate standard tokens
@@ -2804,20 +2867,22 @@
   /**
    * Returns the specified federation token for this Gitblit instance.
    *
    *
    * @param type
    * @return a federation token
    */
   @Override
   public String getFederationToken(FederationToken type) {
      return getFederationToken(type.name());
   }
   /**
    * Returns the specified federation token for this Gitblit instance.
    *
    *
    * @param value
    * @return a federation token
    */
   @Override
   public String getFederationToken(String value) {
      String passphrase = settings.getString(Keys.federation.passphrase, "");
      return StringUtils.getSHA1(passphrase + "-" + value);
@@ -2826,11 +2891,12 @@
   /**
    * 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
    */
   @Override
   public boolean validateFederationRequest(FederationRequest req, String token) {
      String all = getFederationToken(FederationToken.ALL);
      String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
@@ -2852,13 +2918,14 @@
   /**
    * Acknowledge and cache the status of a remote Gitblit instance.
    *
    *
    * @param identification
    *            the identification of the pulling Gitblit instance
    * @param registration
    *            the registration from the pulling Gitblit instance
    * @return true if acknowledged
    */
   @Override
   public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
      // reset the url to the identification of the pulling Gitblit instance
      registration.url = identification;
@@ -2872,9 +2939,10 @@
   /**
    * Returns the list of registration results.
    *
    *
    * @return the list of registration results
    */
   @Override
   public List<FederationModel> getFederationResultRegistrations() {
      return new ArrayList<FederationModel>(federationPullResults.values());
   }
@@ -2882,7 +2950,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
@@ -2890,6 +2958,7 @@
    *            administrators
    * @return true if the proposal was submitted
    */
   @Override
   public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
      // convert proposal to json
      String json = JsonUtils.toJsonString(proposal);
@@ -2914,9 +2983,10 @@
   /**
    * Returns the list of pending federation proposals
    *
    *
    * @return list of federation proposals
    */
   @Override
   public List<FederationProposal> getPendingFederationProposals() {
      List<FederationProposal> list = new ArrayList<FederationProposal>();
      File folder = getProposalsFolder();
@@ -2940,16 +3010,17 @@
   /**
    * Get repositories for the specified token.
    *
    *
    * @param gitblitUrl
    *            the base url of this gitblit instance
    * @param token
    *            the federation token
    * @return a map of <cloneurl, RepositoryModel>
    */
   @Override
   public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
      Map<String, String> federationSets = new HashMap<String, String>();
      for (String set : getStrings(Keys.federation.sets)) {
      for (String set : settings.getStrings(Keys.federation.sets)) {
         federationSets.put(getFederationToken(set), set);
      }
@@ -2999,12 +3070,13 @@
   /**
    * Creates a proposal from the token.
    *
    *
    * @param gitblitUrl
    *            the url of this Gitblit instance
    * @param token
    * @return a potential proposal
    */
   @Override
   public FederationProposal createFederationProposal(String gitblitUrl, String token) {
      FederationToken tokenType = FederationToken.REPOSITORIES;
      for (FederationToken type : FederationToken.values()) {
@@ -3021,10 +3093,11 @@
   /**
    * Returns the proposal identified by the supplied token.
    *
    *
    * @param token
    * @return the specified proposal or null
    */
   @Override
   public FederationProposal getPendingFederationProposal(String token) {
      List<FederationProposal> list = getPendingFederationProposals();
      for (FederationProposal proposal : list) {
@@ -3037,11 +3110,12 @@
   /**
    * Deletes a pending federation proposal.
    *
    *
    * @param a
    *            proposal
    * @return true if the proposal was deleted
    */
   @Override
   public boolean deletePendingFederationProposal(FederationProposal proposal) {
      File folder = getProposalsFolder();
      File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
@@ -3051,11 +3125,12 @@
   /**
    * Returns the list of all Groovy push hook scripts. Script files must have
    * .groovy extension
    *
    *
    * @return list of available hook scripts
    */
   @Override
   public List<String> getAllScripts() {
      File groovyFolder = getGroovyScriptsFolder();
      File groovyFolder = getHooksFolder();
      File[] files = groovyFolder.listFiles(new FileFilter() {
         @Override
         public boolean accept(File pathname) {
@@ -3075,15 +3150,16 @@
   /**
    * 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
    */
   @Override
   public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
      Set<String> scripts = new LinkedHashSet<String>();
      // Globals
      for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
      for (String script : settings.getStrings(Keys.groovy.preReceiveScripts)) {
         if (script.endsWith(".groovy")) {
            scripts.add(script.substring(0, script.lastIndexOf('.')));
         } else {
@@ -3107,11 +3183,12 @@
    * 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
    */
   @Override
   public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
      Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
@@ -3128,15 +3205,16 @@
   /**
    * 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
    */
   @Override
   public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
      Set<String> scripts = new LinkedHashSet<String>();
      // Global Scripts
      for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
      for (String script : settings.getStrings(Keys.groovy.postReceiveScripts)) {
         if (script.endsWith(".groovy")) {
            scripts.add(script.substring(0, script.lastIndexOf('.')));
         } else {
@@ -3159,11 +3237,12 @@
    * 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
    */
   @Override
   public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
      Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
@@ -3176,27 +3255,29 @@
      }
      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) {
   @Override
   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
    */
   @Override
   public void sendMailToAdministrators(String subject, String message) {
      List<String> toAddresses = settings.getStrings(Keys.mail.adminAddresses);
      sendMail(subject, message, toAddresses);
@@ -3204,22 +3285,24 @@
   /**
    * Notify users by email of something.
    *
    *
    * @param subject
    * @param message
    * @param toAddresses
    */
   @Override
   public void sendMail(String subject, String message, Collection<String> toAddresses) {
      this.sendMail(subject, message, toAddresses.toArray(new String[0]));
   }
   /**
    * Notify users by email of something.
    *
    *
    * @param subject
    * @param message
    * @param toAddresses
    */
   @Override
   public void sendMail(String subject, String message, String... toAddresses) {
      if (toAddresses == null || toAddresses.length == 0) {
         logger.debug(MessageFormat.format("Dropping message {0} because there are no recipients", subject));
@@ -3229,16 +3312,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,22 +3331,24 @@
   /**
    * Notify users by email of something.
    *
    *
    * @param subject
    * @param message
    * @param toAddresses
    */
   @Override
   public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {
      this.sendHtmlMail(subject, message, toAddresses.toArray(new String[0]));
   }
   /**
    * Notify users by email of something.
    *
    *
    * @param subject
    * @param message
    * @param toAddresses
    */
   @Override
   public void sendHtmlMail(String subject, String message, String... toAddresses) {
      if (toAddresses == null || toAddresses.length == 0) {
         logger.debug(MessageFormat.format("Dropping message {0} because there are no recipients", subject));
@@ -3273,12 +3358,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,9 +3377,10 @@
   /**
    * Returns the descriptions/comments of the Gitblit config settings.
    *
    *
    * @return SettingsModel
    */
   @Override
   public ServerSettings getSettingsModel() {
      // ensure that the current values are updated in the setting models
      for (String key : settings.getAllKeys(null)) {
@@ -3305,7 +3391,7 @@
            setting.name = key;
            settingsModel.add(setting);
         }
         setting.currentValue = settings.getString(key, "");
         setting.currentValue = settings.getString(key, "");
      }
      settingsModel.pushScripts = getAllScripts();
      return settingsModel;
@@ -3315,7 +3401,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 +3470,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 +3487,12 @@
      mailExecutor = new MailExecutor(settings);
      luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
      gcExecutor = new GCExecutor(settings);
      mirrorExecutor = new MirrorExecutor(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,11 +3501,11 @@
         logger.info("Identifying available repositories...");
         getRepositoryList();
      }
      logTimezone("JVM", TimeZone.getDefault());
      logTimezone(Constants.NAME, getTimezone());
      serverStatus = new ServerStatus(isGO());
      serverStatus = new ServerStatus(goSettings != null);
      if (this.userService == null) {
         String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
@@ -3428,14 +3519,15 @@
         }
         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();
      configureMirrorExecutor();
      if (startFederation) {
         configureFederation();
      }
@@ -3443,10 +3535,8 @@
      configureFanout();
      configureGitDaemon();
      configureCommitCache();
      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 +3545,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 +3570,26 @@
         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 configureMirrorExecutor() {
      if (mirrorExecutor.isReady()) {
         int mins = TimeUtils.convertFrequencyToMinutes(settings.getString(Keys.git.mirrorPeriod, "30 mins"));
         if (mins < 5) {
            mins = 5;
         }
         int delay = 1;
         scheduledExecutor.scheduleAtFixedRate(mirrorExecutor, delay, mins,  TimeUnit.MINUTES);
         logger.info("Mirror executor is scheduled to fetch updates every {} minutes.", mins);
         logger.info("Next scheduled mirror fetch is in {} minutes", delay);
      }
   }
   protected void configureJGit() {
      // Configure JGit
      WindowCacheConfig cfg = new WindowCacheConfig();
@@ -3510,7 +3613,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 +3641,7 @@
         fanoutService.start();
      }
   }
   protected void configureGitDaemon() {
      int port = settings.getInteger(Keys.git.daemonPort, 0);
      String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
@@ -3552,7 +3655,7 @@
         }
      }
   }
   protected void configureCommitCache() {
      int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
      if (daysToCache <= 0) {
@@ -3566,7 +3669,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 +3691,11 @@
               daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
      }
   }
   protected final Logger getLogger() {
      return logger;
   }
   protected final ScheduledExecutorService getScheduledExecutor() {
      return scheduledExecutor;
   }
@@ -3600,7 +3703,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 +3714,7 @@
   /**
    * Configure Gitblit from the web.xml, if no configuration has already been
    * specified.
    *
    *
    * @see ServletContextListener.contextInitialize(ServletContextEvent)
    */
   @Override
@@ -3624,7 +3727,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 +3736,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 +3752,19 @@
                  }
               }
            }
            // disable Git daemon on Express - we can't bind 9418 and we
            // can't port-forward to the daemon
            webxmlSettings.overrideSetting(Keys.git.daemonPort, 0);
            // 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 +3774,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 +3797,19 @@
            }
            // delegate all config to baseFolder/gitblit.properties file
            FileSettings settings = new FileSettings(localSettings.getAbsolutePath());
            FileSettings settings = new FileSettings(localSettings.getAbsolutePath());
            configureContext(settings, base, true);
         }
         // WAR or Express is likely to be running on a Tomcat.
         // Test for the forward-slash/%2F issue and auto-adjust settings.
         ContainerUtils.CVE_2007_0450.test(settings);
      }
      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
@@ -3744,6 +3867,7 @@
      scheduledExecutor.shutdownNow();
      luceneExecutor.close();
      gcExecutor.close();
      mirrorExecutor.close();
      if (fanoutService != null) {
         fanoutService.stop();
      }
@@ -3751,21 +3875,23 @@
         gitDaemon.stop();
      }
   }
   /**
    *
    *
    * @return true if we are running the gc executor
    */
   @Override
   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
    */
   @Override
   public boolean isCollectingGarbage(String repositoryName) {
      return gcExecutor.isCollectingGarbage(repositoryName);
   }
@@ -3773,15 +3899,16 @@
   /**
    * 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
    */
   @Override
   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 +3961,7 @@
         }
         cloneTeams.add(cloneTeam);
      }
      userService.updateTeamModels(cloneTeams);
      userService.updateTeamModels(cloneTeams);
      // add this clone to the cached model
      addToCachedRepositoryList(cloneModel);
@@ -3844,10 +3971,92 @@
   /**
    * Allow to understand if GitBlit supports and is configured to allow
    * cookie-based authentication.
    *
    *
    * @return status of Cookie authentication enablement.
    */
   public boolean allowCookieAuthentication() {
      return GitBlit.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
   @Override
   public boolean supportsCookies() {
      return settings.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
   }
   @Override
   public String getCookie(UserModel model) {
      return userService.getCookie(model);
   }
   @Override
   public UserModel authenticate(char[] cookie) {
      return userService.authenticate(cookie);
   }
   @Override
   public boolean updateUserModel(UserModel model) {
      return userService.updateUserModel(model);
   }
   @Override
   public boolean updateUserModels(Collection<UserModel> models) {
      return userService.updateUserModels(models);
   }
   @Override
   public boolean updateUserModel(String username, UserModel model) {
      return userService.updateUserModel(username, model);
   }
   @Override
   public boolean deleteUserModel(UserModel model) {
      return userService.deleteUserModel(model);
   }
   @Override
   public List<String> getAllTeamNames() {
      return userService.getAllTeamNames();
   }
   @Override
   public List<String> getTeamnamesForRepositoryRole(String role) {
      return userService.getTeamnamesForRepositoryRole(role);
   }
   @Override
   public boolean updateTeamModel(TeamModel model) {
      return userService.updateTeamModel(model);
   }
   @Override
   public boolean updateTeamModels(Collection<TeamModel> models) {
      return userService.updateTeamModels(models);
   }
   @Override
   public boolean updateTeamModel(String teamname, TeamModel model) {
      return userService.updateTeamModel(teamname, model);
   }
   @Override
   public boolean deleteTeamModel(TeamModel model) {
      return userService.deleteTeamModel(model);
   }
   @Override
   public List<String> getUsernamesForRepositoryRole(String role) {
      return userService.getUsernamesForRepositoryRole(role);
   }
   @Override
   public boolean renameRepositoryRole(String oldRole, String newRole) {
      return userService.renameRepositoryRole(oldRole, newRole);
   }
   @Override
   public boolean deleteRepositoryRole(String role) {
      return userService.deleteRepositoryRole(role);
   }
   @Override
   public void logout(HttpServletResponse response, UserModel user) {
      setCookie(response,  null);
      userService.logout(user);
   }
}