From 13417cf9c6eec555b51da49742e47939d2f5715b Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 19 Oct 2012 22:47:33 -0400
Subject: [PATCH] Exclude submodules from zip downloads (issue 151)

---
 src/com/gitblit/GitBlit.java |  511 +++++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 423 insertions(+), 88 deletions(-)

diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index c758654..a121703 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -22,6 +22,8 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.text.MessageFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -30,6 +32,7 @@
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -52,25 +55,24 @@
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextListener;
 import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
 
 import org.apache.wicket.protocol.http.WebResponse;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.WindowCache;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
-import org.eclipse.jgit.transport.ServiceMayNotContinueException;
-import org.eclipse.jgit.transport.resolver.FileResolver;
-import org.eclipse.jgit.transport.resolver.RepositoryResolver;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.Constants.FederationRequest;
@@ -79,6 +81,7 @@
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.FederationProposal;
 import com.gitblit.models.FederationSet;
+import com.gitblit.models.ForkModel;
 import com.gitblit.models.Metric;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RepositoryModel;
@@ -98,6 +101,7 @@
 import com.gitblit.utils.MetricUtils;
 import com.gitblit.utils.ObjectCache;
 import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
 
 /**
  * GitBlit is the servlet context listener singleton that acts as the core for
@@ -138,8 +142,6 @@
 	
 	private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
 
-	private RepositoryResolver<Void> repositoryResolver;
-
 	private ServletContext servletContext;
 
 	private File repositoriesFolder;
@@ -165,6 +167,11 @@
 			// set the static singleton reference
 			gitblit = this;
 		}
+	}
+
+	public GitBlit(final IUserService userService) {
+		this.userService = userService;
+		gitblit = this;
 	}
 
 	/**
@@ -512,6 +519,28 @@
 	}
 
 	/**
+	 * Authenticate a user based on HTTP request paramters.
+	 * This method is inteded to be used as fallback when other
+	 * means of authentication are failing (username / password or cookies).
+	 * @param httpRequest
+	 * @return a user object or null
+	 */
+	public UserModel authenticate(HttpServletRequest httpRequest) {
+		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();
+	}
+
+	/**
 	 * Sets a cookie for the specified user.
 	 * 
 	 * @param response
@@ -621,6 +650,7 @@
 	 * @param usernames
 	 * @return true if successful
 	 */
+	@Deprecated
 	public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
 		return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers);
 	}
@@ -642,6 +672,22 @@
 				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 : getRepositoryModels(user)) {
+				if (model.isUsersPersonalRepository(username)) {
+					// personal repository
+					model.owner = user.username;
+					String oldRepositoryName = model.name;
+					model.name = "~" + user.username + model.name.substring(model.projectPath.length());
+					model.projectPath = "~" + user.username;
+					updateRepositoryModel(oldRepositoryName, model, false);
+				} else if (model.isOwner(username)) {
+					// common/shared repo
+					model.owner = user.username;
+					updateRepositoryModel(model.name, model, false);
+				}
 			}
 		}
 		if (!userService.updateUserModel(username, user)) {
@@ -702,6 +748,7 @@
 	 * @param teamnames
 	 * @return true if successful
 	 */
+	@Deprecated
 	public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
 		return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
 	}
@@ -742,11 +789,19 @@
 	 * Adds the repository to the list of cached repositories if Gitblit is
 	 * configured to cache the repository list.
 	 * 
-	 * @param name
+	 * @param model
 	 */
-	private void addToCachedRepositoryList(String name, RepositoryModel model) {
+	private void addToCachedRepositoryList(RepositoryModel model) {
 		if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
-			repositoryListCache.put(name, model);
+			repositoryListCache.put(model.name, model);
+			
+			// update the fork origin repository with this repository clone
+			if (!StringUtils.isEmpty(model.originRepository)) {
+				if (repositoryListCache.containsKey(model.originRepository)) {
+					RepositoryModel origin = repositoryListCache.get(model.originRepository);
+					origin.addFork(model.name);
+				}
+			}
 		}
 	}
 	
@@ -754,12 +809,13 @@
 	 * Removes the repository from the list of cached repositories.
 	 * 
 	 * @param name
