From 269c5043ab8f66f67d5719ac5149a436ca1baa2b Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 29 Nov 2013 11:05:51 -0500
Subject: [PATCH] Extract Federation, Gitblit and Services manager from GitBlit singleton

---
 src/main/java/com/gitblit/manager/IServicesManager.java      |   21 
 src/main/java/com/gitblit/FederationPullExecutor.java        | 1019 ++++++++---------
 src/main/java/com/gitblit/manager/UserManager.java           |    6 
 src/main/java/com/gitblit/GitBlit.java                       | 1106 +-----------------
 src/main/java/com/gitblit/manager/IFederationManager.java    |    2 
 src/main/java/com/gitblit/manager/SessionManager.java        |    4 
 src/main/java/com/gitblit/manager/RuntimeManager.java        |   22 
 src/main/java/com/gitblit/manager/ProjectManager.java        |    4 
 src/main/java/com/gitblit/DaggerModule.java                  |   45 
 src/main/java/com/gitblit/FederationClient.java              |   35 
 src/main/java/com/gitblit/manager/GitblitManager.java        |  463 +++++++
 src/main/java/com/gitblit/manager/ServicesManager.java       |  188 +++
 src/main/java/com/gitblit/manager/FederationManager.java     |  454 +++++++
 src/main/java/com/gitblit/Gitblit.java                       |   19 
 src/main/java/com/gitblit/manager/IGitblitManager.java       |    2 
 src/main/java/com/gitblit/manager/RepositoryManager.java     |   25 
 src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java |    2 
 src/main/java/com/gitblit/manager/IManager.java              |    3 
 src/main/java/com/gitblit/manager/NotificationManager.java   |   11 
 19 files changed, 1,820 insertions(+), 1,611 deletions(-)

diff --git a/src/main/java/com/gitblit/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java
index 8cd9c8b..0cbb739 100644
--- a/src/main/java/com/gitblit/DaggerModule.java
+++ b/src/main/java/com/gitblit/DaggerModule.java
@@ -20,18 +20,22 @@
 import org.apache.wicket.protocol.http.WebApplication;
 
 import com.gitblit.git.GitServlet;
+import com.gitblit.manager.FederationManager;
+import com.gitblit.manager.GitblitManager;
 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.IServicesManager;
 import com.gitblit.manager.ISessionManager;
 import com.gitblit.manager.IUserManager;
 import com.gitblit.manager.NotificationManager;
 import com.gitblit.manager.ProjectManager;
 import com.gitblit.manager.RepositoryManager;
 import com.gitblit.manager.RuntimeManager;
+import com.gitblit.manager.ServicesManager;
 import com.gitblit.manager.SessionManager;
 import com.gitblit.manager.UserManager;
 import com.gitblit.wicket.GitBlitWebApp;
@@ -47,6 +51,7 @@
  *
  */
 @Module(
+	library = true,
 	injects = {
 			IStoredSettings.class,
 
@@ -59,6 +64,7 @@
 			IProjectManager.class,
 			IGitblitManager.class,
 			IFederationManager.class,
+			IServicesManager.class,
 
 			// the monolithic manager
 			Gitblit.class,
@@ -84,13 +90,6 @@
 	}
 )
 public class DaggerModule {
-
-	final GitBlit gitblit;
-
-	// HACK but necessary for now
-	public DaggerModule(GitBlit gitblit) {
-		this.gitblit = gitblit;
-	}
 
 	@Provides @Singleton IStoredSettings provideSettings() {
 		return new FileSettings();
@@ -137,12 +136,28 @@
 				repositoryManager);
 	}
 
-	@Provides @Singleton IGitblitManager provideGitblitManager() {
-		return gitblit;
+	@Provides @Singleton IFederationManager provideFederationManager(
+			IRuntimeManager runtimeManager,
+			INotificationManager notificationManager,
+			IUserManager userManager,
+			IRepositoryManager repositoryManager) {
+
+		return new FederationManager(
+				runtimeManager,
+				notificationManager,
+				userManager,
+				repositoryManager);
 	}
 
-	@Provides @Singleton IFederationManager provideFederationManager() {
-		return gitblit;
+	@Provides @Singleton IGitblitManager provideGitblitManager(
+			IRuntimeManager runtimeManager,
+			IUserManager userManager,
+			IRepositoryManager repositoryManager) {
+
+		return new GitblitManager(
+				runtimeManager,
+				userManager,
+				repositoryManager);
 	}
 
 	@Provides @Singleton Gitblit provideGitblit(
@@ -162,8 +177,12 @@
 				sessionManager,
 				repositoryManager,
 				projectManager,
-				federationManager,
-				gitblitManager);
+				gitblitManager,
+				federationManager);
+	}
+
+	@Provides @Singleton IServicesManager provideServicesManager(Gitblit gitblit) {
+		return new ServicesManager(gitblit);
 	}
 
 	@Provides @Singleton WebApplication provideWebApplication(
diff --git a/src/main/java/com/gitblit/FederationClient.java b/src/main/java/com/gitblit/FederationClient.java
index 862b64c..f32fa0f 100644
--- a/src/main/java/com/gitblit/FederationClient.java
+++ b/src/main/java/com/gitblit/FederationClient.java
@@ -23,6 +23,11 @@
 import com.beust.jcommander.Parameter;
 import com.beust.jcommander.ParameterException;
 import com.beust.jcommander.Parameters;
+import com.gitblit.manager.FederationManager;
+import com.gitblit.manager.NotificationManager;
+import com.gitblit.manager.RepositoryManager;
+import com.gitblit.manager.RuntimeManager;
+import com.gitblit.manager.UserManager;
 import com.gitblit.models.FederationModel;
 import com.gitblit.utils.FederationUtils;
 import com.gitblit.utils.StringUtils;
@@ -53,7 +58,7 @@
 		}
 
 		File regFile = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.registrationsFile);
-		IStoredSettings settings = new FileSettings(regFile.getAbsolutePath());
+		FileSettings settings = new FileSettings(regFile.getAbsolutePath());
 		List<FederationModel> registrations = new ArrayList<FederationModel>();
 		if (StringUtils.isEmpty(params.url)) {
 			registrations.addAll(FederationUtils.getFederationRegistrations(settings));
@@ -83,14 +88,23 @@
 		}
 
 		// configure the Gitblit singleton for minimal, non-server operation