+	 * @return the model being removed
 	 */
-	private void removeFromCachedRepositoryList(String name) {
+	private RepositoryModel removeFromCachedRepositoryList(String name) {
 		if (StringUtils.isEmpty(name)) {
-			return;
+			return null;
 		}
-		repositoryListCache.remove(name);
+		return repositoryListCache.remove(name);
 	}
 
 	/**
@@ -882,32 +938,18 @@
 	 * @return repository or null
 	 */
 	public Repository getRepository(String repositoryName, boolean logError) {
+		File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
+		if (dir == null)
+			return null;
+
 		Repository r = null;
 		try {
-			r = repositoryResolver.open(null, repositoryName);
-		} catch (RepositoryNotFoundException e) {
-			r = null;
+			FileKey key = FileKey.exact(dir, FS.DETECTED);
+			r = RepositoryCache.open(key, true);
+		} catch (IOException e) {
 			if (logError) {
 				logger.error("GitBlit.getRepository(String) failed to find "
 						+ new File(repositoriesFolder, repositoryName).getAbsolutePath());
-			}
-		} catch (ServiceNotAuthorizedException e) {
-			r = null;
-			if (logError) {
-				logger.error("GitBlit.getRepository(String) failed to find "
-						+ new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
-			}
-		} catch (ServiceNotEnabledException e) {
-			r = null;
-			if (logError) {
-				logger.error("GitBlit.getRepository(String) failed to find "
-						+ new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
-			}
-		} catch (ServiceMayNotContinueException e) {
-			r = null;
-			if (logError) {
-				logger.error("GitBlit.getRepository(String) failed to find "
-						+ new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
 			}
 		}
 		return r;
@@ -965,14 +1007,13 @@
 		if (model == null) {
 			return null;
 		}
-		if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
-			if (user != null && user.canAccessRepository(model)) {
-				return model;
-			}
-			return null;
-		} else {
+		if (user == null) {
+			user = UserModel.ANONYMOUS;
+		}
+		if (user.canView(model)) {
 			return model;
 		}
+		return null;
 	}
 
 	/**
@@ -988,7 +1029,7 @@
 			if (model == null) {
 				return null;
 			}
-			addToCachedRepositoryList(repositoryName, model);			
+			addToCachedRepositoryList(model);
 			return model;
 		}
 		
@@ -1010,7 +1051,7 @@
 			logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
 			model = loadRepositoryModel(repositoryName);
 			removeFromCachedRepositoryList(repositoryName);
-			addToCachedRepositoryList(repositoryName, model);
+			addToCachedRepositoryList(model);
 		} else {
 			// update a few repository parameters 
 			if (!model.hasCommits) {
@@ -1034,7 +1075,7 @@
 	 * @return project config map
 	 */
 	private Map<String, ProjectModel> getProjectConfigs() {
-		if (projectConfigs.isOutdated()) {
+		if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
 			
 			try {
 				projectConfigs.load();
@@ -1077,9 +1118,10 @@
 	 * Returns a list of project models for the user.
 	 * 
 	 * @param user
+	 * @param includeUsers
 	 * @return list of projects that are accessible to the user
 	 */
-	public List<ProjectModel> getProjectModels(UserModel user) {
+	public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
 		Map<String, ProjectModel> configs = getProjectConfigs();
 
 		// per-user project lists, this accounts for security and visibility
@@ -1104,10 +1146,25 @@
 		}
 		
 		// sort projects, root project first
-		List<ProjectModel> projects = new ArrayList<ProjectModel>(map.values());
-		Collections.sort(projects);
-		projects.remove(map.get(""));
-		projects.add(0, map.get(""));
+		List<ProjectModel> projects;
+		if (includeUsers) {
+			// all projects
+			projects = new ArrayList<ProjectModel>(map.values());
+			Collections.sort(projects);
+			projects.remove(map.get(""));
+			projects.add(0, map.get(""));
+		} else {
+			// all non-user projects
+			projects = new ArrayList<ProjectModel>();
+			ProjectModel root = map.remove("");
+			for (ProjectModel model : map.values()) {
+				if (!model.isUserProject()) {
+					projects.add(model);
+				}
+			}
+			Collections.sort(projects);
+			projects.add(0, root);
+		}
 		return projects;
 	}
 	
@@ -1119,7 +1176,7 @@
 	 * @return a project model, or null if it does not exist
 	 */
 	public ProjectModel getProjectModel(String name, UserModel user) {
-		for (ProjectModel project : getProjectModels(user)) {
+		for (ProjectModel project : getProjectModels(user, true)) {
 			if (project.name.equalsIgnoreCase(name)) {
 				return project;
 			}
@@ -1137,15 +1194,37 @@
 		Map<String, ProjectModel> configs = getProjectConfigs();
 		ProjectModel project = configs.get(name.toLowerCase());
 		if (project == null) {
-			return null;
-		}
-		// clone the object
-		project = DeepCopier.copy(project);
-		String folder = name.toLowerCase() + "/";
-		for (String repository : getRepositoryList()) {
-			if (repository.toLowerCase().startsWith(folder)) {
-				project.addRepository(repository);
+			project = new ProjectModel(name);
+			if (name.length() > 0 && name.charAt(0) == '~') {
+				UserModel user = getUserModel(name.substring(1));
+				if (user != null) {
+					project.title = user.getDisplayName();
+					project.description = "personal repositories";
+				}
 			}
+		} else {
+			// clone the object
+			project = DeepCopier.copy(project);
+		}
+		if (StringUtils.isEmpty(name)) {
+			// get root repositories
+			for (String repository : getRepositoryList()) {
+				if (repository.indexOf('/') == -1) {
+					project.addRepository(repository);
+				}
+			}
+		} else {
+			// get repositories in subfolder
+			String folder = name.toLowerCase() + "/";
+			for (String repository : getRepositoryList()) {
+				if (repository.toLowerCase().startsWith(folder)) {
+					project.addRepository(repository);
+				}
+			}
+		}
+		if (project.repositories.size() == 0) {
+			// no repositories == no project
+			return null;
 		}
 		return project;
 	}
@@ -1185,22 +1264,32 @@
 			return null;
 		}
 		RepositoryModel model = new RepositoryModel();
-		model.name = repositoryName;
+		model.isBare = r.isBare();
+		File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "git");
+		if (model.isBare) {
+			model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
+		} else {
+			model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory().getParentFile());
+		}
 		model.hasCommits = JGitUtils.hasCommits(r);
 		model.lastChange = JGitUtils.getLastChange(r);
-		model.isBare = r.isBare();
+		model.projectPath = StringUtils.getFirstPathElement(repositoryName);
 		
 		StoredConfig config = r.getConfig();
+		boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
+		
 		if (config != null) {
 			model.description = getConfig(config, "description", "");
 			model.owner = getConfig(config, "owner", "");
 			model.useTickets = getConfig(config, "useTickets", false);
 			model.useDocs = getConfig(config, "useDocs", false);
+			model.allowForks = getConfig(config, "allowForks", true);
 			model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
 					"accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));
 			model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
 					"authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
-			model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
+			model.verifyCommitter = getConfig(config, "verifyCommitter", false);
+			model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
 			model.isFrozen = getConfig(config, "isFrozen", false);
 			model.showReadme = getConfig(config, "showReadme", false);
 			model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
@@ -1211,6 +1300,9 @@
 					Constants.CONFIG_GITBLIT, null, "federationSets")));
 			model.isFederated = getConfig(config, "isFederated", false);
 			model.origin = config.getString("remote", "origin", "url");
+			if (model.origin != null) {
+				model.origin = model.origin.replace('\\', '/');
+			}
 			model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
 					Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
 			model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
@@ -1229,6 +1321,23 @@
 		model.HEAD = JGitUtils.getHEADRef(r);
 		model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
 		r.close();
+		
+		if (model.origin != null && model.origin.startsWith("file://")) {
+			// repository was cloned locally... perhaps as a fork
+			try {
+				File folder = new File(new URI(model.origin));
+				String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
+				if (!StringUtils.isEmpty(originRepo)) {
+					// ensure origin still exists
+					File repoFolder = new File(getRepositoriesFolder(), originRepo);
+					if (repoFolder.exists()) {
+						model.originRepository = originRepo;
+					}
+				}
+			} catch (URISyntaxException e) {
+				logger.error("Failed to determine fork for " + model, e);
+			}
+		}
 		return model;
 	}
 	
@@ -1250,6 +1359,113 @@
 		}
 		r.close();
 		return true;
+	}
+	
+	/**
+	 * Determines if the specified user has a fork of the specified origin
+	 * repository.
+	 * 
+	 * @param username
+	 * @param origin
+	 * @return true the if the user has a fork
+	 */
+	public boolean hasFork(String username, String origin) {
+		return getFork(username, origin) != null;
+	}
+	
+	/**
+	 * Gets the name of a user's fork of the specified origin
+	 * repository.
+	 * 
+	 * @param username
+	 * @param origin
+	 * @return the name of the user's fork, null otherwise
+	 */
+	public String getFork(String username, String origin) {
+		String userProject = "~" + username.toLowerCase();
+		if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+			String userPath = userProject + "/";
+
+			// collect all origin nodes in fork network
+			Set<String> roots = new HashSet<String>();
+			roots.add(origin);
+			RepositoryModel originModel = repositoryListCache.get(origin);
+			while (originModel != null) {
+				if (!ArrayUtils.isEmpty(originModel.forks)) {
+					for (String fork : originModel.forks) {
+						if (!fork.startsWith(userPath)) {
+							roots.add(fork);
+						}
+					}
+				}
+				
+				if (originModel.originRepository != null) {
+					roots.add(originModel.originRepository);
+					originModel = repositoryListCache.get(originModel.originRepository);
+				} else {
+					// break
+					originModel = null;
+				}
+			}
+			
+			for (String repository : repositoryListCache.keySet()) {
+				if (repository.toLowerCase().startsWith(userPath)) {
+					RepositoryModel model = repositoryListCache.get(repository);
+					if (!StringUtils.isEmpty(model.originRepository)) {
+						if (roots.contains(model.originRepository)) {
+							// user has a fork in this graph
+							return model.name;
+						}
+					}
+				}
+			}
+		} else {
+			// not caching
+			ProjectModel project = getProjectModel(userProject);
+			for (String repository : project.repositories) {
+				if (repository.toLowerCase().startsWith(userProject)) {
+					RepositoryModel model = repositoryListCache.get(repository);
+					if (model.originRepository.equalsIgnoreCase(origin)) {
+						// user has a fork
+						return model.name;
+					}
+				}
+			}
+		}
+		// user does not have a fork
+		return null;
+	}
+	
+	/**
+	 * Returns the fork network for a repository by traversing up the fork graph
+	 * to discover the root and then down through all children of the root node.
+	 * 
+	 * @param repository
+	 * @return a ForkModel
+	 */
+	public ForkModel getForkNetwork(String repository) {
+		if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+			// find the root
+			RepositoryModel model = repositoryListCache.get(repository);
+			while (model.originRepository != null) {
+				model = repositoryListCache.get(model.originRepository);
+			}
+			ForkModel root = getForkModel(model.name);
+			return root;
+		}
+		return null;
+	}
+	
+	private ForkModel getForkModel(String repository) {
+		RepositoryModel model = repositoryListCache.get(repository);
+		ForkModel fork = new ForkModel(model);
+		if (!ArrayUtils.isEmpty(model.forks)) {
+			for (String aFork : model.forks) {
+				ForkModel fm = getForkModel(aFork);
+				fork.forks.add(fm);
+			}
+		}
+		return fork;
 	}
 
 	/**
@@ -1279,6 +1495,11 @@
 	 */
 	private void closeRepository(String repositoryName) {
 		Repository repository = getRepository(repositoryName);
+		if (repository == null) {
+			return;
+		}
+		RepositoryCache.close(repository);
+
 		// assume 2 uses in case reflection fails
 		int uses = 2;
 		try {
@@ -1375,6 +1596,13 @@
 	public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
 			boolean isCreate) throws GitBlitException {
 		Repository r = null;
+		String projectPath = StringUtils.getFirstPathElement(repository.name);
+		if (!StringUtils.isEmpty(projectPath)) {
+			if (projectPath.equalsIgnoreCase(getString(Keys.web.repositoryRootGroupName, "main"))) {
+				// strip leading group name
+				repository.name = repository.name.substring(projectPath.length() + 1);
+			}
+		}
 		if (isCreate) {
 			// ensure created repository name ends with .git
 			if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
@@ -1425,24 +1653,40 @@
 							"Failed to rename repository permissions ''{0}'' to ''{1}''.",
 							repositoryName, repository.name));
 				}
+				
+				// rename fork origins in their configs
+				if (!ArrayUtils.isEmpty(repository.forks)) {
+					for (String fork : repository.forks) {
+						Repository rf = getRepository(fork);
+						try {
+							StoredConfig config = rf.getConfig();
+							String origin = config.getString("remote", "origin", "url");
+							origin = origin.replace(repositoryName, repository.name);
+							config.setString("remote", "origin", "url", origin);
+							config.save();
+						} catch (Exception e) {
+							logger.error("Failed to update repository fork config for " + fork, e);
+						}
+						rf.close();
+					}
+				}
+				
+				// remove this repository from any origin model's fork list
+				if (!StringUtils.isEmpty(repository.originRepository)) {
+					RepositoryModel origin = repositoryListCache.get(repository.originRepository);
+					if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
+						origin.forks.remove(repositoryName);
+					}
+				}
 
 				// clear the cache
 				clearRepositoryMetadataCache(repositoryName);
+				repository.resetDisplayName();
 			}
 
 			// load repository
 			logger.info("edit repository " + repository.name);
-			try {
-				r = repositoryResolver.open(null, repository.name);
-			} catch (RepositoryNotFoundException e) {
-				logger.error("Repository not found", e);
-			} catch (ServiceNotAuthorizedException e) {
-				logger.error("Service not authorized", e);
-			} catch (ServiceNotEnabledException e) {
-				logger.error("Service not enabled", e);
-			} catch (ServiceMayNotContinueException e) {
-				logger.error("Service may not continue", e);
-			}
+			r = getRepository(repository.name);
 		}
 
 		// update settings