-		GitBlit gitblit = new GitBlit(settings, baseFolder);
-		gitblit.beforeServletInjection(null); // XXX broken
-		FederationPullExecutor executor = new FederationPullExecutor(registrations, params.isDaemon);
-		executor.run();
-		if (!params.isDaemon) {
-			System.out.println("Finished.");
-			System.exit(0);
-		}
+		RuntimeManager runtime = new RuntimeManager(settings);
+		runtime.setBaseFolder(baseFolder);
+		NotificationManager notifications = new NotificationManager(settings).start();
+		UserManager users = new UserManager(runtime).start();
+		RepositoryManager repositories = new RepositoryManager(runtime, users).start();
+		FederationManager federation = new FederationManager(runtime, notifications, users, repositories).start();
+
+		FederationPullExecutor puller = new FederationPullExecutor(federation.getFederationRegistrations()) {
+			@Override
+			public void reschedule(FederationModel registration) {
+				// NOOP
+			}
+		};
+		puller.run();
+
+		System.out.println("Finished.");
+		System.exit(0);
 	}
 
 	private static void usage(JCommander jc, ParameterException t) {
@@ -115,9 +129,6 @@
 
 		@Parameter(names = { "--registrations" }, description = "Gitblit Federation Registrations File", required = false)
 		public String registrationsFile = "${baseFolder}/federation.properties";
-
-		@Parameter(names = { "--daemon" }, description = "Runs in daemon mode to schedule and pull repositories", required = false)
-		public boolean isDaemon;
 
 		@Parameter(names = { "--url" }, description = "URL of Gitblit instance to mirror from", required = false)
 		public String url;
diff --git a/src/main/java/com/gitblit/FederationPullExecutor.java b/src/main/java/com/gitblit/FederationPullExecutor.java
index e9a604d..bbe73cf 100644
--- a/src/main/java/com/gitblit/FederationPullExecutor.java
+++ b/src/main/java/com/gitblit/FederationPullExecutor.java
@@ -1,533 +1,486 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit;
-
-import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.Constants.AccessPermission;
-import com.gitblit.Constants.FederationPullStatus;
-import com.gitblit.Constants.FederationStrategy;
-import com.gitblit.GitBlitException.ForbiddenException;
-import com.gitblit.manager.IGitblitManager;
-import com.gitblit.manager.INotificationManager;
-import com.gitblit.manager.IRepositoryManager;
-import com.gitblit.manager.IRuntimeManager;
-import com.gitblit.manager.IUserManager;
-import com.gitblit.models.FederationModel;
-import com.gitblit.models.RefModel;
-import com.gitblit.models.RepositoryModel;
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.ArrayUtils;
-import com.gitblit.utils.FederationUtils;
-import com.gitblit.utils.FileUtils;
-import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.JGitUtils.CloneResult;
-import com.gitblit.utils.StringUtils;
-import com.gitblit.utils.TimeUtils;
-
-/**
- * FederationPullExecutor pulls repository updates and, optionally, user
- * accounts and server settings from registered Gitblit instances.
- */
-public class FederationPullExecutor implements Runnable {
-
-	private final Logger logger = LoggerFactory.getLogger(FederationPullExecutor.class);
-
-	private final List<FederationModel> registrations;
-
-	private final boolean isDaemon;
-
-	/**
-	 * Constructor for specifying a single federation registration. This
-	 * constructor is used to schedule the next pull execution.
-	 *
-	 * @param registration
-	 */
-	private FederationPullExecutor(FederationModel registration) {
-		this(Arrays.asList(registration), true);
-	}
-
-	/**
-	 * Constructor to specify a group of federation registrations. This is
-	 * normally used at startup to pull and then schedule the next update based
-	 * on each registrations frequency setting.
-	 *
-	 * @param registrations
-	 * @param isDaemon
-	 *            if true, registrations are rescheduled in perpetuity. if
-	 *            false, the federation pull operation is executed once.
-	 */
-	public FederationPullExecutor(List<FederationModel> registrations, boolean isDaemon) {
-		this.registrations = registrations;
-		this.isDaemon = isDaemon;
-	}
-
-	/**
-	 * Run method for this pull executor.
-	 */
-	@Override
-	public void run() {
-		for (FederationModel registration : registrations) {
-			FederationPullStatus was = registration.getLowestStatus();
-			try {
-				Date now = new Date(System.currentTimeMillis());
-				pull(registration);
-				sendStatusAcknowledgment(registration);
-				registration.lastPull = now;
-				FederationPullStatus is = registration.getLowestStatus();
-				if (is.ordinal() < was.ordinal()) {
-					// the status for this registration has downgraded
-					logger.warn("Federation pull status of {0} is now {1}", registration.name,
-							is.name());
-					if (registration.notifyOnError) {
-						String message = "Federation pull of " + registration.name + " @ "
-								+ registration.url + " is now at " + is.name();
-						INotificationManager mailManager = GitBlit.getManager(INotificationManager.class);
-						mailManager
-								.sendMailToAdministrators(
-										"Pull Status of " + registration.name + " is " + is.name(),
-										message);
-					}
-				}
-			} catch (Throwable t) {
-				logger.error(MessageFormat.format(
-						"Failed to pull from federated gitblit ({0} @ {1})", registration.name,
-						registration.url), t);
-			} finally {
-				if (isDaemon) {
-					schedule(registration);
-				}
-			}
-		}
-	}
-
-	/**
-	 * Mirrors a repository and, optionally, the server's users, and/or
-	 * configuration settings from a origin Gitblit instance.
-	 *
-	 * @param registration
-	 * @throws Exception
-	 */
-	private void pull(FederationModel registration) throws Exception {
-		Map<String, RepositoryModel> repositories = FederationUtils.getRepositories(registration,
-				true);
-		String registrationFolder = registration.folder.toLowerCase().trim();
-		// confirm valid characters in server alias
-		Character c = StringUtils.findInvalidCharacter(registrationFolder);
-		if (c != null) {
-			logger.error(MessageFormat
-					.format("Illegal character ''{0}'' in folder name ''{1}'' of federation registration {2}!",
-							c, registrationFolder, registration.name));
-			return;
-		}
-		IRepositoryManager repositoryManager = GitBlit.getManager(IRepositoryManager.class);
-		File repositoriesFolder = repositoryManager.getRepositoriesFolder();
-		File registrationFolderFile = new File(repositoriesFolder, registrationFolder);
-		registrationFolderFile.mkdirs();
-
-		// Clone/Pull the repository
-		for (Map.Entry<String, RepositoryModel> entry : repositories.entrySet()) {
-			String cloneUrl = entry.getKey();
-			RepositoryModel repository = entry.getValue();
-			if (!repository.hasCommits) {
-				logger.warn(MessageFormat.format(
-						"Skipping federated repository {0} from {1} @ {2}. Repository is EMPTY.",
-						repository.name, registration.name, registration.url));
-				registration.updateStatus(repository, FederationPullStatus.SKIPPED);
-				continue;
-			}
-
-			// Determine local repository name
-			String repositoryName;
-			if (StringUtils.isEmpty(registrationFolder)) {
-				repositoryName = repository.name;
-			} else {
-				repositoryName = registrationFolder + "/" + repository.name;
-			}
-
-			if (registration.bare) {
-				// bare repository, ensure .git suffix
-				if (!repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
-					repositoryName += DOT_GIT_EXT;
-				}
-			} else {
-				// normal repository, strip .git suffix
-				if (repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
-					repositoryName = repositoryName.substring(0,
-							repositoryName.indexOf(DOT_GIT_EXT));
-				}
-			}
-
-			// confirm that the origin of any pre-existing repository matches
-			// the clone url
-			String fetchHead = null;
-			Repository existingRepository = repositoryManager.getRepository(repositoryName);
-
-			if (existingRepository == null && repositoryManager.isCollectingGarbage(repositoryName)) {
-				logger.warn(MessageFormat.format("Skipping local repository {0}, busy collecting garbage", repositoryName));
-				continue;
-			}
-
-			if (existingRepository != null) {
-				StoredConfig config = existingRepository.getConfig();
-				config.load();
-				String origin = config.getString("remote", "origin", "url");
-				RevCommit commit = JGitUtils.getCommit(existingRepository,
-						org.eclipse.jgit.lib.Constants.FETCH_HEAD);
-				if (commit != null) {
-					fetchHead = commit.getName();
-				}
-				existingRepository.close();
-				if (!origin.startsWith(registration.url)) {
-					logger.warn(MessageFormat
-							.format("Skipping federated repository {0} from {1} @ {2}. Origin does not match, consider EXCLUDING.",
-									repository.name, registration.name, registration.url));
-					registration.updateStatus(repository, FederationPullStatus.SKIPPED);
-					continue;
-				}
-			}
-
-			// clone/pull this repository
-			CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(
-					Constants.FEDERATION_USER, registration.token);
-			logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}",
-					repository.name, registration.name, registration.url));
-
-			CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name,
-					cloneUrl, registration.bare, credentials);
-			Repository r = repositoryManager.getRepository(repositoryName);
-			RepositoryModel rm = repositoryManager.getRepositoryModel(repositoryName);
-			repository.isFrozen = registration.mirror;
-			if (result.createdRepository) {
-				// default local settings
-				repository.federationStrategy = FederationStrategy.EXCLUDE;
-				repository.isFrozen = registration.mirror;
-				repository.showRemoteBranches = !registration.mirror;
-				logger.info(MessageFormat.format("     cloning {0}", repository.name));
-				registration.updateStatus(repository, FederationPullStatus.MIRRORED);
-			} else {
-				// fetch and update
-				boolean fetched = false;
-				RevCommit commit = JGitUtils.getCommit(r, org.eclipse.jgit.lib.Constants.FETCH_HEAD);
-				String newFetchHead = commit.getName();
-				fetched = fetchHead == null || !fetchHead.equals(newFetchHead);
-
-				if (registration.mirror) {
-					// mirror
-					if (fetched) {
-						// update local branches to match the remote tracking branches
-						for (RefModel ref : JGitUtils.getRemoteBranches(r, false, -1)) {
-							if (ref.displayName.startsWith("origin/")) {
-								String branch = org.eclipse.jgit.lib.Constants.R_HEADS
-										+ ref.displayName.substring(ref.displayName.indexOf('/') + 1);
-								String hash = ref.getReferencedObjectId().getName();
-
-								JGitUtils.setBranchRef(r, branch, hash);
-								logger.info(MessageFormat.format("     resetting {0} of {1} to {2}", branch,
-										repository.name, hash));
-							}
-						}
-
-						String newHead;
-						if (StringUtils.isEmpty(repository.HEAD)) {
-							newHead = newFetchHead;
-						} else {
-							newHead = repository.HEAD;
-						}
-						JGitUtils.setHEADtoRef(r, newHead);
-						logger.info(MessageFormat.format("     resetting HEAD of {0} to {1}",
-								repository.name, newHead));
-						registration.updateStatus(repository, FederationPullStatus.MIRRORED);
-					} else {
-						// indicate no commits pulled
-						registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
-					}
-				} else {
-					// non-mirror
-					if (fetched) {
-						// indicate commits pulled to origin/master
-						registration.updateStatus(repository, FederationPullStatus.PULLED);
-					} else {
-						// indicate no commits pulled
-						registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
-					}
-				}
-
-				// preserve local settings
-				repository.isFrozen = rm.isFrozen;
-				repository.federationStrategy = rm.federationStrategy;
-
-				// merge federation sets
-				Set<String> federationSets = new HashSet<String>();
-				if (rm.federationSets != null) {
-					federationSets.addAll(rm.federationSets);
-				}
-				if (repository.federationSets != null) {
-					federationSets.addAll(repository.federationSets);
-				}
-				repository.federationSets = new ArrayList<String>(federationSets);
-
-				// merge indexed branches
-				Set<String> indexedBranches = new HashSet<String>();
-				if (rm.indexedBranches != null) {
-					indexedBranches.addAll(rm.indexedBranches);
-				}
-				if (repository.indexedBranches != null) {
-					indexedBranches.addAll(repository.indexedBranches);
-				}
-				repository.indexedBranches = new ArrayList<String>(indexedBranches);
-
-			}
-			// only repositories that are actually _cloned_ from the origin
-			// Gitblit repository are marked as federated. If the origin
-			// is from somewhere else, these repositories are not considered
-			// "federated" repositories.
-			repository.isFederated = cloneUrl.startsWith(registration.url);
-
-			repositoryManager.updateConfiguration(r, repository);
-			r.close();
-		}
-
-		IUserManager userManager = GitBlit.getManager(IUserManager.class);
-		IGitblitManager gitblitManager = GitBlit.getManager(IGitblitManager.class);
-		IUserService userService = null;
-
-		try {
-			// Pull USERS
-			// TeamModels are automatically pulled because they are contained
-			// within the UserModel. The UserService creates unknown teams
-			// and updates existing teams.
-			Collection<UserModel> users = FederationUtils.getUsers(registration);
-			if (users != null && users.size() > 0) {
-				File realmFile = new File(registrationFolderFile, registration.name + "_users.conf");
-				realmFile.delete();
-				userService = new ConfigUserService(realmFile);
-				for (UserModel user : users) {
-					userService.updateUserModel(user.username, user);
-
-					// merge the origin permissions and origin accounts into
-					// the user accounts of this Gitblit instance
-					if (registration.mergeAccounts) {
-						// reparent all repository permissions if the local
-						// repositories are stored within subfolders
-						if (!StringUtils.isEmpty(registrationFolder)) {
-							if (user.permissions != null) {
-								// pulling from >= 1.2 version
-								Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
-								user.permissions.clear();
-								for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
-									user.setRepositoryPermission(registrationFolder + "/" + entry.getKey(), entry.getValue());
-								}
-							} else {
-								// pulling from <= 1.1 version
-								List<String> permissions = new ArrayList<String>(user.repositories);
-								user.repositories.clear();
-								for (String permission : permissions) {
-									user.addRepositoryPermission(registrationFolder + "/" + permission);
-								}
-							}
-						}
-
-						// insert new user or update local user
-						UserModel localUser = userManager.getUserModel(user.username);
-						if (localUser == null) {
-							// create new local user
-							gitblitManager.updateUserModel(user.username, user, true);
-						} else {
-							// update repository permissions of local user
-							if (user.permissions != null) {
-								// pulling from >= 1.2 version
-								Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
-								for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
-									localUser.setRepositoryPermission(entry.getKey(), entry.getValue());
-								}
-							} else {
-								// pulling from <= 1.1 version
-								for (String repository : user.repositories) {
-									localUser.addRepositoryPermission(repository);
-								}
-							}
-							localUser.password = user.password;
-							localUser.canAdmin = user.canAdmin;
-							gitblitManager.updateUserModel(localUser.username, localUser, false);
-						}
-
-						for (String teamname : userManager.getAllTeamNames()) {
-							TeamModel team = userManager.getTeamModel(teamname);
-							if (user.isTeamMember(teamname) && !team.hasUser(user.username)) {
-								// new team member
-								team.addUser(user.username);
-								userManager.updateTeamModel(teamname, team);
-							} else if (!user.isTeamMember(teamname) && team.hasUser(user.username)) {
-								// remove team member
-								team.removeUser(user.username);
-								userManager.updateTeamModel(teamname, team);
-							}
-
-							// update team repositories
-							TeamModel remoteTeam = user.getTeam(teamname);
-							if (remoteTeam != null) {
-								if (remoteTeam.permissions != null) {
-									// pulling from >= 1.2
-									for (Map.Entry<String, AccessPermission> entry : remoteTeam.permissions.entrySet()){
-										team.setRepositoryPermission(entry.getKey(), entry.getValue());
-									}
-									userManager.updateTeamModel(teamname, team);
-								} else if(!ArrayUtils.isEmpty(remoteTeam.repositories)) {
-									// pulling from <= 1.1
-									team.addRepositoryPermissions(remoteTeam.repositories);
-									userManager.updateTeamModel(teamname, team);
-								}
-							}
-						}
-					}
-				}
-			}
-		} catch (ForbiddenException e) {
-			// ignore forbidden exceptions
-		} catch (IOException e) {
-			logger.warn(MessageFormat.format(
-					"Failed to retrieve USERS from federated gitblit ({0} @ {1})",
-					registration.name, registration.url), e);
-		}
-
-		try {
-			// Pull TEAMS
-			// We explicitly pull these even though they are embedded in
-			// UserModels because it is possible to use teams to specify
-			// mailing lists or push scripts without specifying users.
-			if (userService != null) {
-				Collection<TeamModel> teams = FederationUtils.getTeams(registration);
-				if (teams != null && teams.size() > 0) {
-					for (TeamModel team : teams) {
-						userService.updateTeamModel(team);
-					}
-				}
-			}
-		} catch (ForbiddenException e) {
-			// ignore forbidden exceptions
-		} catch (IOException e) {
-			logger.warn(MessageFormat.format(
-					"Failed to retrieve TEAMS from federated gitblit ({0} @ {1})",
-					registration.name, registration.url), e);
-		}
-
-		try {
-			// Pull SETTINGS
-			Map<String, String> settings = FederationUtils.getSettings(registration);
-			if (settings != null && settings.size() > 0) {
-				Properties properties = new Properties();
-				properties.putAll(settings);
-				FileOutputStream os = new FileOutputStream(new File(registrationFolderFile,
-						registration.name + "_" + Constants.PROPERTIES_FILE));
-				properties.store(os, null);
-				os.close();
-			}
-		} catch (ForbiddenException e) {
-			// ignore forbidden exceptions
-		} catch (IOException e) {
-			logger.warn(MessageFormat.format(
-					"Failed to retrieve SETTINGS from federated gitblit ({0} @ {1})",
-					registration.name, registration.url), e);
-		}
-
-		try {
-			// Pull SCRIPTS
-			Map<String, String> scripts = FederationUtils.getScripts(registration);
-			if (scripts != null && scripts.size() > 0) {
-				for (Map.Entry<String, String> script : scripts.entrySet()) {
-					String scriptName = script.getKey();
-					if (scriptName.endsWith(".groovy")) {
-						scriptName = scriptName.substring(0, scriptName.indexOf(".groovy"));
-					}
-					File file = new File(registrationFolderFile, registration.name + "_"
-							+ scriptName + ".groovy");
-					FileUtils.writeContent(file, script.getValue());
-				}
-			}
-		} catch (ForbiddenException e) {
-			// ignore forbidden exceptions
-		} catch (IOException e) {
-			logger.warn(MessageFormat.format(
-					"Failed to retrieve SCRIPTS from federated gitblit ({0} @ {1})",
-					registration.name, registration.url), e);
-		}
-	}
-
-	/**
-	 * Sends a status acknowledgment to the origin Gitblit instance. This
-	 * includes the results of the federated pull.
-	 *
-	 * @param registration
-	 * @throws Exception
-	 */
-	private void sendStatusAcknowledgment(FederationModel registration) throws Exception {
-		if (!registration.sendStatus) {
-			// skip status acknowledgment
-			return;
-		}
-		InetAddress addr = InetAddress.getLocalHost();
-		IStoredSettings settings = GitBlit.getManager(IRuntimeManager.class).getSettings();
-		String federationName = settings.getString(Keys.federation.name, null);
-		if (StringUtils.isEmpty(federationName)) {
-			federationName = addr.getHostName();
-		}
-		FederationUtils.acknowledgeStatus(addr.getHostAddress(), registration);
-		logger.info(MessageFormat.format("Pull status sent to {0}", registration.url));
-	}
-
-	/**
-	 * Schedules the next check of the federated Gitblit instance.
-	 *
-	 * @param registration
-	 */
-	private void schedule(FederationModel registration) {
-		// schedule the next pull
-		int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency);
-		registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
-		GitBlit.self().executor()
-				.schedule(new FederationPullExecutor(registration), mins, TimeUnit.MINUTES);
-		logger.info(MessageFormat.format(
-				"Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
-				registration.name, registration.url, registration.nextPull));
-	}
-}
+package com.gitblit;
+
+import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.FederationPullStatus;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JGitUtils.CloneResult;
+import com.gitblit.utils.StringUtils;
+
+public abstract class FederationPullExecutor implements Runnable {
+
+	Logger logger = LoggerFactory.getLogger(getClass());
+
+	Gitblit gitblit;
+
+	private final List<FederationModel> registrations;
+
+	/**
+	 * Constructor for specifying a single federation registration. This
+	 * constructor is used to schedule the next pull execution.
+	 *
+	 * @param provider
+	 * @param registration
+	 */
+	public FederationPullExecutor(FederationModel registration) {
+		this(Arrays.asList(registration));
+	}
+
+	/**
+	 * Constructor to specify a group of federation registrations. This is
+	 * normally used at startup to pull and then schedule the next update based
+	 * on each registrations frequency setting.
+	 *
+	 * @param provider
+	 * @param registrations
+	 * @param isDaemon
+	 *            if true, registrations are rescheduled in perpetuity. if
+	 *            false, the federation pull operation is executed once.
+	 */
+	public FederationPullExecutor(List<FederationModel> registrations) {
+		this.registrations = registrations;
+	}
+
+	public abstract void reschedule(FederationModel registration);
+
+	/**
+	 * Run method for this pull executor.
+	 */
+	@Override
+	public void run() {
+		for (FederationModel registration : registrations) {
+			FederationPullStatus was = registration.getLowestStatus();
+			try {
+				Date now = new Date(System.currentTimeMillis());
+				pull(registration);
+				sendStatusAcknowledgment(registration);
+				registration.lastPull = now;
+				FederationPullStatus is = registration.getLowestStatus();
+				if (is.ordinal() < was.ordinal()) {
+					// the status for this registration has downgraded
+					logger.warn("Federation pull status of {0} is now {1}", registration.name,
+							is.name());
+					if (registration.notifyOnError) {
+						String message = "Federation pull of " + registration.name + " @ "
+								+ registration.url + " is now at " + is.name();
+						gitblit.sendMailToAdministrators(
+								"Pull Status of " + registration.name + " is " + is.name(),
+								message);
+					}
+				}
+			} catch (Throwable t) {
+				logger.error(MessageFormat.format(
+						"Failed to pull from federated gitblit ({0} @ {1})", registration.name,
+						registration.url), t);
+			} finally {
+				reschedule(registration);
+			}
+		}
+	}
+
+	/**
+	 * Mirrors a repository and, optionally, the server's users, and/or
+	 * configuration settings from a origin Gitblit instance.
+	 *
+	 * @param registration
+	 * @throws Exception
+	 */
+	private void pull(FederationModel registration) throws Exception {
+		Map<String, RepositoryModel> repositories = FederationUtils.getRepositories(registration,
+				true);
+		String registrationFolder = registration.folder.toLowerCase().trim();
+		// confirm valid characters in server alias
+		Character c = StringUtils.findInvalidCharacter(registrationFolder);
+		if (c != null) {
+			logger.error(MessageFormat
+					.format("Illegal character ''{0}'' in folder name ''{1}'' of federation registration {2}!",
+							c, registrationFolder, registration.name));
+			return;
+		}
+		File repositoriesFolder = gitblit.getRepositoriesFolder();
+		File registrationFolderFile = new File(repositoriesFolder, registrationFolder);
+		registrationFolderFile.mkdirs();
+
+		// Clone/Pull the repository
+		for (Map.Entry<String, RepositoryModel> entry : repositories.entrySet()) {
+			String cloneUrl = entry.getKey();
+			RepositoryModel repository = entry.getValue();
+			if (!repository.hasCommits) {
+				logger.warn(MessageFormat.format(
+						"Skipping federated repository {0} from {1} @ {2}. Repository is EMPTY.",
+						repository.name, registration.name, registration.url));
+				registration.updateStatus(repository, FederationPullStatus.SKIPPED);
+				continue;
+			}
+
+			// Determine local repository name
+			String repositoryName;
+			if (StringUtils.isEmpty(registrationFolder)) {
+				repositoryName = repository.name;
+			} else {
+				repositoryName = registrationFolder + "/" + repository.name;
+			}
+
+			if (registration.bare) {
+				// bare repository, ensure .git suffix
+				if (!repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
+					repositoryName += DOT_GIT_EXT;
+				}
+			} else {
+				// normal repository, strip .git suffix
+				if (repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
+					repositoryName = repositoryName.substring(0,
+							repositoryName.indexOf(DOT_GIT_EXT));
+				}
+			}
+
+			// confirm that the origin of any pre-existing repository matches
+			// the clone url
+			String fetchHead = null;
+			Repository existingRepository = gitblit.getRepository(repositoryName);
+
+			if (existingRepository == null && gitblit.isCollectingGarbage(repositoryName)) {
+				logger.warn(MessageFormat.format("Skipping local repository {0}, busy collecting garbage", repositoryName));
+				continue;
+			}
+
+			if (existingRepository != null) {
+				StoredConfig config = existingRepository.getConfig();
+				config.load();
+				String origin = config.getString("remote", "origin", "url");
+				RevCommit commit = JGitUtils.getCommit(existingRepository,
+						org.eclipse.jgit.lib.Constants.FETCH_HEAD);
+				if (commit != null) {
+					fetchHead = commit.getName();
+				}
+				existingRepository.close();
+				if (!origin.startsWith(registration.url)) {
+					logger.warn(MessageFormat
+							.format("Skipping federated repository {0} from {1} @ {2}. Origin does not match, consider EXCLUDING.",
+									repository.name, registration.name, registration.url));
+					registration.updateStatus(repository, FederationPullStatus.SKIPPED);
+					continue;
+				}
+			}
+
+			// clone/pull this repository
+			CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(
+					Constants.FEDERATION_USER, registration.token);
+			logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}",
+					repository.name, registration.name, registration.url));
+
+			CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name,
+					cloneUrl, registration.bare, credentials);
+			Repository r = gitblit.getRepository(repositoryName);
+			RepositoryModel rm = gitblit.getRepositoryModel(repositoryName);
+			repository.isFrozen = registration.mirror;
+			if (result.createdRepository) {
+				// default local settings
+				repository.federationStrategy = FederationStrategy.EXCLUDE;
+				repository.isFrozen = registration.mirror;
+				repository.showRemoteBranches = !registration.mirror;
+				logger.info(MessageFormat.format("     cloning {0}", repository.name));
+				registration.updateStatus(repository, FederationPullStatus.MIRRORED);
+			} else {
+				// fetch and update
+				boolean fetched = false;
+				RevCommit commit = JGitUtils.getCommit(r, org.eclipse.jgit.lib.Constants.FETCH_HEAD);
+				String newFetchHead = commit.getName();
+				fetched = fetchHead == null || !fetchHead.equals(newFetchHead);
+
+				if (registration.mirror) {
+					// mirror
+					if (fetched) {
+						// update local branches to match the remote tracking branches
+						for (RefModel ref : JGitUtils.getRemoteBranches(r, false, -1)) {
+							if (ref.displayName.startsWith("origin/")) {
+								String branch = org.eclipse.jgit.lib.Constants.R_HEADS
+										+ ref.displayName.substring(ref.displayName.indexOf('/') + 1);
+								String hash = ref.getReferencedObjectId().getName();
+
+								JGitUtils.setBranchRef(r, branch, hash);
+								logger.info(MessageFormat.format("     resetting {0} of {1} to {2}", branch,
+										repository.name, hash));
+							}
+						}
+
+						String newHead;
+						if (StringUtils.isEmpty(repository.HEAD)) {
+							newHead = newFetchHead;
+						} else {
+							newHead = repository.HEAD;
+						}
+						JGitUtils.setHEADtoRef(r, newHead);
+						logger.info(MessageFormat.format("     resetting HEAD of {0} to {1}",
+								repository.name, newHead));
+						registration.updateStatus(repository, FederationPullStatus.MIRRORED);
+					} else {
+						// indicate no commits pulled
+						registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
+					}
+				} else {
+					// non-mirror
+					if (fetched) {
+						// indicate commits pulled to origin/master
+						registration.updateStatus(repository, FederationPullStatus.PULLED);
+					} else {
+						// indicate no commits pulled
+						registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
+					}
+				}
+
+				// preserve local settings
+				repository.isFrozen = rm.isFrozen;
+				repository.federationStrategy = rm.federationStrategy;
+
+				// merge federation sets
+				Set<String> federationSets = new HashSet<String>();
+				if (rm.federationSets != null) {
+					federationSets.addAll(rm.federationSets);
+				}
+				if (repository.federationSets != null) {
+					federationSets.addAll(repository.federationSets);
+				}
+				repository.federationSets = new ArrayList<String>(federationSets);
+
+				// merge indexed branches
+				Set<String> indexedBranches = new HashSet<String>();
+				if (rm.indexedBranches != null) {
+					indexedBranches.addAll(rm.indexedBranches);
+				}
+				if (repository.indexedBranches != null) {
+					indexedBranches.addAll(repository.indexedBranches);
+				}
+				repository.indexedBranches = new ArrayList<String>(indexedBranches);
+
+			}
+			// only repositories that are actually _cloned_ from the origin
+			// Gitblit repository are marked as federated. If the origin
+			// is from somewhere else, these repositories are not considered
+			// "federated" repositories.
+			repository.isFederated = cloneUrl.startsWith(registration.url);
+
+			gitblit.updateConfiguration(r, repository);
+			r.close();
+		}
+
+		IUserService userService = null;
+
+		try {
+			// Pull USERS
+			// TeamModels are automatically pulled because they are contained
+			// within the UserModel. The UserService creates unknown teams
+			// and updates existing teams.
+			Collection<UserModel> users = FederationUtils.getUsers(registration);
+			if (users != null && users.size() > 0) {
+				File realmFile = new File(registrationFolderFile, registration.name + "_users.conf");
+				realmFile.delete();
+				userService = new ConfigUserService(realmFile);
+				for (UserModel user : users) {
+					userService.updateUserModel(user.username, user);
+
+					// merge the origin permissions and origin accounts into
+					// the user accounts of this Gitblit instance
+					if (registration.mergeAccounts) {
+						// reparent all repository permissions if the local
+						// repositories are stored within subfolders
+						if (!StringUtils.isEmpty(registrationFolder)) {
+							if (user.permissions != null) {
+								// pulling from >= 1.2 version
+								Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
+								user.permissions.clear();
+								for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
+									user.setRepositoryPermission(registrationFolder + "/" + entry.getKey(), entry.getValue());
+								}
+							} else {
+								// pulling from <= 1.1 version
+								List<String> permissions = new ArrayList<String>(user.repositories);
+								user.repositories.clear();
+								for (String permission : permissions) {
+									user.addRepositoryPermission(registrationFolder + "/" + permission);
+								}
+							}
+						}
+
+						// insert new user or update local user
+						UserModel localUser = gitblit.getUserModel(user.username);
+						if (localUser == null) {
+							// create new local user
+							gitblit.updateUserModel(user.username, user, true);
+						} else {
+							// update repository permissions of local user
+							if (user.permissions != null) {
+								// pulling from >= 1.2 version
+								Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
+								for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
+									localUser.setRepositoryPermission(entry.getKey(), entry.getValue());
+								}
+							} else {
+								// pulling from <= 1.1 version
+								for (String repository : user.repositories) {
+									localUser.addRepositoryPermission(repository);
+								}
+							}
+							localUser.password = user.password;
+							localUser.canAdmin = user.canAdmin;
+							gitblit.updateUserModel(localUser.username, localUser, false);
+						}
+
+						for (String teamname : gitblit.getAllTeamNames()) {
+							TeamModel team = gitblit.getTeamModel(teamname);
+							if (user.isTeamMember(teamname) && !team.hasUser(user.username)) {
+								// new team member
+								team.addUser(user.username);
+								gitblit.updateTeamModel(teamname, team);
+							} else if (!user.isTeamMember(teamname) && team.hasUser(user.username)) {
+								// remove team member
+								team.removeUser(user.username);
+								gitblit.updateTeamModel(teamname, team);
+							}
+
+							// update team repositories
+							TeamModel remoteTeam = user.getTeam(teamname);
+							if (remoteTeam != null) {
+								if (remoteTeam.permissions != null) {
+									// pulling from >= 1.2
+									for (Map.Entry<String, AccessPermission> entry : remoteTeam.permissions.entrySet()){
+										team.setRepositoryPermission(entry.getKey(), entry.getValue());
+									}
+									gitblit.updateTeamModel(teamname, team);
+								} else if (!ArrayUtils.isEmpty(remoteTeam.repositories)) {
+									// pulling from <= 1.1
+									team.addRepositoryPermissions(remoteTeam.repositories);
+									gitblit.updateTeamModel(teamname, team);
+								}
+							}
+						}
+					}
+				}
+			}
+		} catch (ForbiddenException e) {
+			// ignore forbidden exceptions
+		} catch (IOException e) {
+			logger.warn(MessageFormat.format(
+					"Failed to retrieve USERS from federated gitblit ({0} @ {1})",
+					registration.name, registration.url), e);
+		}
+
+		try {
+			// Pull TEAMS
+			// We explicitly pull these even though they are embedded in
+			// UserModels because it is possible to use teams to specify
+			// mailing lists or push scripts without specifying users.
+			if (userService != null) {
+				Collection<TeamModel> teams = FederationUtils.getTeams(registration);
+				if (teams != null && teams.size() > 0) {
+					for (TeamModel team : teams) {
+						userService.updateTeamModel(team);
+					}
+				}
+			}
+		} catch (ForbiddenException e) {
+			// ignore forbidden exceptions
+		} catch (IOException e) {
+			logger.warn(MessageFormat.format(
+					"Failed to retrieve TEAMS from federated gitblit ({0} @ {1})",
+					registration.name, registration.url), e);
+		}
+
+		try {
+			// Pull SETTINGS
+			Map<String, String> settings = FederationUtils.getSettings(registration);
+			if (settings != null && settings.size() > 0) {
+				Properties properties = new Properties();
+				properties.putAll(settings);
+				FileOutputStream os = new FileOutputStream(new File(registrationFolderFile,
+						registration.name + "_" + Constants.PROPERTIES_FILE));
+				properties.store(os, null);
+				os.close();
+			}
+		} catch (ForbiddenException e) {
+			// ignore forbidden exceptions
+		} catch (IOException e) {
+			logger.warn(MessageFormat.format(
+					"Failed to retrieve SETTINGS from federated gitblit ({0} @ {1})",
+					registration.name, registration.url), e);
+		}
+
+		try {
+			// Pull SCRIPTS
+			Map<String, String> scripts = FederationUtils.getScripts(registration);
+			if (scripts != null && scripts.size() > 0) {
+				for (Map.Entry<String, String> script : scripts.entrySet()) {
+					String scriptName = script.getKey();
+					if (scriptName.endsWith(".groovy")) {
+						scriptName = scriptName.substring(0, scriptName.indexOf(".groovy"));
+					}
+					File file = new File(registrationFolderFile, registration.name + "_"
+							+ scriptName + ".groovy");
+					FileUtils.writeContent(file, script.getValue());
+				}
+			}
+		} catch (ForbiddenException e) {
+			// ignore forbidden exceptions
+		} catch (IOException e) {
+			logger.warn(MessageFormat.format(
+					"Failed to retrieve SCRIPTS from federated gitblit ({0} @ {1})",
+					registration.name, registration.url), e);
+		}
+	}
+
+	/**
+	 * Sends a status acknowledgment to the origin Gitblit instance. This
+	 * includes the results of the federated pull.
+	 *
+	 * @param registration
+	 * @throws Exception
+	 */
+	private void sendStatusAcknowledgment(FederationModel registration) throws Exception {
+		if (!registration.sendStatus) {
+			// skip status acknowledgment
+			return;
+		}
+		InetAddress addr = InetAddress.getLocalHost();
+		String federationName = gitblit.getSettings().getString(Keys.federation.name, null);
+		if (StringUtils.isEmpty(federationName)) {
+			federationName = addr.getHostName();
+		}
+		FederationUtils.acknowledgeStatus(addr.getHostAddress(), registration);
+		logger.info(MessageFormat.format("Pull status sent to {0}", registration.url));
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 493f8fc..ca676ff 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -15,50 +15,25 @@
  */
 package com.gitblit;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileFilter;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.lang.reflect.Type;
 import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
 
 import javax.naming.Context;
 import javax.naming.InitialContext;
 import javax.naming.NamingException;
 import javax.servlet.ServletContext;
 import javax.servlet.annotation.WebListener;
-import javax.servlet.http.HttpServletRequest;
 
-import org.apache.wicket.resource.ContextRelativeResource;
-import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
-import org.slf4j.Logger;
-
-import com.gitblit.Constants.AccessPermission;
-import com.gitblit.Constants.AccessRestrictionType;
-import com.gitblit.Constants.FederationRequest;
-import com.gitblit.Constants.FederationToken;
 import com.gitblit.dagger.DaggerContextListener;
-import com.gitblit.fanout.FanoutNioService;
-import com.gitblit.fanout.FanoutService;
-import com.gitblit.fanout.FanoutSocketService;
-import com.gitblit.git.GitDaemon;
 import com.gitblit.git.GitServlet;
 import com.gitblit.manager.IFederationManager;
 import com.gitblit.manager.IGitblitManager;
@@ -67,80 +42,54 @@
 import com.gitblit.manager.IProjectManager;
 import com.gitblit.manager.IRepositoryManager;
 import com.gitblit.manager.IRuntimeManager;
+import com.gitblit.manager.IServicesManager;
 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;
-import com.gitblit.models.GitClientApplication;
-import com.gitblit.models.RepositoryModel;
-import com.gitblit.models.RepositoryUrl;
-import com.gitblit.models.ServerSettings;
-import com.gitblit.models.SettingModel;
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.ContainerUtils;
-import com.gitblit.utils.FederationUtils;
-import com.gitblit.utils.HttpUtils;
-import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.JsonUtils;
-import com.gitblit.utils.ObjectCache;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitblitWicketFilter;
-import com.gitblit.wicket.WicketUtils;
-import com.google.gson.Gson;
-import com.google.gson.JsonIOException;
-import com.google.gson.JsonSyntaxException;
-import com.google.gson.reflect.TypeToken;
 
 import dagger.ObjectGraph;
 
 /**
- * GitBlit is the servlet context listener singleton that acts as the core for
- * the web ui and the servlets. This class is either directly instantiated by
- * the GitBlitServer class (Gitblit GO) or is reflectively instantiated by the
- * servlet 3 container (Gitblit WAR or Express).
+ * This class is the main entry point for the entire webapp.  It is a singleton
+ * created manually by Gitblit GO or dynamically by the WAR/Express servlet
+ * container.  This class instantiates and starts all managers followed by
+ * instantiating and registering all servlets and filters.
  *
- * This class is the central logic processor for Gitblit. All settings, user
- * object, and repository object operations pass through this class.
+ * Leveraging Servlet 3 and Dagger static dependency injection allows Gitblit to
+ * be modular and completely code-driven rather then relying on the fragility of
+ * a web.xml descriptor and the static & monolithic design previously used.
  *
  * @author James Moger
  *
  */
 @WebListener
-public class GitBlit extends DaggerContextListener
-					 implements IFederationManager,
-								IGitblitManager {
+public class GitBlit extends DaggerContextListener {
 
 	private static GitBlit gitblit;
+
+	private final List<IManager> managers = new ArrayList<IManager>();
 
 	private final IStoredSettings goSettings;
 
 	private final File goBaseFolder;
 
-	private final List<IManager> managers = new ArrayList<IManager>();
-
-	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>();
-
-	private IStoredSettings settings;
-
-	private FanoutService fanoutService;
-
-	private GitDaemon gitDaemon;
-
+	/**
+	 * Construct a Gitblit WAR/Express context.
+	 */
 	public GitBlit() {
 		this.goSettings = null;
 		this.goBaseFolder = null;
+		gitblit = this;
 	}
 
+	/**
+	 * Construct a Gitblit GO context.
+	 *
+	 * @param settings
+	 * @param baseFolder
+	 */
 	public GitBlit(IStoredSettings settings, File baseFolder) {
 		this.goSettings = settings;
 		this.goBaseFolder = baseFolder;
@@ -148,20 +97,13 @@
 	}
 
 	/**
-	 * Returns the Gitblit singleton.
+	 * This method is only used for unit and integration testing.
 	 *
-	 * @return gitblit singleton
+	 * @param managerClass
+	 * @return a manager
 	 */
-	public static GitBlit self() {
-		return gitblit;
-	}
-
 	@SuppressWarnings("unchecked")
-	public static <X> X getManager(Class<X> managerClass) {
-		if (managerClass.isAssignableFrom(GitBlit.class)) {
-			return (X) gitblit;
-		}
-
+	public static <X extends IManager> X getManager(Class<X> managerClass) {
 		for (IManager manager : gitblit.managers) {
 			if (managerClass.isAssignableFrom(manager.getClass())) {
 				return (X) manager;
@@ -171,786 +113,15 @@
 	}
 
 	/**
-	 * 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
+	 * Returns Gitblit's Dagger injection modules.
 	 */
 	@Override
-	public File getProposalsFolder() {
-		return getManager(IRuntimeManager.class).getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
+	protected Object [] getModules() {
+		return new Object [] { new DaggerModule() };
 	}
 
 	/**
-	 * 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;
-		}
-		String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
-
-		List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();
-		// http/https url
-		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {
-			AccessPermission permission = user.getRepositoryPermission(repository).permission;
-			if (permission.exceeds(AccessPermission.NONE)) {
-				list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission));
-			}
-		}
-
-		// git daemon url
-		String gitDaemonUrl = getGitDaemonUrl(request, user, repository);
-		if (!StringUtils.isEmpty(gitDaemonUrl)) {
-			AccessPermission permission = getGitDaemonAccessPermission(user, repository);
-			if (permission.exceeds(AccessPermission.NONE)) {
-				list.add(new RepositoryUrl(gitDaemonUrl, permission));
-			}
-		}
-
-		// add all other urls
-		// {0} = repository
-		// {1} = username
-		for (String url : settings.getStrings(Keys.web.otherUrls)) {
-			if (url.contains("{1}")) {
-				// external url requires username, only add url IF we have one
-				if(!StringUtils.isEmpty(username)) {
-					list.add(new RepositoryUrl(MessageFormat.format(url, repository.name, username), null));
-				}
-			} else {
-				// external url does not require username
-				list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null));
-			}
-		}
-		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)) {
-			sb.insert(sb.indexOf("://") + 3, username + "@");
-		}
-		return sb.toString();
-	}
-
-	protected String getGitDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {
-		if (gitDaemon != null) {
-			String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
-			if (bindInterface.equals("localhost")
-					&& (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) {
-				// git daemon is bound to localhost and the request is from elsewhere
-				return null;
-			}
-			if (user.canClone(repository)) {
-				String servername = request.getServerName();
-				String url = gitDaemon.formatUrl(servername, repository.name);
-				return url;
-			}
-		}
-		return null;
-	}
-
-	protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {
-		if (gitDaemon != null && user.canClone(repository)) {
-			AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;
-			if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {
-				if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {
-					// can not authenticate clone via anonymous git protocol
-					gitDaemonPermission = AccessPermission.NONE;
-				} else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
-					// can not authenticate push via anonymous git protocol
-					gitDaemonPermission = AccessPermission.CLONE;
-				} else {
-					// normal user permission
-				}
-			}
-			return gitDaemonPermission;
-		}
-		return AccessPermission.NONE;
-	}
-
-	/**
-	 * 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(getManager(IRuntimeManager.class).getBaseFolder(), "clientapps.json");
-		if (userDefs.exists()) {
-			Date lastModified = new Date(userDefs.lastModified());
-			if (clientApplications.hasCurrent("user", lastModified)) {
-				return clientApplications.getObject("user");
-			} else {
-				// (re)load user definitions
-				try {
-					InputStream is = new FileInputStream(userDefs);
-					Collection<GitClientApplication> clients = readClientApplications(is);
-					is.close();
-					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 {
-				InputStream is = getClass().getResourceAsStream("/clientapps.json");
-				Collection<GitClientApplication> clients = readClientApplications(is);
-				is.close();
-				if (clients != null) {
-					clientApplications.updateObject("system", new Date(0), clients);
-				}
-			} catch (IOException e) {
-				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>>() {
-			}.getType();
-			InputStreamReader reader = new InputStreamReader(is);
-			Gson gson = JsonUtils.gson();
-			Collection<GitClientApplication> links = gson.fromJson(reader, type);
-			return links;
-		} catch (JsonIOException e) {
-			logger.error("Error deserializing client applications!", e);
-		} catch (JsonSyntaxException e) {
-			logger.error("Error deserializing client applications!", e);
-		}
-		return null;
-	}
-
-	/**
-	 * Open a file resource using the Servlet container.
-	 * @param file to open
-	 * @return InputStream of the opened file
-	 * @throws ResourceStreamNotFoundException
-	 */
-	public InputStream getResourceAsStream(String file) throws ResourceStreamNotFoundException {
-		ContextRelativeResource res = WicketUtils.getResource(file);
-		return res.getResourceStream().getInputStream();
-	}
-
-	@Override
-	public UserModel getFederationUser() {
-		// the federation user is an administrator
-		UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
-		federationUser.canAdmin = true;
-		return federationUser;
-	}
-
-	/**
-	 * 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)) {
-			if (getManager(IUserManager.class).getUserModel(user.username) != null) {
-				throw new GitBlitException(MessageFormat.format(
-						"Failed to rename ''{0}'' because ''{1}'' already exists.", username,
-						user.username));
-			}
-
-			// rename repositories and owner fields for all repositories
-			for (RepositoryModel model : getManager(IRepositoryManager.class).getRepositoryModels(user)) {
-				if (model.isUsersPersonalRepository(username)) {
-					// personal repository
-					model.addOwner(user.username);
-					String oldRepositoryName = model.name;
-					model.name = user.getPersonalPath() + model.name.substring(model.projectPath.length());
-					model.projectPath = user.getPersonalPath();
-					getManager(IRepositoryManager.class).updateRepositoryModel(oldRepositoryName, model, false);
-				} else if (model.isOwner(username)) {
-					// common/shared repo
-					model.addOwner(user.username);
-					getManager(IRepositoryManager.class).updateRepositoryModel(model.name, model, false);
-				}
-			}
-		}
-		if (!getManager(IUserManager.class).updateUserModel(username, user)) {
-			throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
-		}
-	}
-
-	/**
-	 * 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)) {
-			if (getManager(IUserManager.class).getTeamModel(team.name) != null) {
-				throw new GitBlitException(MessageFormat.format(
-						"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
-						team.name));
-			}
-		}
-		if (!getManager(IUserManager.class).updateTeamModel(teamname, team)) {
-			throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
-		}
-	}
-
-
-	/**
-	 * Returns Gitblit's scheduled executor service for scheduling tasks.
-	 *
-	 * @return scheduledExecutor
-	 */
-	public ScheduledExecutorService executor() {
-		return scheduledExecutor;
-	}
-
-	@Override
-	public boolean canFederate() {
-		String passphrase = settings.getString(Keys.federation.passphrase, "");
-		return !StringUtils.isEmpty(passphrase);
-	}
-
-	/**
-	 * Configures this Gitblit instance to pull any registered federated gitblit
-	 * instances.
-	 */
-	private void configureFederation() {
-		boolean validPassphrase = true;
-		String passphrase = settings.getString(Keys.federation.passphrase, "");
-		if (StringUtils.isEmpty(passphrase)) {
-			logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
-			validPassphrase = false;
-		}
-		if (validPassphrase) {
-			// standard tokens
-			for (FederationToken tokenType : FederationToken.values()) {
-				logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
-						getFederationToken(tokenType)));
-			}
-
-			// federation set tokens
-			for (String set : settings.getStrings(Keys.federation.sets)) {
-				logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
-						getFederationToken(set)));
-			}
-		}
-
-		// Schedule the federation executor
-		List<FederationModel> registrations = getFederationRegistrations();
-		if (registrations.size() > 0) {
-			FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
-			scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
-		}
-	}
-
-	/**
-	 * 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));
-		}
-		return federationRegistrations;
-	}
-
-	/**
-	 * 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()) {
-			if (r.name.equals(name) && r.url.equals(url)) {
-				return r;
-			}
-		}
-
-		// check the results
-		for (FederationModel r : getFederationResultRegistrations()) {
-			if (r.name.equals(name) && r.url.equals(url)) {
-				return r;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * 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
-		for (FederationToken type : FederationToken.values()) {
-			FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
-			fset.repositories = getRepositories(gitblitUrl, fset.token);
-			list.add(fset);
-		}
-		// generate tokens for federation sets
-		for (String set : settings.getStrings(Keys.federation.sets)) {
-			FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
-					getFederationToken(set));
-			fset.repositories = getRepositories(gitblitUrl, fset.token);
-			list.add(fset);
-		}
-		return list;
-	}
-
-	/**
-	 * 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
-		for (FederationToken type : FederationToken.values()) {
-			tokens.add(getFederationToken(type));
-		}
-		// generate tokens for federation sets
-		for (String set : settings.getStrings(Keys.federation.sets)) {
-			tokens.add(getFederationToken(set));
-		}
-		return tokens;
-	}
-
-	/**
-	 * 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);
-	}
-
-	/**
-	 * 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);
-		String jur = getFederationToken(FederationToken.REPOSITORIES);
-		switch (req) {
-		case PULL_REPOSITORIES:
-			return token.equals(all) || token.equals(unr) || token.equals(jur);
-		case PULL_USERS:
-		case PULL_TEAMS:
-			return token.equals(all) || token.equals(unr);
-		case PULL_SETTINGS:
-		case PULL_SCRIPTS:
-			return token.equals(all);
-		default:
-			break;
-		}
-		return false;
-	}
-
-	/**
-	 * 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;
-		String id = identification;
-		if (!StringUtils.isEmpty(registration.folder)) {
-			id += "-" + registration.folder;
-		}
-		federationPullResults.put(id, registration);
-		return true;
-	}
-
-	/**
-	 * Returns the list of registration results.
-	 *
-	 * @return the list of registration results
-	 */
-	@Override
-	public List<FederationModel> getFederationResultRegistrations() {
-		return new ArrayList<FederationModel>(federationPullResults.values());
-	}
-
-	/**
-	 * Submit a federation proposal. The proposal is cached locally and the
-	 * Gitblit administrator(s) are notified via email.
-	 *
-	 * @param proposal
-	 *            the proposal
-	 * @param gitblitUrl
-	 *            the url of your gitblit instance to send an email to
-	 *            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);
-
-		try {
-			// make the proposals folder
-			File proposalsFolder = getProposalsFolder();
-			proposalsFolder.mkdirs();
-
-			// cache json to a file
-			File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
-			com.gitblit.utils.FileUtils.writeContent(file, json);
-		} catch (Exception e) {
-			logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
-		}
-
-		// send an email, if possible
-		getManager(INotificationManager.class).sendMailToAdministrators("Federation proposal from " + proposal.url,
-				"Please review the proposal @ " + gitblitUrl + "/proposal/" + proposal.token);
-		return true;
-	}
-
-	/**
-	 * 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();
-		if (folder.exists()) {
-			File[] files = folder.listFiles(new FileFilter() {
-				@Override
-				public boolean accept(File file) {
-					return file.isFile()
-							&& file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
-				}
-			});
-			for (File file : files) {
-				String json = com.gitblit.utils.FileUtils.readContent(file, null);
-				FederationProposal proposal = JsonUtils.fromJsonString(json,
-						FederationProposal.class);
-				list.add(proposal);
-			}
-		}
-		return list;
-	}
-
-	/**
-	 * 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 : settings.getStrings(Keys.federation.sets)) {
-			federationSets.put(getFederationToken(set), set);
-		}
-
-		// Determine the Gitblit clone url
-		StringBuilder sb = new StringBuilder();
-		sb.append(gitblitUrl);
-		sb.append(Constants.GIT_PATH);
-		sb.append("{0}");
-		String cloneUrl = sb.toString();
-
-		// Retrieve all available repositories
-		UserModel user = getFederationUser();
-		List<RepositoryModel> list = getManager(IRepositoryManager.class).getRepositoryModels(user);
-
-		// create the [cloneurl, repositoryModel] map
-		Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
-		for (RepositoryModel model : list) {
-			// by default, setup the url for THIS repository
-			String url = MessageFormat.format(cloneUrl, model.name);
-			switch (model.federationStrategy) {
-			case EXCLUDE:
-				// skip this repository
-				continue;
-			case FEDERATE_ORIGIN:
-				// federate the origin, if it is defined
-				if (!StringUtils.isEmpty(model.origin)) {
-					url = model.origin;
-				}
-				break;
-			default:
-				break;
-			}
-
-			if (federationSets.containsKey(token)) {
-				// include repositories only for federation set
-				String set = federationSets.get(token);
-				if (model.federationSets.contains(set)) {
-					repositories.put(url, model);
-				}
-			} else {
-				// standard federation token for ALL
-				repositories.put(url, model);
-			}
-		}
-		return repositories;
-	}
-
-	/**
-	 * 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()) {
-			if (token.equals(getFederationToken(type))) {
-				tokenType = type;
-				break;
-			}
-		}
-		Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
-		FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
-				repositories);
-		return proposal;
-	}
-
-	/**
-	 * 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) {
-			if (proposal.token.equals(token)) {
-				return proposal;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * 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);
-		return file.delete();
-	}
-
-	/**
-	 * 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(ServerSettings settingsModel) {
-		// this entire "supports" concept will go away with user service refactoring
-		UserModel externalUser = new UserModel(Constants.EXTERNAL_ACCOUNT);
-		externalUser.password = Constants.EXTERNAL_ACCOUNT;
-		IUserManager userManager = getManager(IUserManager.class);
-		settingsModel.supportsCredentialChanges = userManager.supportsCredentialChanges(externalUser);
-		settingsModel.supportsDisplayNameChanges = userManager.supportsDisplayNameChanges(externalUser);
-		settingsModel.supportsEmailAddressChanges = userManager.supportsEmailAddressChanges(externalUser);
-		settingsModel.supportsTeamMembershipChanges = userManager.supportsTeamMembershipChanges(externalUser);
-		try {
-			// Read bundled Gitblit properties to extract setting descriptions.
-			// This copy is pristine and only used for populating the setting
-			// models map.
-			InputStream is = getClass().getResourceAsStream("/reference.properties");
-			BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
-			StringBuilder description = new StringBuilder();
-			SettingModel setting = new SettingModel();
-			String line = null;
-			while ((line = propertiesReader.readLine()) != null) {
-				if (line.length() == 0) {
-					description.setLength(0);
-					setting = new SettingModel();
-				} else {
-					if (line.charAt(0) == '#') {
-						if (line.length() > 1) {
-							String text = line.substring(1).trim();
-							if (SettingModel.CASE_SENSITIVE.equals(text)) {
-								setting.caseSensitive = true;
-							} else if (SettingModel.RESTART_REQUIRED.equals(text)) {
-								setting.restartRequired = true;
-							} else if (SettingModel.SPACE_DELIMITED.equals(text)) {
-								setting.spaceDelimited = true;
-							} else if (text.startsWith(SettingModel.SINCE)) {
-								try {
-									setting.since = text.split(" ")[1];
-								} catch (Exception e) {
-									setting.since = text;
-								}
-							} else {
-								description.append(text);
-								description.append('\n');
-							}
-						}
-					} else {
-						String[] kvp = line.split("=", 2);
-						String key = kvp[0].trim();
-						setting.name = key;
-						setting.defaultValue = kvp[1].trim();
-						setting.currentValue = setting.defaultValue;
-						setting.description = description.toString().trim();
-						settingsModel.add(setting);
-						description.setLength(0);
-						setting = new SettingModel();
-					}
-				}
-			}
-			propertiesReader.close();
-		} catch (NullPointerException e) {
-			logger.error("Failed to find resource copy of gitblit.properties");
-		} catch (IOException e) {
-			logger.error("Failed to load resource copy of gitblit.properties");
-		}
-		return settingsModel;
-	}
-
-	protected void configureFanout() {
-		// startup Fanout PubSub service
-		if (settings.getInteger(Keys.fanout.port, 0) > 0) {
-			String bindInterface = settings.getString(Keys.fanout.bindInterface, null);
-			int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT);
-			boolean useNio = settings.getBoolean(Keys.fanout.useNio, true);
-			int limit = settings.getInteger(Keys.fanout.connectionLimit, 0);
-
-			if (useNio) {
-				if (StringUtils.isEmpty(bindInterface)) {
-					fanoutService = new FanoutNioService(port);
-				} else {
-					fanoutService = new FanoutNioService(bindInterface, port);
-				}
-			} else {
-				if (StringUtils.isEmpty(bindInterface)) {
-					fanoutService = new FanoutSocketService(port);
-				} else {
-					fanoutService = new FanoutSocketService(bindInterface, port);
-				}
-			}
-
-			fanoutService.setConcurrentConnectionLimit(limit);
-			fanoutService.setAllowAllChannelAnnouncements(false);
-			fanoutService.start();
-		}
-	}
-
-	protected void configureGitDaemon() {
-		int port = settings.getInteger(Keys.git.daemonPort, 0);
-		String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
-		if (port > 0) {
-			try {
-				// HACK temporary pending manager separation and injection
-				Gitblit gitblit = new Gitblit(
-						getManager(IRuntimeManager.class),
-						getManager(INotificationManager.class),
-						getManager(IUserManager.class),
-						getManager(ISessionManager.class),
-						getManager(IRepositoryManager.class),
-						getManager(IProjectManager.class),
-						this,
-						this);
-				gitDaemon = new GitDaemon(gitblit);
-				gitDaemon.start();
-			} catch (IOException e) {
-				gitDaemon = null;
-				logger.error(MessageFormat.format("Failed to start Git daemon on {0}:{1,number,0}", bindInterface, port), e);
-			}
-		}
-	}
-
-	protected final Logger getLogger() {
-		return logger;
-	}
-
-	protected final ScheduledExecutorService getScheduledExecutor() {
-		return scheduledExecutor;
-	}
-
-	/**
-	 * Configure Gitblit from the web.xml, if no configuration has already been
-	 * specified.
-	 *
-	 * @see ServletContextListener.contextInitialize(ServletContextEvent)
+	 * Prepare runtime settings and start all manager instances.
 	 */
 	@Override
 	protected void beforeServletInjection(ServletContext context) {
@@ -958,12 +129,10 @@
 
 		// create the runtime settings object
 		IStoredSettings runtimeSettings = injector.get(IStoredSettings.class);
-		this.settings = runtimeSettings; // XXX remove me eventually
 		final File baseFolder;
 
 		if (goSettings != null) {
 			// Gitblit GO
-			logger.debug("configuring Gitblit GO");
 			baseFolder = configureGO(context, goSettings, goBaseFolder, runtimeSettings);
 		} else {
 			// servlet container
@@ -973,11 +142,9 @@
 
 			if (!StringUtils.isEmpty(System.getenv("OPENSHIFT_DATA_DIR"))) {
 				// RedHat OpenShift
-				logger.debug("configuring Gitblit Express");
 				baseFolder = configureExpress(context, webxmlSettings, contextFolder, runtimeSettings);
 			} else {
 				// standard WAR
-				logger.debug("configuring Gitblit WAR");
 				baseFolder = configureWAR(context, webxmlSettings, contextFolder, runtimeSettings);
 			}
 
@@ -985,27 +152,85 @@
 			ContainerUtils.CVE_2007_0450.test(runtimeSettings);
 		}
 
-		// Runtime manager is a container for settings and other parameters
-		IRuntimeManager runtime = startManager(injector, IRuntimeManager.class);
+		// Manually configure IRuntimeManager
+		logManager(IRuntimeManager.class);
+		IRuntimeManager runtime = injector.get(IRuntimeManager.class);
 		runtime.setBaseFolder(baseFolder);
 		runtime.getStatus().isGO = goSettings != null;
 		runtime.getStatus().servletContainer = context.getServerInfo();
+		runtime.start();
+		managers.add(runtime);
 
+		// start all other managers
 		startManager(injector, INotificationManager.class);
 		startManager(injector, IUserManager.class);
 		startManager(injector, ISessionManager.class);
 		startManager(injector, IRepositoryManager.class);
 		startManager(injector, IProjectManager.class);
+		startManager(injector, IGitblitManager.class);
+		startManager(injector, IFederationManager.class);
+		startManager(injector, IServicesManager.class);
 
-		logger.info("Gitblit base folder     = " + baseFolder.getAbsolutePath());
+		logger.info("");
+		logger.info("All managers started.");
+		logger.info("");
+	}
 
-		loadSettingModels(runtime.getSettingsModel());
+	protected <X extends IManager> X startManager(ObjectGraph injector, Class<X> clazz) {
+		logManager(clazz);
+		X x = injector.get(clazz);
+		x.start();
+		managers.add(x);
+		return x;
+	}
 
-		if (true/*startFederation*/) {
-			configureFederation();
+	protected void logManager(Class<? extends IManager> clazz) {
+		logger.info("");
+		logger.info("----[{}]----", clazz.getName());
+	}
+
+	/**
+	 * Instantiate and inject all filters and servlets into the container using
+	 * the servlet 3 specification.
+	 */
+	@Override
+	protected void injectServlets(ServletContext context) {
+		// access restricted servlets
+		serve(context, Constants.GIT_PATH, GitServlet.class, GitFilter.class);
+		serve(context, Constants.PAGES, PagesServlet.class, PagesFilter.class);
+		serve(context, Constants.RPC_PATH, RpcServlet.class, RpcFilter.class);
+		serve(context, Constants.ZIP_PATH, DownloadZipServlet.class, DownloadZipFilter.class);
+		serve(context, Constants.SYNDICATION_PATH, SyndicationServlet.class, SyndicationFilter.class);
+
+		// servlets
+		serve(context, Constants.FEDERATION_PATH, FederationServlet.class);
+		serve(context, Constants.SPARKLESHARE_INVITE_PATH, SparkleShareInviteServlet.class);
+		serve(context, Constants.BRANCH_GRAPH_PATH, BranchGraphServlet.class);
+		file(context, "/robots.txt", RobotsTxtServlet.class);
+		file(context, "/logo.png", LogoServlet.class);
+
+		// optional force basic authentication
+		filter(context, "/*", EnforceAuthenticationFilter.class, null);
+
+		// Wicket
+		String toIgnore = StringUtils.flattenStrings(getRegisteredPaths(), ",");
+		Map<String, String> params = new HashMap<String, String>();
+		params.put(GitblitWicketFilter.FILTER_MAPPING_PARAM, "/*");
+		params.put(GitblitWicketFilter.IGNORE_PATHS_PARAM, toIgnore);
+		filter(context, "/*", GitblitWicketFilter.class, params);
+	}
+
+	/**
+	 * Gitblit is being shutdown either because the servlet container is
+	 * shutting down or because the servlet container is re-deploying Gitblit.
+	 */
+	@Override
+	protected void destroyContext(ServletContext context) {
+		logger.info("Gitblit context destroyed by servlet container.");
+		for (IManager manager : managers) {
+			logger.debug("stopping {}", manager.getClass().getSimpleName());
+			manager.stop();
 		}
-		configureFanout();
-		configureGitDaemon();
 	}
 
 	/**
@@ -1022,6 +247,8 @@
 			IStoredSettings goSettings,
 			File goBaseFolder,
 			IStoredSettings runtimeSettings) {
+
+		logger.debug("configuring Gitblit GO");
 
 		// merge the stored settings into the runtime settings
 		//
@@ -1049,6 +276,7 @@
 			IStoredSettings runtimeSettings) {
 
 		// Gitblit is running in a standard servlet container
+		logger.debug("configuring Gitblit WAR");
 		logger.info("WAR contextFolder is " + ((contextFolder != null) ? contextFolder.getAbsolutePath() : "<empty>"));
 
 		String path = webxmlSettings.getString(Constants.baseFolder, Constants.contextFolder$ + "/WEB-INF/data");
@@ -1112,6 +340,7 @@
 			IStoredSettings runtimeSettings) {
 
 		// Gitblit is running in OpenShift/JBoss
+		logger.debug("configuring Gitblit Express");
 		String openShift = System.getenv("OPENSHIFT_DATA_DIR");
 		File base = new File(openShift);
 		logger.info("EXPRESS contextFolder is " + contextFolder.getAbsolutePath());
@@ -1193,142 +422,5 @@
 				}
 			}
 		}
-	}
-
-	/**
-	 * Gitblit is being shutdown either because the servlet container is
-	 * shutting down or because the servlet container is re-deploying Gitblit.
-	 */
-	@Override
-	protected void destroyContext(ServletContext context) {
-		logger.info("Gitblit context destroyed by servlet container.");
-		for (IManager manager : managers) {
-			logger.debug("stopping {}", manager.getClass().getSimpleName());
-			manager.stop();
-		}
-
-		scheduledExecutor.shutdownNow();
-		if (fanoutService != null) {
-			fanoutService.stop();
-		}
-		if (gitDaemon != null) {
-			gitDaemon.stop();
-		}
-	}
-
-	/**
-	 * 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.
-	 *
-	 * @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.getPersonalPath(), StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
-		String fromUrl = MessageFormat.format("file://{0}/{1}", getManager(IRepositoryManager.class).getRepositoriesFolder().getAbsolutePath(), repository.name);
-
-		// clone the repository
-		try {
-			JGitUtils.cloneRepository(getManager(IRepositoryManager.class).getRepositoriesFolder(), cloneName, fromUrl, true, null);
-		} catch (Exception e) {
-			throw new GitBlitException(e);
-		}
-
-		// create a Gitblit repository model for the clone
-		RepositoryModel cloneModel = repository.cloneAs(cloneName);
-		// owner has REWIND/RW+ permissions
-		cloneModel.addOwner(user.username);
-		getManager(IRepositoryManager.class).updateRepositoryModel(cloneName, cloneModel, false);
-
-		// add the owner of the source repository to the clone's access list
-		if (!ArrayUtils.isEmpty(repository.owners)) {
-			for (String owner : repository.owners) {
-				UserModel originOwner = getManager(IUserManager.class).getUserModel(owner);
-				if (originOwner != null) {
-					originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
-					updateUserModel(originOwner.username, originOwner, false);
-				}
-			}
-		}
-
-		// grant origin's user list clone permission to fork
-		List<String> users = getManager(IRepositoryManager.class).getRepositoryUsers(repository);
-		List<UserModel> cloneUsers = new ArrayList<UserModel>();
-		for (String name : users) {
-			if (!name.equalsIgnoreCase(user.username)) {
-				UserModel cloneUser = getManager(IUserManager.class).getUserModel(name);
-				if (cloneUser.canClone(repository)) {
-					// origin user can clone origin, grant clone access to fork
-					cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
-				}
-				cloneUsers.add(cloneUser);
-			}
-		}
-		getManager(IUserManager.class).updateUserModels(cloneUsers);
-
-		// grant origin's team list clone permission to fork
-		List<String> teams = getManager(IRepositoryManager.class).getRepositoryTeams(repository);
-		List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
-		for (String name : teams) {
-			TeamModel cloneTeam = getManager(IUserManager.class).getTeamModel(name);
-			if (cloneTeam.canClone(repository)) {
-				// origin team can clone origin, grant clone access to fork
-				cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
-			}
-			cloneTeams.add(cloneTeam);
-		}
-		getManager(IUserManager.class).updateTeamModels(cloneTeams);
-
-		// add this clone to the cached model
-		getManager(IRepositoryManager.class).addToCachedRepositoryList(cloneModel);
-		return cloneModel;
-	}
-
-	@Override
-	protected Object [] getModules() {
-		return new Object [] { new DaggerModule(this) };
-	}
-
-	protected <X extends IManager> X startManager(ObjectGraph injector, Class<X> clazz) {
-		logger.debug("injecting and starting {}", clazz.getSimpleName());
-		X x = injector.get(clazz);
-		x.setup();
-		managers.add(x);
-		return x;
-	}
-
-	/**
-	 * Instantiate and inject all filters and servlets into the container using
-	 * the servlet 3 specification.
-	 */
-	@Override
-	protected void injectServlets(ServletContext context) {
-		// access restricted servlets
-		serve(context, Constants.GIT_PATH, GitServlet.class, GitFilter.class);
-		serve(context, Constants.PAGES, PagesServlet.class, PagesFilter.class);
-		serve(context, Constants.RPC_PATH, RpcServlet.class, RpcFilter.class);
-		serve(context, Constants.ZIP_PATH, DownloadZipServlet.class, DownloadZipFilter.class);
-		serve(context, Constants.SYNDICATION_PATH, SyndicationServlet.class, SyndicationFilter.class);
-
-		// servlets
-		serve(context, Constants.FEDERATION_PATH, FederationServlet.class);
-		serve(context, Constants.SPARKLESHARE_INVITE_PATH, SparkleShareInviteServlet.class);
-		serve(context, Constants.BRANCH_GRAPH_PATH, BranchGraphServlet.class);
-		file(context, "/robots.txt", RobotsTxtServlet.class);
-		file(context, "/logo.png", LogoServlet.class);
-
-		// optional force basic authentication
-		filter(context, "/*", EnforceAuthenticationFilter.class, null);
-
-		// Wicket
-		String toIgnore = StringUtils.flattenStrings(getRegisteredPaths(), ",");
-		Map<String, String> params = new HashMap<String, String>();
-		params.put(GitblitWicketFilter.FILTER_MAPPING_PARAM, "/*");
-		params.put(GitblitWicketFilter.IGNORE_PATHS_PARAM, toIgnore);
-		filter(context, "/*", GitblitWicketFilter.class, params);
 	}
 }
diff --git a/src/main/java/com/gitblit/Gitblit.java b/src/main/java/com/gitblit/Gitblit.java
index 7a5c73e..64316bb 100644
--- a/src/main/java/com/gitblit/Gitblit.java
+++ b/src/main/java/com/gitblit/Gitblit.java
@@ -22,8 +22,6 @@
 import java.util.Map;
 import java.util.TimeZone;
 
-import javax.inject.Inject;
-import javax.inject.Singleton;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -33,7 +31,6 @@
 import com.gitblit.Constants.FederationToken;
 import com.gitblit.manager.IFederationManager;
 import com.gitblit.manager.IGitblitManager;
-import com.gitblit.manager.IManager;
 import com.gitblit.manager.INotificationManager;
 import com.gitblit.manager.IProjectManager;
 import com.gitblit.manager.IRepositoryManager;
@@ -65,7 +62,6 @@
  * @author James Moger
  *
  */
-@Singleton
 public class Gitblit implements IRuntimeManager,
 								INotificationManager,
 								IUserManager,
@@ -87,11 +83,10 @@
 
 	private final IProjectManager projectManager;
 
-	private final IFederationManager federationManager;
-
 	private final IGitblitManager gitblitManager;
 
-	@Inject
+	private final IFederationManager federationManager;
+
 	public Gitblit(
 			IRuntimeManager runtimeManager,
 			INotificationManager notificationManager,
@@ -99,8 +94,8 @@
 			ISessionManager sessionManager,
 			IRepositoryManager repositoryManager,
 			IProjectManager projectManager,
-			IFederationManager federationManager,
-			IGitblitManager gitblitManager) {
+			IGitblitManager gitblitManager,
+			IFederationManager federationManager) {
 
 		this.runtimeManager = runtimeManager;
 		this.notificationManager = notificationManager;
@@ -108,17 +103,17 @@
 		this.sessionManager = sessionManager;
 		this.repositoryManager = repositoryManager;
 		this.projectManager = projectManager;
-		this.federationManager = federationManager;
 		this.gitblitManager = gitblitManager;
+		this.federationManager = federationManager;
 	}
 
 	@Override
-	public IManager setup() {
+	public Gitblit start() {
 		return this;
 	}
 
 	@Override
-	public IManager stop() {
+	public Gitblit stop() {
 		return this;
 	}
 
diff --git a/src/main/java/com/gitblit/manager/FederationManager.java b/src/main/java/com/gitblit/manager/FederationManager.java
new file mode 100644
index 0000000..07d2018
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/FederationManager.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.manager;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.FederationSet;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Federation manager controls all aspects of handling federation sets, tokens,
+ * and proposals.
+ *
+ * @author James Moger
+ *
+ */
+public class FederationManager implements IFederationManager {
+
+	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final List<FederationModel> federationRegistrations = Collections
+			.synchronizedList(new ArrayList<FederationModel>());
+
+	private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
+
+	private final IStoredSettings settings;
+
+	private final IRuntimeManager runtimeManager;
+
+	private final INotificationManager notificationManager;
+
+	private final IRepositoryManager repositoryManager;
+
+	public FederationManager(
+			IRuntimeManager runtimeManager,
+			INotificationManager notificationManager,
+			IUserManager userManager,
+			IRepositoryManager repositoryManager) {
+
+		this.settings = runtimeManager.getSettings();
+		this.runtimeManager = runtimeManager;
+		this.notificationManager = notificationManager;
+		this.repositoryManager = repositoryManager;
+	}
+
+	@Override
+	public FederationManager start() {
+		return this;
+	}
+
+	@Override
+	public FederationManager stop() {
+		return this;
+	}
+
+	/**
+	 * 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
+	 */
+	@Override
+	public File getProposalsFolder() {
+		return runtimeManager.getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
+	}
+
+	@Override
+	public UserModel getFederationUser() {
+		// the federation user is an administrator
+		UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
+		federationUser.canAdmin = true;
+		return federationUser;
+	}
+
+	@Override
+	public boolean canFederate() {
+		String passphrase = settings.getString(Keys.federation.passphrase, "");
+		return !StringUtils.isEmpty(passphrase);
+	}
+
+	/**
+	 * 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));
+		}
+		return federationRegistrations;
+	}
+
+	/**
+	 * 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()) {
+			if (r.name.equals(name) && r.url.equals(url)) {
+				return r;
+			}
+		}
+
+		// check the results
+		for (FederationModel r : getFederationResultRegistrations()) {
+			if (r.name.equals(name) && r.url.equals(url)) {
+				return r;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 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
+		for (FederationToken type : FederationToken.values()) {
+			FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
+			fset.repositories = getRepositories(gitblitUrl, fset.token);
+			list.add(fset);
+		}
+		// generate tokens for federation sets
+		for (String set : settings.getStrings(Keys.federation.sets)) {
+			FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
+					getFederationToken(set));
+			fset.repositories = getRepositories(gitblitUrl, fset.token);
+			list.add(fset);
+		}
+		return list;
+	}
+
+	/**
+	 * 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
+		for (FederationToken type : FederationToken.values()) {
+			tokens.add(getFederationToken(type));
+		}
+		// generate tokens for federation sets
+		for (String set : settings.getStrings(Keys.federation.sets)) {
+			tokens.add(getFederationToken(set));
+		}
+		return tokens;
+	}
+
+	/**
+	 * 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);
+	}
+
+	/**
+	 * 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);
+		String jur = getFederationToken(FederationToken.REPOSITORIES);
+		switch (req) {
+		case PULL_REPOSITORIES:
+			return token.equals(all) || token.equals(unr) || token.equals(jur);
+		case PULL_USERS:
+		case PULL_TEAMS:
+			return token.equals(all) || token.equals(unr);
+		case PULL_SETTINGS:
+		case PULL_SCRIPTS:
+			return token.equals(all);
+		default:
+			break;
+		}
+		return false;
+	}
+
+	/**
+	 * 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;
+		String id = identification;
+		if (!StringUtils.isEmpty(registration.folder)) {
+			id += "-" + registration.folder;
+		}
+		federationPullResults.put(id, registration);
+		return true;
+	}
+
+	/**
+	 * Returns the list of registration results.
+	 *
+	 * @return the list of registration results
+	 */
+	@Override
+	public List<FederationModel> getFederationResultRegistrations() {
+		return new ArrayList<FederationModel>(federationPullResults.values());
+	}
+
+	/**
+	 * Submit a federation proposal. The proposal is cached locally and the
+	 * Gitblit administrator(s) are notified via email.
+	 *
+	 * @param proposal
+	 *            the proposal
+	 * @param gitblitUrl
+	 *            the url of your gitblit instance to send an email to
+	 *            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);
+
+		try {
+			// make the proposals folder
+			File proposalsFolder = getProposalsFolder();
+			proposalsFolder.mkdirs();
+
+			// cache json to a file
+			File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
+			com.gitblit.utils.FileUtils.writeContent(file, json);
+		} catch (Exception e) {
+			logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
+		}
+
+		// send an email, if possible
+		notificationManager.sendMailToAdministrators("Federation proposal from " + proposal.url,
+				"Please review the proposal @ " + gitblitUrl + "/proposal/" + proposal.token);
+		return true;
+	}
+
+	/**
+	 * 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();
+		if (folder.exists()) {
+			File[] files = folder.listFiles(new FileFilter() {
+				@Override
+				public boolean accept(File file) {
+					return file.isFile()
+							&& file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
+				}
+			});
+			for (File file : files) {
+				String json = com.gitblit.utils.FileUtils.readContent(file, null);
+				FederationProposal proposal = JsonUtils.fromJsonString(json,
+						FederationProposal.class);
+				list.add(proposal);
+			}
+		}
+		return list;
+	}
+
+	/**
+	 * 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 : settings.getStrings(Keys.federation.sets)) {
+			federationSets.put(getFederationToken(set), set);
+		}
+
+		// Determine the Gitblit clone url
+		StringBuilder sb = new StringBuilder();
+		sb.append(gitblitUrl);
+		sb.append(Constants.GIT_PATH);
+		sb.append("{0}");
+		String cloneUrl = sb.toString();
+
+		// Retrieve all available repositories
+		UserModel user = getFederationUser();
+		List<RepositoryModel> list = repositoryManager.getRepositoryModels(user);
+
+		// create the [cloneurl, repositoryModel] map
+		Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
+		for (RepositoryModel model : list) {
+			// by default, setup the url for THIS repository
+			String url = MessageFormat.format(cloneUrl, model.name);
+			switch (model.federationStrategy) {
+			case EXCLUDE:
+				// skip this repository
+				continue;
+			case FEDERATE_ORIGIN:
+				// federate the origin, if it is defined
+				if (!StringUtils.isEmpty(model.origin)) {
+					url = model.origin;
+				}
+				break;
+			default:
+				break;
+			}
+
+			if (federationSets.containsKey(token)) {
+				// include repositories only for federation set
+				String set = federationSets.get(token);
+				if (model.federationSets.contains(set)) {
+					repositories.put(url, model);
+				}
+			} else {
+				// standard federation token for ALL
+				repositories.put(url, model);
+			}
+		}
+		return repositories;
+	}
+
+	/**
+	 * 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()) {
+			if (token.equals(getFederationToken(type))) {
+				tokenType = type;
+				break;
+			}
+		}
+		Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
+		FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
+				repositories);
+		return proposal;
+	}
+
+	/**
+	 * 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) {
+			if (proposal.token.equals(token)) {
+				return proposal;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 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);
+		return file.delete();
+	}
+}
diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java
new file mode 100644
index 0000000..2e6a33e
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/GitblitManager.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.manager;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlitException;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.GitClientApplication;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.RepositoryUrl;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.SettingModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.ObjectCache;
+import com.gitblit.utils.StringUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+public class GitblitManager implements IGitblitManager {
+
+	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final ObjectCache<Collection<GitClientApplication>> clientApplications = new ObjectCache<Collection<GitClientApplication>>();
+
+	private final IStoredSettings settings;
+
+	private final IRuntimeManager runtimeManager;
+
+	private final IUserManager userManager;
+
+	private final IRepositoryManager repositoryManager;
+
+	public GitblitManager(
+			IRuntimeManager runtimeManager,
+			IUserManager userManager,
+			IRepositoryManager repositoryManager) {
+
+		this.settings = runtimeManager.getSettings();
+		this.runtimeManager = runtimeManager;
+		this.userManager = userManager;
+		this.repositoryManager = repositoryManager;
+	}
+
+	@Override
+	public GitblitManager start() {
+		loadSettingModels(runtimeManager.getSettingsModel());
+		return this;
+	}
+
+	@Override
+	public GitblitManager stop() {
+		return this;
+	}
+
+	/**
+	 * 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 void loadSettingModels(ServerSettings settingsModel) {
+		// this entire "supports" concept will go away with user service refactoring
+		UserModel externalUser = new UserModel(Constants.EXTERNAL_ACCOUNT);
+		externalUser.password = Constants.EXTERNAL_ACCOUNT;
+		settingsModel.supportsCredentialChanges = userManager.supportsCredentialChanges(externalUser);
+		settingsModel.supportsDisplayNameChanges = userManager.supportsDisplayNameChanges(externalUser);
+		settingsModel.supportsEmailAddressChanges = userManager.supportsEmailAddressChanges(externalUser);
+		settingsModel.supportsTeamMembershipChanges = userManager.supportsTeamMembershipChanges(externalUser);
+		try {
+			// Read bundled Gitblit properties to extract setting descriptions.
+			// This copy is pristine and only used for populating the setting
+			// models map.
+			InputStream is = getClass().getResourceAsStream("/reference.properties");
+			BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
+			StringBuilder description = new StringBuilder();
+			SettingModel setting = new SettingModel();
+			String line = null;
+			while ((line = propertiesReader.readLine()) != null) {
+				if (line.length() == 0) {
+					description.setLength(0);
+					setting = new SettingModel();
+				} else {
+					if (line.charAt(0) == '#') {
+						if (line.length() > 1) {
+							String text = line.substring(1).trim();
+							if (SettingModel.CASE_SENSITIVE.equals(text)) {
+								setting.caseSensitive = true;
+							} else if (SettingModel.RESTART_REQUIRED.equals(text)) {
+								setting.restartRequired = true;
+							} else if (SettingModel.SPACE_DELIMITED.equals(text)) {
+								setting.spaceDelimited = true;
+							} else if (text.startsWith(SettingModel.SINCE)) {
+								try {
+									setting.since = text.split(" ")[1];
+								} catch (Exception e) {
+									setting.since = text;
+								}
+							} else {
+								description.append(text);
+								description.append('\n');
+							}
+						}
+					} else {
+						String[] kvp = line.split("=", 2);
+						String key = kvp[0].trim();
+						setting.name = key;
+						setting.defaultValue = kvp[1].trim();
+						setting.currentValue = setting.defaultValue;
+						setting.description = description.toString().trim();
+						settingsModel.add(setting);
+						description.setLength(0);
+						setting = new SettingModel();
+					}
+				}
+			}
+			propertiesReader.close();
+		} catch (NullPointerException e) {
+			logger.error("Failed to find resource copy of gitblit.properties");
+		} catch (IOException e) {
+			logger.error("Failed to load resource copy of gitblit.properties");
+		}
+	}
+
+	/**
+	 * 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;
+		}
+		String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
+
+		List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();
+		// http/https url
+		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {
+			AccessPermission permission = user.getRepositoryPermission(repository).permission;
+			if (permission.exceeds(AccessPermission.NONE)) {
+				list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission));
+			}
+		}
+
+		// git daemon url
+		String gitDaemonUrl = getGitDaemonUrl(request, user, repository);
+		if (!StringUtils.isEmpty(gitDaemonUrl)) {
+			AccessPermission permission = getGitDaemonAccessPermission(user, repository);
+			if (permission.exceeds(AccessPermission.NONE)) {
+				list.add(new RepositoryUrl(gitDaemonUrl, permission));
+			}
+		}
+
+		// add all other urls
+		// {0} = repository
+		// {1} = username
+		for (String url : settings.getStrings(Keys.web.otherUrls)) {
+			if (url.contains("{1}")) {
+				// external url requires username, only add url IF we have one
+				if (!StringUtils.isEmpty(username)) {
+					list.add(new RepositoryUrl(MessageFormat.format(url, repository.name, username), null));
+				}
+			} else {
+				// external url does not require username
+				list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null));
+			}
+		}
+		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)) {
+			sb.insert(sb.indexOf("://") + 3, username + "@");
+		}
+		return sb.toString();
+	}
+
+	protected String getGitDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {
+//		if (gitDaemon != null) {
+//			String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
+//			if (bindInterface.equals("localhost")
+//					&& (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) {
+//				// git daemon is bound to localhost and the request is from elsewhere
+//				return null;
+//			}
+//			if (user.canClone(repository)) {
+//				String servername = request.getServerName();
+//				String url = gitDaemon.formatUrl(servername, repository.name);
+//				return url;
+//			}
+//		}
+		return null;
+	}
+
+	protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {
+//		if (gitDaemon != null && user.canClone(repository)) {
+//			AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;
+//			if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {
+//				if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {
+//					// can not authenticate clone via anonymous git protocol
+//					gitDaemonPermission = AccessPermission.NONE;
+//				} else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
+//					// can not authenticate push via anonymous git protocol
+//					gitDaemonPermission = AccessPermission.CLONE;
+//				} else {
+//					// normal user permission
+//				}
+//			}
+//			return gitDaemonPermission;
+//		}
+		return AccessPermission.NONE;
+	}
+
+	/**
+	 * 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(runtimeManager.getBaseFolder(), "clientapps.json");
+		if (userDefs.exists()) {
+			Date lastModified = new Date(userDefs.lastModified());
+			if (clientApplications.hasCurrent("user", lastModified)) {
+				return clientApplications.getObject("user");
+			} else {
+				// (re)load user definitions
+				try {
+					InputStream is = new FileInputStream(userDefs);
+					Collection<GitClientApplication> clients = readClientApplications(is);
+					is.close();
+					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 {
+				InputStream is = getClass().getResourceAsStream("/clientapps.json");
+				Collection<GitClientApplication> clients = readClientApplications(is);
+				is.close();
+				if (clients != null) {
+					clientApplications.updateObject("system", new Date(0), clients);
+				}
+			} catch (IOException e) {
+				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>>() {
+			}.getType();
+			InputStreamReader reader = new InputStreamReader(is);
+			Gson gson = JsonUtils.gson();
+			Collection<GitClientApplication> links = gson.fromJson(reader, type);
+			return links;
+		} catch (JsonIOException e) {
+			logger.error("Error deserializing client applications!", e);
+		} catch (JsonSyntaxException e) {
+			logger.error("Error deserializing client applications!", e);
+		}
+		return null;
+	}
+
+	/**
+	 * 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.
+	 *
+	 * @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.getPersonalPath(), StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
+		String fromUrl = MessageFormat.format("file://{0}/{1}", repositoryManager.getRepositoriesFolder().getAbsolutePath(), repository.name);
+
+		// clone the repository
+		try {
+			JGitUtils.cloneRepository(repositoryManager.getRepositoriesFolder(), cloneName, fromUrl, true, null);
+		} catch (Exception e) {
+			throw new GitBlitException(e);
+		}
+
+		// create a Gitblit repository model for the clone
+		RepositoryModel cloneModel = repository.cloneAs(cloneName);
+		// owner has REWIND/RW+ permissions
+		cloneModel.addOwner(user.username);
+		repositoryManager.updateRepositoryModel(cloneName, cloneModel, false);
+
+		// add the owner of the source repository to the clone's access list
+		if (!ArrayUtils.isEmpty(repository.owners)) {
+			for (String owner : repository.owners) {
+				UserModel originOwner = userManager.getUserModel(owner);
+				if (originOwner != null) {
+					originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+					updateUserModel(originOwner.username, originOwner, false);
+				}
+			}
+		}
+
+		// grant origin's user list clone permission to fork
+		List<String> users = repositoryManager.getRepositoryUsers(repository);
+		List<UserModel> cloneUsers = new ArrayList<UserModel>();
+		for (String name : users) {
+			if (!name.equalsIgnoreCase(user.username)) {
+				UserModel cloneUser = userManager.getUserModel(name);
+				if (cloneUser.canClone(repository)) {
+					// origin user can clone origin, grant clone access to fork
+					cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+				}
+				cloneUsers.add(cloneUser);
+			}
+		}
+		userManager.updateUserModels(cloneUsers);
+
+		// grant origin's team list clone permission to fork
+		List<String> teams = repositoryManager.getRepositoryTeams(repository);
+		List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
+		for (String name : teams) {
+			TeamModel cloneTeam = userManager.getTeamModel(name);
+			if (cloneTeam.canClone(repository)) {
+				// origin team can clone origin, grant clone access to fork
+				cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+			}
+			cloneTeams.add(cloneTeam);
+		}
+		userManager.updateTeamModels(cloneTeams);
+
+		// add this clone to the cached model
+		repositoryManager.addToCachedRepositoryList(cloneModel);
+		return cloneModel;
+	}
+
+	/**
+	 * 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)) {
+			if (userManager.getUserModel(user.username) != null) {
+				throw new GitBlitException(MessageFormat.format(
+						"Failed to rename ''{0}'' because ''{1}'' already exists.", username,
+						user.username));
+			}
+
+			// rename repositories and owner fields for all repositories
+			for (RepositoryModel model : repositoryManager.getRepositoryModels(user)) {
+				if (model.isUsersPersonalRepository(username)) {
+					// personal repository
+					model.addOwner(user.username);
+					String oldRepositoryName = model.name;
+					model.name = user.getPersonalPath() + model.name.substring(model.projectPath.length());
+					model.projectPath = user.getPersonalPath();
+					repositoryManager.updateRepositoryModel(oldRepositoryName, model, false);
+				} else if (model.isOwner(username)) {
+					// common/shared repo
+					model.addOwner(user.username);
+					repositoryManager.updateRepositoryModel(model.name, model, false);
+				}
+			}
+		}
+		if (!userManager.updateUserModel(username, user)) {
+			throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
+		}
+	}
+
+	/**
+	 * 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)) {
+			if (userManager.getTeamModel(team.name) != null) {
+				throw new GitBlitException(MessageFormat.format(
+						"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
+						team.name));
+			}
+		}
+		if (!userManager.updateTeamModel(teamname, team)) {
+			throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/manager/IFederationManager.java b/src/main/java/com/gitblit/manager/IFederationManager.java
index debe362..5afdeea 100644
--- a/src/main/java/com/gitblit/manager/IFederationManager.java
+++ b/src/main/java/com/gitblit/manager/IFederationManager.java
@@ -27,7 +27,7 @@
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 
-public interface IFederationManager {
+public interface IFederationManager extends IManager {
 
 	/**
 	 * Returns the path of the proposals folder. This method checks to see if
diff --git a/src/main/java/com/gitblit/manager/IGitblitManager.java b/src/main/java/com/gitblit/manager/IGitblitManager.java
index 2f5295b..21c9e6a 100644
--- a/src/main/java/com/gitblit/manager/IGitblitManager.java
+++ b/src/main/java/com/gitblit/manager/IGitblitManager.java
@@ -27,7 +27,7 @@
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 
-public interface IGitblitManager {
+public interface IGitblitManager extends IManager {
 
 	/**
 	 * Returns a list of repository URLs and the user access permission.
diff --git a/src/main/java/com/gitblit/manager/IManager.java b/src/main/java/com/gitblit/manager/IManager.java
index 955c610..115831b 100644
--- a/src/main/java/com/gitblit/manager/IManager.java
+++ b/src/main/java/com/gitblit/manager/IManager.java
@@ -17,7 +17,8 @@
 
 public interface IManager {
 
-	IManager setup();
+	IManager start();
 
 	IManager stop();
+
 }
diff --git a/src/main/java/com/gitblit/manager/IServicesManager.java b/src/main/java/com/gitblit/manager/IServicesManager.java
new file mode 100644
index 0000000..9fdd063
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/IServicesManager.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.manager;
+
+
+public interface IServicesManager extends IManager {
+
+}
diff --git a/src/main/java/com/gitblit/manager/NotificationManager.java b/src/main/java/com/gitblit/manager/NotificationManager.java
index eae563f..e38e1f1 100644
--- a/src/main/java/com/gitblit/manager/NotificationManager.java
+++ b/src/main/java/com/gitblit/manager/NotificationManager.java
@@ -58,18 +58,19 @@
 	}
 
 	@Override
-	public IManager setup() {
+	public NotificationManager start() {
 		if (mailExecutor.isReady()) {
-			logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
-			scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
+			int period = 2;
+			logger.info("Mail service will process the queue every {} minutes.", period);
+			scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, period, TimeUnit.MINUTES);
 		} else {
-			logger.warn("Mail server is not properly configured.  Mail services disabled.");
+			logger.warn("Mail service disabled.");
 		}
 		return this;
 	}
 
 	@Override
-	public IManager stop() {
+	public NotificationManager stop() {
 		scheduledExecutor.shutdownNow();
 		return this;
 	}
diff --git a/src/main/java/com/gitblit/manager/ProjectManager.java b/src/main/java/com/gitblit/manager/ProjectManager.java
index 83a6310..b30f4f1 100644
--- a/src/main/java/com/gitblit/manager/ProjectManager.java
+++ b/src/main/java/com/gitblit/manager/ProjectManager.java
@@ -80,7 +80,7 @@
 	}
 
 	@Override
-	public IManager setup() {
+	public ProjectManager start() {
 		// load and cache the project metadata
 		projectConfigs = new FileBasedConfig(runtimeManager.getFileOrFolder(Keys.web.projectsFile, "${baseFolder}/projects.conf"), FS.detect());
 		getProjectConfigs();
@@ -89,7 +89,7 @@
 	}
 
 	@Override
-	public IManager stop() {
+	public ProjectManager stop() {
 		return this;
 	}
 
diff --git a/src/main/java/com/gitblit/manager/RepositoryManager.java b/src/main/java/com/gitblit/manager/RepositoryManager.java
index 9d38b30..4845e23 100644
--- a/src/main/java/com/gitblit/manager/RepositoryManager.java
+++ b/src/main/java/com/gitblit/manager/RepositoryManager.java
@@ -135,8 +135,8 @@
 	}
 
 	@Override
-	public IManager setup() {
-		logger.info("Git repositories folder = " + repositoriesFolder.getAbsolutePath());
+	public RepositoryManager start() {
+		logger.info("Repositories folder : {}", repositoriesFolder.getAbsolutePath());
 
 		// initialize utilities
 		String prefix = settings.getString(Keys.git.userRepositoryPrefix, "~");
@@ -147,7 +147,7 @@
 
 		// build initial repository list
 		if (settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
-			logger.info("Identifying available repositories...");
+			logger.info("Identifying repositories...");
 			getRepositoryList();
 		}
 
@@ -161,7 +161,7 @@
 	}
 
 	@Override
-	public IManager stop() {
+	public RepositoryManager stop() {
 		scheduledExecutor.shutdownNow();
 		luceneExecutor.close();
 		gcExecutor.close();
@@ -1645,15 +1645,16 @@
 
 	protected void configureLuceneIndexing() {
 		luceneExecutor = new LuceneExecutor(settings, this);
-		scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2,  TimeUnit.MINUTES);
-		logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes.");
+		int period = 2;
+		scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, period,  TimeUnit.MINUTES);
+		logger.info("Lucene will process indexed branches every {} minutes.", period);
 	}
 
 	protected void configureGarbageCollector() {
 		// schedule gc engine
 		gcExecutor = new GCExecutor(settings, this);
 		if (gcExecutor.isReady()) {
-			logger.info("GC executor is scheduled to scan repositories every 24 hours.");
+			logger.info("Garbage Collector (GC) will scan repositories every 24 hours.");
 			Calendar c = Calendar.getInstance();
 			c.set(Calendar.HOUR_OF_DAY, settings.getInteger(Keys.git.garbageCollectionHour, 0));
 			c.set(Calendar.MINUTE, 0);
@@ -1673,6 +1674,8 @@
 			}
 			logger.info(MessageFormat.format("Next scheculed GC scan is in {0}", when));
 			scheduledExecutor.scheduleAtFixedRate(gcExecutor, delay, 60 * 24, TimeUnit.MINUTES);
+		} else {
+			logger.info("Garbage Collector (GC) is disabled.");
 		}
 	}
 
@@ -1685,8 +1688,10 @@
 			}
 			int delay = 1;
 			scheduledExecutor.scheduleAtFixedRate(mirrorExecutor, delay, mins,  TimeUnit.MINUTES);
-			logger.info("Mirror executor is scheduled to fetch updates every {} minutes.", mins);
+			logger.info("Mirror service will fetch updates every {} minutes.", mins);
 			logger.info("Next scheduled mirror fetch is in {} minutes", delay);
+		} else {
+			logger.info("Mirror service is disabled.");
 		}
 	}
 
@@ -1717,12 +1722,12 @@
 	protected void configureCommitCache() {
 		int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
 		if (daysToCache <= 0) {
-			logger.info("commit cache disabled");
+			logger.info("Commit cache is disabled");
 		} else {
 			long start = System.nanoTime();
 			long repoCount = 0;
 			long commitCount = 0;
-			logger.info(MessageFormat.format("preparing {0} day commit cache. please wait...", daysToCache));
+			logger.info(MessageFormat.format("Preparing {0} day commit cache. please wait...", daysToCache));
 			CommitCache.instance().setCacheDays(daysToCache);
 			Date cutoff = CommitCache.instance().getCutoffDate();
 			for (String repositoryName : getRepositoryList()) {
diff --git a/src/main/java/com/gitblit/manager/RuntimeManager.java b/src/main/java/com/gitblit/manager/RuntimeManager.java
index cfb4543..45d1ea1 100644
--- a/src/main/java/com/gitblit/manager/RuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -40,23 +40,29 @@
 
 	private final ServerStatus serverStatus;
 
-	private TimeZone timezone;
+	private final ServerSettings settingsModel;
 
 	private File baseFolder;
 
-	private ServerSettings settingsModel;
+	private TimeZone timezone;
 
 	public RuntimeManager(IStoredSettings settings) {
+		this(settings, null);
+	}
+
+	public RuntimeManager(IStoredSettings settings, File baseFolder) {
 		this.settings = settings;
 		this.settingsModel = new ServerSettings();
 		this.serverStatus = new ServerStatus();
+		this.baseFolder = baseFolder == null ? new File("") : baseFolder;
 	}
 
 	@Override
-	public RuntimeManager setup() {
-		logger.info("Gitblit settings        = " + settings.toString());
-		logTimezone("JVM", TimeZone.getDefault());
-		logTimezone(Constants.NAME, getTimezone());
+	public RuntimeManager start() {
+		logger.info("Basefolder  : " + baseFolder.getAbsolutePath());
+		logger.info("Settings    : " + settings.toString());
+		logTimezone("JVM timezone: ", TimeZone.getDefault());
+		logTimezone("App timezone: ", getTimezone());
 		return this;
 	}
 
@@ -121,7 +127,7 @@
 	@Override
 	public TimeZone getTimezone() {
 		if (timezone == null) {
-			String tzid = settings.getString("web.timezone", null);
+			String tzid = settings.getString(Keys.web.timezone, null);
 			if (StringUtils.isEmpty(tzid)) {
 				timezone = TimeZone.getDefault();
 				return timezone;
@@ -135,7 +141,7 @@
 		SimpleDateFormat df = new SimpleDateFormat("z Z");
 		df.setTimeZone(zone);
 		String offset = df.format(new Date());
-		logger.info(type + " timezone is " + zone.getID() + " (" + offset + ")");
+		logger.info("{}{} ({})", new Object [] { type, zone.getID(), offset });
 	}
 
 	/**
diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java
new file mode 100644
index 0000000..82a8a04
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/ServicesManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.manager;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.FederationPullExecutor;
+import com.gitblit.Gitblit;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.fanout.FanoutNioService;
+import com.gitblit.fanout.FanoutService;
+import com.gitblit.fanout.FanoutSocketService;
+import com.gitblit.git.GitDaemon;
+import com.gitblit.models.FederationModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * Services manager manages long-running services/processes that either have no
+ * direct relation to other managers OR require really high-level manager
+ * integration (i.e. a Gitblit instance).
+ *
+ * @author James Moger
+ *
+ */
+public class ServicesManager implements IServicesManager {
+
+	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
+
+	private final IStoredSettings settings;
+
+	private final Gitblit gitblit;
+
+	private FanoutService fanoutService;
+
+	private GitDaemon gitDaemon;
+
+	public ServicesManager(Gitblit gitblit) {
+		this.settings = gitblit.getSettings();
+		this.gitblit = gitblit;
+	}
+
+	@Override
+	public ServicesManager start() {
+		configureFederation();
+		configureFanout();
+		configureGitDaemon();
+
+		return this;
+	}
+
+	@Override
+	public ServicesManager stop() {
+		scheduledExecutor.shutdownNow();
+		if (fanoutService != null) {
+			fanoutService.stop();
+		}
+		if (gitDaemon != null) {
+			gitDaemon.stop();
+		}
+		return this;
+	}
+
+	protected void configureFederation() {
+		boolean validPassphrase = true;
+		String passphrase = settings.getString(Keys.federation.passphrase, "");
+		if (StringUtils.isEmpty(passphrase)) {
+			logger.info("Federation passphrase is blank! This server can not be PULLED from.");
+			validPassphrase = false;
+		}
+		if (validPassphrase) {
+			// standard tokens
+			for (FederationToken tokenType : FederationToken.values()) {
+				logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
+						gitblit.getFederationToken(tokenType)));
+			}
+
+			// federation set tokens
+			for (String set : settings.getStrings(Keys.federation.sets)) {
+				logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
+						gitblit.getFederationToken(set)));
+			}
+		}
+
+		// Schedule or run the federation executor
+		List<FederationModel> registrations = gitblit.getFederationRegistrations();
+		if (registrations.size() > 0) {
+			FederationPuller executor = new FederationPuller(registrations);
+			scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
+		}
+	}
+
+	protected void configureGitDaemon() {
+		int port = settings.getInteger(Keys.git.daemonPort, 0);
+		String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
+		if (port > 0) {
+			try {
+				gitDaemon = new GitDaemon(gitblit);
+				gitDaemon.start();
+			} catch (IOException e) {
+				gitDaemon = null;
+				logger.error(MessageFormat.format("Failed to start Git Daemon on {0}:{1,number,0}", bindInterface, port), e);
+			}
+		} else {
+			logger.info("Git Daemon is disabled.");
+		}
+	}
+
+	protected void configureFanout() {
+		// startup Fanout PubSub service
+		if (settings.getInteger(Keys.fanout.port, 0) > 0) {
+			String bindInterface = settings.getString(Keys.fanout.bindInterface, null);
+			int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT);
+			boolean useNio = settings.getBoolean(Keys.fanout.useNio, true);
+			int limit = settings.getInteger(Keys.fanout.connectionLimit, 0);
+
+			if (useNio) {
+				if (StringUtils.isEmpty(bindInterface)) {
+					fanoutService = new FanoutNioService(port);
+				} else {
+					fanoutService = new FanoutNioService(bindInterface, port);
+				}
+			} else {
+				if (StringUtils.isEmpty(bindInterface)) {
+					fanoutService = new FanoutSocketService(port);
+				} else {
+					fanoutService = new FanoutSocketService(bindInterface, port);
+				}
+			}
+
+			fanoutService.setConcurrentConnectionLimit(limit);
+			fanoutService.setAllowAllChannelAnnouncements(false);
+			fanoutService.start();
+		} else {
+			logger.info("Fanout PubSub service is disabled.");
+		}
+	}
+
+	private class FederationPuller extends FederationPullExecutor {
+
+		public FederationPuller(FederationModel registration) {
+			super(Arrays.asList(registration));
+		}
+
+		public FederationPuller(List<FederationModel> registrations) {
+			super(registrations);
+		}
+
+		@Override
+		public void reschedule(FederationModel registration) {
+			// schedule the next pull
+			int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency);
+			registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
+			scheduledExecutor.schedule(new FederationPuller(registration), mins, TimeUnit.MINUTES);
+			logger.info(MessageFormat.format(
+					"Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
+					registration.name, registration.url, registration.nextPull));
+		}
+
+	}
+}
diff --git a/src/main/java/com/gitblit/manager/SessionManager.java b/src/main/java/com/gitblit/manager/SessionManager.java
index e6c3a2d..6a85da8 100644
--- a/src/main/java/com/gitblit/manager/SessionManager.java
+++ b/src/main/java/com/gitblit/manager/SessionManager.java
@@ -65,7 +65,7 @@
 	}
 
 	@Override