@@ -1466,7 +1710,7 @@
 		// update repository cache
 		removeFromCachedRepositoryList(repositoryName);
 		// model will actually be replaced on next load because config is stale
-		addToCachedRepositoryList(repository.name, repository);
+		addToCachedRepositoryList(repository);
 	}
 	
 	/**
@@ -1483,8 +1727,10 @@
 		config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
+		config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
 		config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
 		config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
+		config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme);
@@ -1558,7 +1804,11 @@
 			closeRepository(repositoryName);
 			// clear the repository cache
 			clearRepositoryMetadataCache(repositoryName);
-			removeFromCachedRepositoryList(repositoryName);
+			
+			RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
+			if (model != null && !ArrayUtils.isEmpty(model.forks)) {
+				resetRepositoryListCache();
+			}
 
 			File folder = new File(repositoriesFolder, repositoryName);
 			if (folder.exists() && folder.isDirectory()) {
@@ -2208,10 +2458,11 @@
 	 * 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.
+	 * @param referencePropertiesInputStream
 	 * 
 	 * @return Map<String, SettingModel>
 	 */
-	private ServerSettings loadSettingModels() {
+	private ServerSettings loadSettingModels(InputStream referencePropertiesInputStream) {
 		ServerSettings settingsModel = new ServerSettings();
 		settingsModel.supportsCredentialChanges = userService.supportsCredentialChanges();
 		settingsModel.supportsDisplayNameChanges = userService.supportsDisplayNameChanges();
@@ -2221,7 +2472,7 @@
 			// Read bundled Gitblit properties to extract setting descriptions.
 			// This copy is pristine and only used for populating the setting
 			// models map.
-			InputStream is = servletContext.getResourceAsStream("/WEB-INF/reference.properties");
+			InputStream is = referencePropertiesInputStream;
 			BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
 			StringBuilder description = new StringBuilder();
 			SettingModel setting = new SettingModel();
@@ -2285,7 +2536,6 @@
 		this.settings = settings;
 		repositoriesFolder = getRepositoriesFolder();
 		logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
-		repositoryResolver = new FileResolver<Void>(repositoriesFolder, true);
 
 		// calculate repository list settings checksum for future config changes
 		repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum());
@@ -2300,21 +2550,23 @@
 		logTimezone(Constants.NAME, getTimezone());
 
 		serverStatus = new ServerStatus(isGO());
-		String realm = settings.getString(Keys.realm.userService, "users.properties");
-		IUserService loginService = null;
-		try {
-			// check to see if this "file" is a login service class
-			Class<?> realmClass = Class.forName(realm);
-			loginService = (IUserService) realmClass.newInstance();
-		} catch (Throwable t) {
-			loginService = new GitblitUserService();
+
+		if (this.userService == null) {
+			String realm = settings.getString(Keys.realm.userService, "users.properties");
+			IUserService loginService = null;
+			try {
+				// check to see if this "file" is a login service class
+				Class<?> realmClass = Class.forName(realm);
+				loginService = (IUserService) realmClass.newInstance();
+			} catch (Throwable t) {
+				loginService = new GitblitUserService();
+			}
+			setUserService(loginService);
 		}
-		setUserService(loginService);
 		
 		// load and cache the project metadata
 		projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect());
 		getProjectConfigs();
-		
 		mailExecutor = new MailExecutor(settings);
 		if (mailExecutor.isReady()) {
 			logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
@@ -2369,6 +2621,10 @@
 	 */
 	@Override
 	public void contextInitialized(ServletContextEvent contextEvent) {
+		contextInitialized(contextEvent, contextEvent.getServletContext().getResourceAsStream("/WEB-INF/reference.properties"));
+	}
+
+	public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) {
 		servletContext = contextEvent.getServletContext();
 		if (settings == null) {
 			// Gitblit WAR is running in a servlet container
@@ -2409,7 +2665,7 @@
 			}
 		}
 		
-		settingsModel = loadSettingModels();
+		settingsModel = loadSettingModels(referencePropertiesInputStream);
 		serverStatus.servletContainer = servletContext.getServerInfo();
 	}
 
@@ -2423,4 +2679,83 @@
 		scheduledExecutor.shutdownNow();
 		luceneExecutor.close();
 	}
+	
+	/**
+	 * 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
+	 */
+	public RepositoryModel fork(RepositoryModel repository, UserModel user) throws GitBlitException {
+		String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
+		String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name);
+
+		// clone the repository
+		try {
+			JGitUtils.cloneRepository(repositoriesFolder, 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.owner = user.username;
+		updateRepositoryModel(cloneName, cloneModel, false);
+
+		// add the owner of the source repository to the clone's access list
+		if (!StringUtils.isEmpty(repository.owner)) {
+			UserModel originOwner = getUserModel(repository.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 = getRepositoryUsers(repository);
+		List<UserModel> cloneUsers = new ArrayList<UserModel>();
+		for (String name : users) {
+			if (!name.equalsIgnoreCase(user.username)) {
+				UserModel cloneUser = getUserModel(name);
+				if (cloneUser.canClone(repository)) {
+					// origin user can clone origin, grant clone access to fork
+					cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+				}
+				cloneUsers.add(cloneUser);
+			}
+		}
+		userService.updateUserModels(cloneUsers);
+
+		// grant origin's team list clone permission to fork
+		List<String> teams = getRepositoryTeams(repository);
+		List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
+		for (String name : teams) {
+			TeamModel cloneTeam = getTeamModel(name);
+			if (cloneTeam.canClone(repository)) {
+				// origin team can clone origin, grant clone access to fork
+				cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+			}
+			cloneTeams.add(cloneTeam);
+		}
+		userService.updateTeamModels(cloneTeams);			
+
+		// add this clone to the cached model
+		addToCachedRepositoryList(cloneModel);
+		return cloneModel;
+	}
+
+	/**
+	 * Allow to understand if GitBlit supports and is configured to allow
+	 * cookie-based authentication.
+	 * 
+	 * @return status of Cookie authentication enablement.
+	 */
+	public boolean allowCookieAuthentication() {
+		return GitBlit.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
+	}
 }

--
Gitblit v1.9.1