-	public IManager setup() {
+	public SessionManager start() {
 		List<String> services = settings.getStrings("realm.authenticationServices");
 		for (String service : services) {
 			// TODO populate authentication services here
@@ -74,7 +74,7 @@
 	}
 
 	@Override
-	public IManager stop() {
+	public SessionManager stop() {
 		return this;
 	}
 
diff --git a/src/main/java/com/gitblit/manager/UserManager.java b/src/main/java/com/gitblit/manager/UserManager.java
index f781c4c..90b9d1e 100644
--- a/src/main/java/com/gitblit/manager/UserManager.java
+++ b/src/main/java/com/gitblit/manager/UserManager.java
@@ -64,13 +64,13 @@
 	 * @param userService
 	 */
 	public void setUserService(IUserService userService) {
-		logger.info("Setting up user service " + userService.toString());
+		logger.info("UserService: " + userService.toString());
 		this.userService = userService;
 		this.userService.setup(runtimeManager);
 	}
 
 	@Override
-	public IManager setup() {
+	public UserManager start() {
 		if (this.userService == null) {
 			String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
 			IUserService service = null;
@@ -114,7 +114,7 @@
 	}
 
 	@Override
-	public IManager stop() {
+	public UserManager stop() {
 		return this;
 	}
 
diff --git a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
index aaa6c66..8e518de 100644
--- a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
+++ b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
@@ -137,7 +137,7 @@
 	}
 
 	@Override
-	public IRuntimeManager setup() {
+	public IRuntimeManager start() {
 		return this;
 	}
 }

--
Gitblit v1.9.1