From 1e1b85270f93b3bca624c99b478f3a9a23be2395 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sat, 29 Sep 2012 23:40:46 -0400
Subject: [PATCH] Preliminary implementation of server-side forking (issue 137)

---
 src/com/gitblit/utils/StringUtils.java                    |   29 +
 src/com/gitblit/ConfigUserService.java                    |    4 
 src/com/gitblit/wicket/pages/ProjectsPage.java            |    8 
 src/com/gitblit/wicket/panels/RepositoriesPanel.java      |   23 
 src/com/gitblit/models/UserModel.java                     |   29 +
 src/com/gitblit/wicket/pages/RepositoryPage.java          |  123 ++++
 src/com/gitblit/wicket/pages/EditUserPage.html            |    5 
 src/com/gitblit/wicket/pages/ProjectPage.html             |   70 --
 src/com/gitblit/GitBlit.java                              |  187 ++++++
 src/com/gitblit/wicket/GitBlitWebApp.properties           |   16 
 docs/04_releases.mkd                                      |   13 
 src/com/gitblit/wicket/pages/RootPage.java                |    9 
 src/com/gitblit/wicket/pages/RepositoryPage.html          |   34 +
 src/com/gitblit/wicket/panels/RepositoriesPanel.html      |    2 
 src/com/gitblit/wicket/pages/EditRepositoryPage.html      |    4 
 src/com/gitblit/Constants.java                            |    2 
 src/com/gitblit/utils/JGitUtils.java                      |   14 
 src/com/gitblit/wicket/pages/UserPage.java                |  146 +++++
 src/com/gitblit/wicket/pages/GitSearchPage.java           |    2 
 src/com/gitblit/wicket/pages/EditRepositoryPage.java      |    1 
 src/com/gitblit/wicket/panels/GravatarImage.java          |    2 
 src/com/gitblit/wicket/pages/ForksPage.html               |   24 
 src/com/gitblit/wicket/pages/ProjectPage.java             |  152 -----
 src/com/gitblit/wicket/pages/EditUserPage.java            |    1 
 src/com/gitblit/wicket/pages/UserPage.html                |   44 +
 src/com/gitblit/SyndicationServlet.java                   |    2 
 .project                                                  |   46 
 src/com/gitblit/FileUserService.java                      |    5 
 src/com/gitblit/wicket/pages/ForksPage.java               |  133 +++++
 src/com/gitblit/wicket/pages/LogPage.java                 |    2 
 src/com/gitblit/wicket/panels/HistoryPanel.java           |    4 
 src/com/gitblit/utils/ActivityUtils.java                  |    2 
 src/com/gitblit/wicket/pages/BasePage.java                |    9 
 src/com/gitblit/wicket/panels/SearchPanel.java            |    5 
 src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html |   79 +++
 src/com/gitblit/models/RepositoryModel.java               |   49 +
 src/com/gitblit/wicket/panels/LogPanel.java               |    4 
 src/com/gitblit/models/ProjectModel.java                  |    4 
 src/com/gitblit/wicket/pages/HistoryPage.java             |    2 
 src/com/gitblit/wicket/GitBlitWebApp.java                 |    4 
 docs/05_roadmap.mkd                                       |    1 
 resources/gitblit.css                                     |   46 +
 src/com/gitblit/wicket/pages/SummaryPage.java             |    2 
 src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java |  199 +++++++
 44 files changed, 1,238 insertions(+), 304 deletions(-)

diff --git a/.project b/.project
index 65c86e8..90c747b 100644
--- a/.project
+++ b/.project
@@ -1,23 +1,23 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-	<name>gitblit</name>
-	<comment></comment>
-	<projects>
-	</projects>
-	<buildSpec>
-		<buildCommand>
-			<name>org.eclipse.jdt.core.javabuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-	</buildSpec>
-	<natures>
-		<nature>org.eclipse.jdt.core.javanature</nature>
-		<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
-	</natures>
-</projectDescription>
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>Gitblit</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
+	</natures>
+</projectDescription>
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 454ce65..30582d5 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -17,10 +17,15 @@
 
 #### additions
 
-- added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
-- delete branch feature (issue 121, Github/ajermakovics)
-- added line links to blob view at the expense of zebra striping (issue 130)
-- added RedmineUserService (github/mallowlabs)
+- Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.
+- Added support for personal repositories.  This builds on the simple project pages.  
+Personal repositories are stored in *git.repositoriesFolder*/*~username*.  Each user with personal repositories will have a user page, something like the GitHub profile page.  Personal repositories have all the same features as common repositories.
+- Added support for server-side forking of a repository to a personal repository (issue 137)  
+In order to fork a repository to a personal clone, the user account must have the *fork* permission **and** the repository must *allow forks*.  The clone inherits the access restrictions of its origin.  i.e. if Team A has access to the origin repository, then by default Team A also has access to the fork.  This is to facilitate collaboration.  However, the fork owner may change access to the fork and add/remove users/teams, etc as required.
+- Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
+- Delete branch feature (issue 121, Github/ajermakovics)
+- Added line links to blob view at the expense of zebra striping (issue 130)
+- Added RedmineUserService (github/mallowlabs)
 
 #### changes
 
diff --git a/docs/05_roadmap.mkd b/docs/05_roadmap.mkd
index 3238f73..562cc7e 100644
--- a/docs/05_roadmap.mkd
+++ b/docs/05_roadmap.mkd
@@ -26,7 +26,6 @@
 ### IDEAS
 
 * Gitblit: Re-use the EGit branch visualization table cell renderer as some sort of servlet
-* Gitblit: Support personal repositories (~username/repo)
 * Gitblit: diff should highlight inserted/removed fragment compared to original line
 * Gitblit: implement branch permission controls as Groovy pre-receive script.  
 *Maintain permissions text file similar to a gitolite configuration file or svn authz file.*
diff --git a/resources/gitblit.css b/resources/gitblit.css
index fc948b6..66387d1 100644
--- a/resources/gitblit.css
+++ b/resources/gitblit.css
@@ -20,6 +20,11 @@
 	outline: none;
 }
 
+[class^="icon-"], [class*=" icon-"] a i {
+	/* override for a links that look like bootstrap buttons */
+	vertical-align: text-bottom;
+}
+
 hr {
 	margin-top: 10px;
 	margin-bottom: 10px;
@@ -127,6 +132,47 @@
 	font-weight: bold;
 }
 
+.pageTitle {
+	color: #888;
+	font-size: 18px;
+	line-height: 27px;
+}
+.pageTitle .project, .pageTitle .repository {
+	font-family: Helvetica, arial, freesans, clean, sans-serif;
+	font-size: 22px;
+}
+
+.pageTitle .controls {
+	font-size: 12px;
+}
+
+.pageTitle .repository {
+	font-weight: bold;
+}
+
+.originRepository {
+	font-family: Helvetica, arial, freesans, clean, sans-serif;
+	color: #888;
+	font-size: 12px;
+	line-height: 14px;
+	margin: 0px;
+}
+
+.forkSource, .forkEntry {
+	color: #888;
+}
+
+.forkSource {
+	font-size: 18px;
+	line-height: 20px;
+	padding: 5px 0px;
+}
+
+.forkEntry {
+	font-size: 14px;
+	padding: 2px 0px;
+}
+
 div.page_footer {
 	clear: both;
 	height: 17px;
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index faad691..f526835 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -750,6 +750,9 @@
 			if (model.canAdmin) {
 				roles.add(Constants.ADMIN_ROLE);
 			}
+			if (model.canFork) {
+				roles.add(Constants.FORK_ROLE);
+			}
 			if (model.excludeFromFederation) {
 				roles.add(Constants.NOT_FEDERATED_ROLE);
 			}
@@ -858,6 +861,7 @@
 					Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
 							USER, username, ROLE)));
 					user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
+					user.canFork = roles.contains(Constants.FORK_ROLE);
 					user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
 
 					// repository memberships
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index 55d4980..c53119b 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -41,6 +41,8 @@
 	public static final String JGIT_VERSION = "JGit 2.1.0 (201209190230-r)";
 
 	public static final String ADMIN_ROLE = "#admin";
+	
+	public static final String FORK_ROLE = "#fork";
 
 	public static final String NOT_FEDERATED_ROLE = "#notfederated";
 	
diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java
index 7705dfd..40bc3f6 100644
--- a/src/com/gitblit/FileUserService.java
+++ b/src/com/gitblit/FileUserService.java
@@ -234,6 +234,8 @@
 				// Permissions
 				if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
 					model.canAdmin = true;
+				} else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
+					model.canFork = true;
 				} else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
 					model.excludeFromFederation = true;
 				}
@@ -283,6 +285,9 @@
 			if (model.canAdmin) {
 				roles.add(Constants.ADMIN_ROLE);
 			}
+			if (model.canFork) {
+				roles.add(Constants.FORK_ROLE);
+			}
 			if (model.excludeFromFederation) {
 				roles.add(Constants.NOT_FEDERATED_ROLE);
 			}
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index c758654..699bbac 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;
@@ -747,6 +749,14 @@
 	private void addToCachedRepositoryList(String name, RepositoryModel model) {
 		if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
 			repositoryListCache.put(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(name);
+				}
+			}
 		}
 	}
 	
@@ -754,12 +764,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);
 	}
 
 	/**
@@ -988,7 +999,7 @@
 			if (model == null) {
 				return null;
 			}
-			addToCachedRepositoryList(repositoryName, model);			
+			addToCachedRepositoryList(repositoryName, model);
 			return model;
 		}
 		
@@ -1034,7 +1045,7 @@
 	 * @return project config map
 	 */
 	private Map<String, ProjectModel> getProjectConfigs() {
-		if (projectConfigs.isOutdated()) {
+		if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
 			
 			try {
 				projectConfigs.load();
@@ -1077,9 +1088,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 +1116,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 +1146,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 +1164,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;
 	}
@@ -1189,18 +1238,26 @@
 		model.hasCommits = JGitUtils.hasCommits(r);
 		model.lastChange = JGitUtils.getLastChange(r);
 		model.isBare = r.isBare();
+		if (repositoryName.indexOf('/') == -1) {
+			model.projectPath = "";
+		} else {
+			model.projectPath = repositoryName.substring(0, repositoryName.indexOf('/'));
+		}
 		
 		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.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
 			model.isFrozen = getConfig(config, "isFrozen", false);
 			model.showReadme = getConfig(config, "showReadme", false);
 			model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
@@ -1229,6 +1286,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;
 	}
 	
@@ -1425,9 +1499,27 @@
 							"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();
+					}
+				}
 
 				// clear the cache
 				clearRepositoryMetadataCache(repositoryName);
+				repository.resetDisplayName();
 			}
 
 			// load repository
@@ -1483,6 +1575,7 @@
 		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, "showRemoteBranches", repository.showRemoteBranches);
@@ -1558,7 +1651,11 @@
 			closeRepository(repositoryName);
 			// clear the repository cache
 			clearRepositoryMetadataCache(repositoryName);
-			removeFromCachedRepositoryList(repositoryName);
+			
+			RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
+			if (!ArrayUtils.isEmpty(model.forks)) {
+				resetRepositoryListCache();
+			}
 
 			File folder = new File(repositoriesFolder, repositoryName);
 			if (folder.exists() && folder.isDirectory()) {
@@ -2423,4 +2520,52 @@
 		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 true, if successful
+	 */
+	public boolean fork(RepositoryModel repository, UserModel user) {
+		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);
+		try {
+			// clone the repository
+			JGitUtils.cloneRepository(repositoriesFolder, cloneName, fromUrl, true, null);
+			
+			// create a Gitblit repository model for the clone
+			RepositoryModel cloneModel = repository.cloneAs(cloneName);
+			cloneModel.owner = user.username;
+			updateRepositoryModel(cloneName, cloneModel, false);
+			
+			if (AuthorizationControl.NAMED.equals(cloneModel.authorizationControl)) {
+				// add the owner of the source repository to the clone's access list
+				if (!StringUtils.isEmpty(repository.owner)) {
+					UserModel owner = getUserModel(repository.owner);
+					if (owner != null) {
+						owner.repositories.add(cloneName);
+						updateUserModel(owner.username, owner, false);
+					}
+				}
+				
+				// inherit origin's access lists
+				List<String> users = getRepositoryUsers(repository);
+				setRepositoryUsers(cloneModel, users);
+				
+				List<String> teams = getRepositoryTeams(repository);
+				setRepositoryTeams(cloneModel, teams);
+			}
+			
+			// add this clone to the cached model
+			addToCachedRepositoryList(cloneModel.name, cloneModel);
+			return true;
+		} catch (Exception e) {
+			logger.error("failed to fork", e);
+		}
+		return false;
+	}
 }
diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java
index 4c542b6..a36f583 100644
--- a/src/com/gitblit/SyndicationServlet.java
+++ b/src/com/gitblit/SyndicationServlet.java
@@ -227,7 +227,7 @@
 				commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType,
 						offset, length);
 			}
-			Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
+			Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, model.showRemoteBranches);
 
 			// convert RevCommit to SyndicatedEntryModel
 			for (RevCommit commit : commits) {
diff --git a/src/com/gitblit/models/ProjectModel.java b/src/com/gitblit/models/ProjectModel.java
index bc35903..189a409 100644
--- a/src/com/gitblit/models/ProjectModel.java
+++ b/src/com/gitblit/models/ProjectModel.java
@@ -53,6 +53,10 @@
 		this.title = "";
 		this.description = "";
 	}
+	
+	public boolean isUserProject() {
+		return name.charAt(0) == '~';
+	}
 
 	public boolean hasRepository(String name) {
 		return repositories.contains(name.toLowerCase());
diff --git a/src/com/gitblit/models/RepositoryModel.java b/src/com/gitblit/models/RepositoryModel.java
index 2719663..44aba1d 100644
--- a/src/com/gitblit/models/RepositoryModel.java
+++ b/src/com/gitblit/models/RepositoryModel.java
@@ -20,6 +20,8 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Constants.AuthorizationControl;
@@ -68,7 +70,11 @@
 	public List<String> postReceiveScripts;
 	public List<String> mailingLists;
 	public Map<String, String> customFields;
+	public String projectPath;
 	private String displayName;
+	public boolean allowForks;
+	public Set<String> forks;
+	public String originRepository;
 	
 	public RepositoryModel() {
 		this("", "", "", new Date(0));
@@ -97,6 +103,24 @@
 		}
 		return localBranches;
 	}
+	
+	public void addFork(String repository) {
+		if (forks == null) {
+			forks = new TreeSet<String>();
+		}
+		forks.add(repository);
+	}
+	
+	public void removeFork(String repository) {
+		if (forks == null) {
+			return;
+		}
+		forks.remove(repository);
+	}
+	
+	public void resetDisplayName() {
+		displayName = null;
+	}
 
 	@Override
 	public String toString() {
@@ -110,4 +134,29 @@
 	public int compareTo(RepositoryModel o) {
 		return StringUtils.compareRepositoryNames(name, o.name);
 	}
+	
+	public boolean isPersonalRepository() {
+		return !StringUtils.isEmpty(projectPath) && projectPath.charAt(0) == '~';
+	}
+	
+	public boolean isUsersPersonalRepository(String username) {
+		return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username);
+	}
+	
+	public RepositoryModel cloneAs(String cloneName) {
+		RepositoryModel clone = new RepositoryModel();
+		clone.name = cloneName;
+		clone.description = description;
+		clone.accessRestriction = accessRestriction;
+		clone.authorizationControl = authorizationControl;
+		clone.federationStrategy = federationStrategy;
+		clone.showReadme = showReadme;
+		clone.showRemoteBranches = false;
+		clone.allowForks = false;
+		clone.useDocs = useDocs;
+		clone.useTickets = useTickets;
+		clone.skipSizeCalculation = skipSizeCalculation;
+		clone.skipSummaryMetrics = skipSummaryMetrics;
+		return clone;
+	}
 }
\ No newline at end of file
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index 8349bab..0ede878 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -20,6 +20,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.utils.StringUtils;
 
@@ -42,6 +43,7 @@
 	public String displayName;
 	public String emailAddress;
 	public boolean canAdmin;
+	public boolean canFork;
 	public boolean excludeFromFederation;
 	public final Set<String> repositories = new HashSet<String>();
 	public final Set<TeamModel> teams = new HashSet<TeamModel>();
@@ -83,6 +85,33 @@
 		}
 		return false;
 	}
+	
+	public boolean canForkRepository(RepositoryModel repository) {
+		if (canAdmin) {
+			return true;
+		}
+		if (!canFork) {
+			// user has been prohibited from forking
+			return false;
+		}
+		if (!isAuthenticated) {
+			// unauthenticated user model
+			return false;
+		}
+		if (("~" + username).equalsIgnoreCase(repository.projectPath)) {
+			// this repository is already a personal repository
+			return false;
+		}
+		if (!repository.allowForks) {
+			// repository prohibits forks
+			return false;
+		}
+		if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {
+			return canAccessRepository(repository);
+		}
+		// repository is not clone-restricted
+		return true;
+	}
 
 	public boolean hasRepository(String name) {
 		return repositories.contains(name.toLowerCase());
diff --git a/src/com/gitblit/utils/ActivityUtils.java b/src/com/gitblit/utils/ActivityUtils.java
index 02a9924..e515994 100644
--- a/src/com/gitblit/utils/ActivityUtils.java
+++ b/src/com/gitblit/utils/ActivityUtils.java
@@ -94,7 +94,7 @@
 					branches.add(objectId);
 				}
 				Map<ObjectId, List<RefModel>> allRefs = JGitUtils
-						.getAllRefs(repository);
+						.getAllRefs(repository, model.showRemoteBranches);
 
 				for (String branch : branches) {
 					String shortName = branch;
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index c5cd1c3..050b591 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -1369,9 +1369,23 @@
 	 * @return all refs grouped by their referenced object id
 	 */
 	public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository) {
+		return getAllRefs(repository, true);
+	}
+	
+	/**
+	 * Returns all refs grouped by their associated object id.
+	 * 
+	 * @param repository
+	 * @param includeRemoteRefs
+	 * @return all refs grouped by their referenced object id
+	 */
+	public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
 		List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
 		Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
 		for (RefModel ref : list) {
+			if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) {
+				continue;
+			}
 			ObjectId objectid = ref.getReferencedObjectId();
 			if (!refs.containsKey(objectid)) {
 				refs.put(objectid, new ArrayList<RefModel>());
diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java
index e440790..0711338 100644
--- a/src/com/gitblit/utils/StringUtils.java
+++ b/src/com/gitblit/utils/StringUtils.java
@@ -367,7 +367,7 @@
 	 * @return the first invalid character found or null if string is acceptable
 	 */
 	public static Character findInvalidCharacter(String name) {
-		char[] validChars = { '/', '.', '_', '-' };
+		char[] validChars = { '/', '.', '_', '-', '~' };
 		for (char c : name.toCharArray()) {
 			if (!Character.isLetterOrDigit(c)) {
 				boolean ok = false;
@@ -660,4 +660,31 @@
 		}
 		return input;
 	}
+	
+	/**
+	 * Returns the first path element of a path string.  If no path separator is
+	 * found in the path, an empty string is returned. 
+	 * 
+	 * @param path
+	 * @return the first element in the path
+	 */
+	public static String getFirstPathElement(String path) {
+		if (path.indexOf('/') > -1) {
+			return path.substring(0, path.indexOf('/')).trim();
+		}
+		return "";
+	}
+	
+	/**
+	 * Returns the last path element of a path string
+	 * 
+	 * @param path
+	 * @return the last element in the path
+	 */
+	public static String getLastPathElement(String path) {
+		if (path.indexOf('/') > -1) {
+			return path.substring(path.lastIndexOf('/') + 1);
+		}
+		return path;
+	}
 }
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.java b/src/com/gitblit/wicket/GitBlitWebApp.java
index 507de15..b691cea 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/com/gitblit/wicket/GitBlitWebApp.java
@@ -34,6 +34,7 @@
 import com.gitblit.wicket.pages.CommitPage;
 import com.gitblit.wicket.pages.DocsPage;
 import com.gitblit.wicket.pages.FederationRegistrationPage;
+import com.gitblit.wicket.pages.ForksPage;
 import com.gitblit.wicket.pages.GitSearchPage;
 import com.gitblit.wicket.pages.GravatarProfilePage;
 import com.gitblit.wicket.pages.HistoryPage;
@@ -53,6 +54,7 @@
 import com.gitblit.wicket.pages.TicketPage;
 import com.gitblit.wicket.pages.TicketsPage;
 import com.gitblit.wicket.pages.TreePage;
+import com.gitblit.wicket.pages.UserPage;
 import com.gitblit.wicket.pages.UsersPage;
 
 public class GitBlitWebApp extends WebApplication {
@@ -116,6 +118,8 @@
 		mount("/lucene", LuceneSearchPage.class);
 		mount("/project", ProjectPage.class, "p");
 		mount("/projects", ProjectsPage.class);
+		mount("/user", UserPage.class, "user");
+		mount("/forks", ForksPage.class, "r");
 	}
 
 	private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index c427dd3..50c43fe 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -318,4 +318,18 @@
 gb.projects = projects
 gb.project = project
 gb.allProjects = all projects
-gb.copyToClipboard = copy to clipboard
\ No newline at end of file
+gb.copyToClipboard = copy to clipboard
+gb.fork = fork
+gb.forks = forks
+gb.forkRepository = fork {0}?
+gb.repositoryForked = {0} has been forked
+gb.repositoryForkFailed= failed to fork {1}
+gb.personalRepositories = personal repositories
+gb.allowForks = allow forks
+gb.allowForksDescription = allow authorized users to fork this repository
+gb.forkedFrom = forked from
+gb.canFork = can fork
+gb.canForkDescription = user is permitted to fork authorized repositories
+gb.myFork = view my fork
+gb.forksProhibited = forks prohibited
+gb.forksProhibitedWarning = this repository forbids forks
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java
index f9f90b0..00d9677 100644
--- a/src/com/gitblit/wicket/pages/BasePage.java
+++ b/src/com/gitblit/wicket/pages/BasePage.java
@@ -18,7 +18,6 @@
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -36,6 +35,7 @@
 import org.apache.wicket.MarkupContainer;
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.RedirectToUrlException;
+import org.apache.wicket.RequestCycle;
 import org.apache.wicket.RestartResponseException;
 import org.apache.wicket.markup.html.CSSPackageResource;
 import org.apache.wicket.markup.html.WebPage;
@@ -63,7 +63,6 @@
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.LinkPanel;
 
@@ -235,9 +234,9 @@
 		return req.getServerName();
 	}
 	
-	protected String getRepositoryUrl(RepositoryModel repository) {
+	public static String getRepositoryUrl(RepositoryModel repository) {
 		StringBuilder sb = new StringBuilder();
-		sb.append(WicketUtils.getGitblitURL(getRequestCycle().getRequest()));
+		sb.append(WicketUtils.getGitblitURL(RequestCycle.get().getRequest()));
 		sb.append(Constants.GIT_PATH);
 		sb.append(repository.name);
 		
@@ -252,7 +251,7 @@
 	
 	protected List<ProjectModel> getProjectModels() {
 		final UserModel user = GitBlitWebSession.get().getUser();
-		List<ProjectModel> projects = GitBlit.self().getProjectModels(user);
+		List<ProjectModel> projects = GitBlit.self().getProjectModels(user, true);
 		return projects;
 	}
 	
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
index 2bb5776..6809c2d 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -34,10 +34,12 @@
 					</wicket:container>
 				</td></tr>
 				<tr><th colspan="2"><hr/></th></tr>
+				<tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="17" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
+				<tr><th colspan="2"><hr/></th></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
 				<tr><td colspan="2"><h3><wicket:message key="gb.federation"></wicket:message> &nbsp;<small><wicket:message key="gb.federationRepositoryDescription"></wicket:message></small></h3></td></tr>	
-				<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="17" /></td></tr>
+				<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="18" /></td></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
 				<tr><td colspan="2"><h3><wicket:message key="gb.search"></wicket:message> &nbsp;<small><wicket:message key="gb.indexedBranchesDescription"></wicket:message></small></h3></td></tr>	
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.indexedBranches"></wicket:message></th><td style="padding:2px;"><span wicket:id="indexedBranches"></span></td></tr>
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
index 505cb54..214d0f0 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -343,6 +343,7 @@
 		form.add(new TextField<String>("description"));
 		form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames())
 				.setEnabled(GitBlitWebSession.get().canAdmin()));
+		form.add(new CheckBox("allowForks"));
 		form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
 				.asList(AccessRestrictionType.values()), new AccessRestrictionRenderer()));
 		form.add(new CheckBox("isFrozen"));
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.html b/src/com/gitblit/wicket/pages/EditUserPage.html
index 1c076bd..685eb64 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.html
+++ b/src/com/gitblit/wicket/pages/EditUserPage.html
@@ -17,12 +17,13 @@
 				<tr><th><wicket:message key="gb.displayName"></wicket:message></th><td class="edit"><input type="text" wicket:id="displayName" size="30" tabindex="4" /></td></tr>
 				<tr><th><wicket:message key="gb.emailAddress"></wicket:message></th><td class="edit"><input type="text" wicket:id="emailAddress" size="30" tabindex="5" /></td></tr>
 				<tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>				
-				<tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>				
+				<tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>				
+				<tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>				
 				<tr><td colspan="2" style="padding-top:15px;"><h3><wicket:message key="gb.accessPermissions"></wicket:message> &nbsp;<small><wicket:message key="gb.accessPermissionsForUserDescription"></wicket:message></small></h3></td></tr>	
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
 				<tr><td colspan="2"><hr></hr></td></tr>
 				<tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
-				<tr><td colspan='2'><div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="8" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="9" /></div></td></tr>
+				<tr><td colspan='2'><div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="10" /></div></td></tr>
 			</tbody>
 		</table>
 	</form>	
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index cfe7c35..31f91c1 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -231,6 +231,7 @@
 		form.add(new TextField<String>("displayName").setEnabled(editDisplayName));
 		form.add(new TextField<String>("emailAddress").setEnabled(editEmailAddress));
 		form.add(new CheckBox("canAdmin"));
+		form.add(new CheckBox("canFork"));
 		form.add(new CheckBox("excludeFromFederation"));
 		form.add(repositories);
 		form.add(teams.setEnabled(editTeams));
diff --git a/src/com/gitblit/wicket/pages/ForksPage.html b/src/com/gitblit/wicket/pages/ForksPage.html
new file mode 100644
index 0000000..68f8489
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/ForksPage.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:extend>
+
+	<div class="forkSource">
+		<b><span class="repositorySwatch" wicket:id="forkSourceSwatch"></span></b>
+		<span wicket:id="forkSourceAvatar" style="vertical-align: baseline;"></span>
+		<span wicket:id="forkSourceProject">[a project]</span> / <span wicket:id="forkSource">[a fork]</span> 
+	</div>
+	
+	<div wicket:id="fork">
+		<div class="forkEntry">
+			<span wicket:id="anAvatar" style="vertical-align: baseline;"></span>
+			<span wicket:id="aProject">[a project]</span> / <span wicket:id="aFork">[a fork]</span>
+		</div>
+	</div>
+</wicket:extend>	
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/ForksPage.java b/src/com/gitblit/wicket/pages/ForksPage.java
new file mode 100644
index 0000000..7b8235b
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/ForksPage.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2012 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.wicket.pages;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class ForksPage extends RepositoryPage {
+
+	public ForksPage(PageParameters params) {
+		super(params);
+		
+		RepositoryModel model = getRepositoryModel();
+		RepositoryModel origin;
+		List<String> list;
+		if (ArrayUtils.isEmpty(model.forks)) {
+			// origin repository has forks
+			origin = GitBlit.self().getRepositoryModel(model.originRepository);
+			list = new ArrayList<String>(origin.forks);
+		} else {
+			// this repository has forks
+			origin = model;
+			list = new ArrayList<String>(model.forks);
+		}
+		
+		if (origin.isPersonalRepository()) {
+			// personal repository
+			UserModel user = GitBlit.self().getUserModel(origin.projectPath.substring(1));
+			PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
+			add(new GravatarImage("forkSourceAvatar", ident, 20));
+			add(new Label("forkSourceSwatch").setVisible(false));
+			add(new LinkPanel("forkSourceProject", null, user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(user.username)));
+		} else {
+			// standard repository
+			add(new GravatarImage("forkSourceAvatar", new PersonIdent("", ""), 20).setVisible(false));
+			Component swatch;
+			if (origin.isBare){
+				swatch = new Label("forkSourceSwatch", "&nbsp;").setEscapeModelStrings(false);
+			} else {
+				swatch = new Label("forkSourceSwatch", "!");
+				WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
+			}
+			WicketUtils.setCssBackground(swatch, origin.toString());
+			add(swatch);
+			final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
+			swatch.setVisible(showSwatch);
+			
+			String projectName = origin.projectPath;
+			if (StringUtils.isEmpty(projectName)) {
+				projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+			}
+			add(new LinkPanel("forkSourceProject", null, projectName, ProjectPage.class, WicketUtils.newProjectParameter(origin.projectPath)));
+		}
+		
+		String source = StringUtils.getLastPathElement(origin.name);
+		add(new LinkPanel("forkSource", null, StringUtils.stripDotGit(source), SummaryPage.class, WicketUtils.newRepositoryParameter(origin.name)));
+
+		// only display user-accessible forks
+		UserModel user = GitBlitWebSession.get().getUser();
+		List<RepositoryModel> forks = new ArrayList<RepositoryModel>();
+		for (String aFork : list) {
+			RepositoryModel fork = GitBlit.self().getRepositoryModel(user, aFork);
+			if (fork != null) {
+				forks.add(fork);
+			}
+		}
+		
+		ListDataProvider<RepositoryModel> forksDp = new ListDataProvider<RepositoryModel>(forks);
+		DataView<RepositoryModel> forksList = new DataView<RepositoryModel>("fork", forksDp) {
+			private static final long serialVersionUID = 1L;
+
+			public void populateItem(final Item<RepositoryModel> item) {
+				RepositoryModel fork = item.getModelObject();
+				
+				if (fork.isPersonalRepository()) {
+					UserModel user = GitBlit.self().getUserModel(fork.projectPath.substring(1));
+					PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
+					item.add(new GravatarImage("anAvatar", ident, 20));
+					item.add(new LinkPanel("aProject", null, user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(user.username)));
+				} else {
+					PersonIdent ident = new PersonIdent(fork.name, fork.name);
+					item.add(new GravatarImage("anAvatar", ident, 20));
+					item.add(new LinkPanel("aProject", null, fork.projectPath, ProjectPage.class, WicketUtils.newProjectParameter(fork.projectPath)));
+				}
+				
+				String repo = StringUtils.getLastPathElement(fork.name);
+				item.add(new LinkPanel("aFork", null, StringUtils.stripDotGit(repo), SummaryPage.class, WicketUtils.newRepositoryParameter(fork.name)));
+				
+				WicketUtils.setCssStyle(item, "margin-left:25px;");
+			}
+		};
+		
+		add(forksList);
+
+	}
+
+	@Override
+	protected String getPageName() {
+		return getString("gb.forks");
+	}
+}
diff --git a/src/com/gitblit/wicket/pages/GitSearchPage.java b/src/com/gitblit/wicket/pages/GitSearchPage.java
index ca813ac..6b0714f 100644
--- a/src/com/gitblit/wicket/pages/GitSearchPage.java
+++ b/src/com/gitblit/wicket/pages/GitSearchPage.java
@@ -36,7 +36,7 @@
 		int nextPage = pageNumber + 1;
 
 		SearchPanel search = new SearchPanel("searchPanel", repositoryName, objectId, value,
-				searchType, getRepository(), -1, pageNumber - 1);
+				searchType, getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
 		boolean hasMore = search.hasMore();
 		add(search);
 
diff --git a/src/com/gitblit/wicket/pages/HistoryPage.java b/src/com/gitblit/wicket/pages/HistoryPage.java
index 122eeb8..563202e 100644
--- a/src/com/gitblit/wicket/pages/HistoryPage.java
+++ b/src/com/gitblit/wicket/pages/HistoryPage.java
@@ -32,7 +32,7 @@
 		int nextPage = pageNumber + 1;
 
 		HistoryPanel history = new HistoryPanel("historyPanel", repositoryName, objectId, path,
-				getRepository(), -1, pageNumber - 1);
+				getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
 		boolean hasMore = history.hasMore();
 		add(history);
 
diff --git a/src/com/gitblit/wicket/pages/LogPage.java b/src/com/gitblit/wicket/pages/LogPage.java
index d3dc3a9..ee8ddfe 100644
--- a/src/com/gitblit/wicket/pages/LogPage.java
+++ b/src/com/gitblit/wicket/pages/LogPage.java
@@ -37,7 +37,7 @@
 			refid = getRepositoryModel().HEAD;
 		}
 		LogPanel logPanel = new LogPanel("logPanel", repositoryName, refid, getRepository(), -1,
-				pageNumber - 1);
+				pageNumber - 1, getRepositoryModel().showRemoteBranches);
 		boolean hasMore = logPanel.hasMore();
 		add(logPanel);
 
diff --git a/src/com/gitblit/wicket/pages/ProjectPage.html b/src/com/gitblit/wicket/pages/ProjectPage.html
index db10329..3e73ba5 100644
--- a/src/com/gitblit/wicket/pages/ProjectPage.html
+++ b/src/com/gitblit/wicket/pages/ProjectPage.html
@@ -7,29 +7,6 @@
 <body>
 <wicket:extend>
 
-	<wicket:fragment wicket:id="repositoryAdminLinks">
-		<span class="link">
-			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
-			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
-			| <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
-			| <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a>
-		</span>
-	</wicket:fragment>
-
-	<wicket:fragment wicket:id="repositoryOwnerLinks">
-		<span class="link">
-			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
-			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
-			| <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
-		</span>
-	</wicket:fragment>
-
-	<wicket:fragment wicket:id="repositoryUserLinks">
-		<span class="link">
-			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
-			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
-		</span>
-	</wicket:fragment>
 
 	<div class="row">
 		<div class="span12">
@@ -60,50 +37,11 @@
 						<div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div>
 					</div>
 				</div>
-				
-				<!-- repositories -->
 				<div class="row">
-					<div class="span6" style="border-top:1px solid #eee;" wicket:id="repository">
-						<div style="padding-top:15px;padding-bottom:15px;margin-right:15px;">
-							<div class="pull-right" style="text-align:right;padding-right:15px;">
-								<span wicket:id="repositoryLinks"></span>
-
-								<div>
-        							<img class="inlineIcon" wicket:id="frozenIcon" />
-        							<img class="inlineIcon" wicket:id="federatedIcon" />
-        							
-        							<a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets">
-										<img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img>
-									</a>
-									<a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs">
-										<img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img>
-									</a>
-									<a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
-										<img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
-									</a>
-								</div>
-								<span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span>
-							</div>	
-			
-							<h3><span class="repositorySwatch" wicket:id="repositorySwatch"></span>
-								<span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span>
-								<img class="inlineIcon" wicket:id="accessRestrictionIcon" />
-							</h3>
-			
-							<div style="padding-left:20px;">
-
-        						<div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div>
-
-			        			<div style="color: #999;">
-        							<wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
-        							<span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
-        						</div>
-        
-								<div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div>
-							</div>
-						</div>
-					</div>		
-				</div>
+					<div class="span6" style="border-bottom:1px solid #eee;" wicket:id="repositoryList">
+						<span wicket:id="repository"></span>
+					</div>
+				</div>				
 			</div>
 			
 			<!-- activity tab -->
diff --git a/src/com/gitblit/wicket/pages/ProjectPage.java b/src/com/gitblit/wicket/pages/ProjectPage.java
index 808cc06..3679a6f 100644
--- a/src/com/gitblit/wicket/pages/ProjectPage.java
+++ b/src/com/gitblit/wicket/pages/ProjectPage.java
@@ -34,10 +34,7 @@
 import org.apache.wicket.RedirectException;
 import org.apache.wicket.behavior.HeaderContributor;
 import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.link.ExternalLink;
-import org.apache.wicket.markup.html.link.Link;
-import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -52,7 +49,6 @@
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ActivityUtils;
-import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.MarkdownUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebApp;
@@ -66,9 +62,7 @@
 import com.gitblit.wicket.charting.GoogleLineChart;
 import com.gitblit.wicket.charting.GooglePieChart;
 import com.gitblit.wicket.panels.ActivityPanel;
-import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
-import com.gitblit.wicket.panels.LinkPanel;
-import com.gitblit.wicket.panels.RepositoryUrlPanel;
+import com.gitblit.wicket.panels.ProjectRepositoryPanel;
 
 public class ProjectPage extends RootPage {
 	
@@ -148,143 +142,16 @@
 			}
 		});
 
-		final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
-		final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
-		final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
-		
 		final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
-		DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) {
+		DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
 			private static final long serialVersionUID = 1L;
 
 			public void populateItem(final Item<RepositoryModel> item) {
 				final RepositoryModel entry = item.getModelObject();
-
-				// repository swatch
-				Component swatch;
-				if (entry.isBare){
-					swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
-				} else {
-					swatch = new Label("repositorySwatch", "!");
-					WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
-				}
-				WicketUtils.setCssBackground(swatch, entry.toString());
-				item.add(swatch);
-				swatch.setVisible(showSwatch);
 				
-				PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
-				item.add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(projectPath, StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
-				item.add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils.isEmpty(entry.description)));
-				
-				item.add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
-				item.add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
-
-				if (entry.isFrozen) {
-					item.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png",
-							getString("gb.isFrozen")));
-				} else {
-					item.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
-				}
-
-				if (entry.isFederated) {
-					item.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png",
-							getString("gb.isFederated")));
-				} else {
-					item.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
-				}
-				switch (entry.accessRestriction) {
-				case NONE:
-					item.add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
-					break;
-				case PUSH:
-					item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
-							getAccessRestrictions().get(entry.accessRestriction)));
-					break;
-				case CLONE:
-					item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
-							getAccessRestrictions().get(entry.accessRestriction)));
-					break;
-				case VIEW:
-					item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
-							getAccessRestrictions().get(entry.accessRestriction)));
-					break;
-				default:
-					item.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
-				}
-
-				item.add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " (" + getString("gb.owner") + ")")));
-				
-				
-				UserModel user = GitBlitWebSession.get().getUser();
-				Fragment repositoryLinks;				
-				boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
-				if (showAdmin || showOwner) {
-					repositoryLinks = new Fragment("repositoryLinks",
-							showAdmin ? "repositoryAdminLinks" : "repositoryOwnerLinks", this);
-					repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
-							EditRepositoryPage.class, WicketUtils
-									.newRepositoryParameter(entry.name)));
-					if (showAdmin) {
-						Link<Void> deleteLink = new Link<Void>("deleteRepository") {
-
-							private static final long serialVersionUID = 1L;
-
-							@Override
-							public void onClick() {
-								if (GitBlit.self().deleteRepositoryModel(entry)) {
-									info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
-									// TODO dp.remove(entry);
-								} else {
-									error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
-								}
-							}
-						};
-						deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
-								getString("gb.deleteRepository"), entry)));
-						repositoryLinks.add(deleteLink);
-					}
-				} else {
-					repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
-				}
-				
-				repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
-						WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
-
-				repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class,
-						WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
-
-				item.add(repositoryLinks);
-				
-				String lastChange;
-				if (entry.lastChange.getTime() == 0) {
-					lastChange = "--";
-				} else {
-					lastChange = getTimeUtils().timeAgo(entry.lastChange);
-				}
-				Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
-				item.add(lastChangeLabel);
-				WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
-				
-				if (entry.hasCommits) {
-					// Existing repository
-					item.add(new Label("repositorySize", entry.size).setVisible(showSize));
-				} else {
-					// New repository
-					item.add(new Label("repositorySize", getString("gb.empty"))
-							.setEscapeModelStrings(false));
-				}
-				
-				item.add(new ExternalLink("syndication", SyndicationServlet.asLink("",
-						entry.name, null, 0)));
-				
-				List<String> repositoryUrls = new ArrayList<String>();
-				if (gitServlet) {
-					// add the Gitblit repository url
-					repositoryUrls.add(getRepositoryUrl(entry));
-				}
-				repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
-				
-				String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
-				item.add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
+				ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository", 
+						getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
+				item.add(row);
 			}
 		};
 		add(dataView);
@@ -434,7 +301,7 @@
 	protected List<ProjectModel> getProjectModels() {
 		if (projectModels.isEmpty()) {
 			final UserModel user = GitBlitWebSession.get().getUser();
-			List<ProjectModel> projects = GitBlit.self().getProjectModels(user);
+			List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
 			projectModels.addAll(projects);
 		}
 		return projectModels;
@@ -451,7 +318,12 @@
 	
 	protected List<DropDownMenuItem> getProjectsMenu() {
 		List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
-		List<ProjectModel> projects = getProjectModels();
+		List<ProjectModel> projects = new ArrayList<ProjectModel>();
+		for (ProjectModel model : getProjectModels()) {
+			if (!model.isUserProject()) {
+				projects.add(model);
+			}
+		}
 		int maxProjects = 15;
 		boolean showAllProjects = projects.size() > maxProjects;
 		if (showAllProjects) {
diff --git a/src/com/gitblit/wicket/pages/ProjectsPage.java b/src/com/gitblit/wicket/pages/ProjectsPage.java
index f3c4416..7161d0f 100644
--- a/src/com/gitblit/wicket/pages/ProjectsPage.java
+++ b/src/com/gitblit/wicket/pages/ProjectsPage.java
@@ -36,6 +36,7 @@
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
 import com.gitblit.models.ProjectModel;
+import com.gitblit.models.UserModel;
 import com.gitblit.utils.MarkdownUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebSession;
@@ -63,6 +64,13 @@
 	protected boolean reusePageParameters() {
 		return true;
 	}
+	
+	@Override
+	protected List<ProjectModel> getProjectModels() {
+		final UserModel user = GitBlitWebSession.get().getUser();
+		List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
+		return projects;
+	}
 
 	private void setup(PageParameters params) {
 		setupPage("", "");
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.html b/src/com/gitblit/wicket/pages/RepositoryPage.html
index de64fce..4b53dba 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.html
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.html
@@ -42,9 +42,18 @@
 			<!-- page header -->
 			<div class="pageTitle">
 				<div class="row">
-					<div wicket:id="workingCopy"></div>
-					<div class="span9">
-						<h2><span wicket:id="repositoryName">[repository name]</span> <small><span wicket:id="pageName">[page name]</span></small></h2>
+					<div class="controls">
+						<span wicket:id="workingCopyIndicator"></span>
+						<span wicket:id="forksProhibitedIndicator"></span>
+						<div class="hidden-phone btn-group pull-right">
+							<!-- future spot for other repo buttons -->
+							<a class="btn" wicket:id="myForkLink"><i class="icon-random"></i> <wicket:message key="gb.myFork"></wicket:message></a>
+							<a class="btn" wicket:id="forkLink"><i class="icon-random"></i> <wicket:message key="gb.fork"></wicket:message></a>
+						</div>
+					</div>
+					<div class="span7">
+						<div><span class="project" wicket:id="projectTitle">[project title]</span>/<span class="repository" wicket:id="repositoryName">[repository name]</span> <span wicket:id="pageName">[page name]</span></div>
+						<span wicket:id="originRepository">[origin repository]</span>
 					</div>
 				</div>
 			</div>
@@ -52,11 +61,22 @@
 			<wicket:child />
 		</div>
 		
-		<wicket:fragment wicket:id="workingCopyFragment">
-			<p class="pull-right" style="padding-top:5px;">
-				<span class="alert alert-info" style="padding: 8px 14px 8px 14px;vertical-align: middle;"><i class="icon-exclamation-sign" style="vertical-align: middle;"></i>&nbsp;<span class="hidden-phone" wicket:id="workingCopy" style="font-weight:bold;">[working copy]</span></span>
-			</p>
+		<wicket:fragment wicket:id="originFragment">
+			<p class="originRepository"><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
 		</wicket:fragment>
+		
+		<wicket:fragment wicket:id="workingCopyFragment">
+			<div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
+				<span class="alert alert-info" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-exclamation-sign"></i>&nbsp;<span class="hidden-phone" wicket:id="workingCopy" style="font-weight:bold;">[working copy]</span></span>
+			</div>
+		</wicket:fragment>
+
+		<wicket:fragment wicket:id="forksProhibitedFragment">
+			<div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
+				<span class="alert alert-error" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-ban-circle"></i>&nbsp;<span class="hidden-phone" wicket:id="forksProhibited" style="font-weight:bold;">[forks prohibited]</span></span>
+			</div>
+		</wicket:fragment>
+		
 	</wicket:extend>
 </body>
 </html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java
index eb8536c..a85d21e 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -28,10 +28,12 @@
 
 import org.apache.wicket.Component;
 import org.apache.wicket.PageParameters;
+import org.apache.wicket.RedirectException;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.form.DropDownChoice;
 import org.apache.wicket.markup.html.form.TextField;
 import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
@@ -47,8 +49,10 @@
 import com.gitblit.Keys;
 import com.gitblit.PagesServlet;
 import com.gitblit.SyndicationServlet;
+import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.SubmoduleModel;
+import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
@@ -58,6 +62,7 @@
 import com.gitblit.wicket.PageRegistration.OtherPageLink;
 import com.gitblit.wicket.SessionlessForm;
 import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.NavigationPanel;
 import com.gitblit.wicket.panels.RefsPanel;
@@ -81,10 +86,11 @@
 	public RepositoryPage(PageParameters params) {
 		super(params);
 		repositoryName = WicketUtils.getRepositoryName(params);
-		if (repositoryName.indexOf('/') > -1) {
-			projectName = repositoryName.substring(0, repositoryName.indexOf('/'));
-		} else {
+		String root =StringUtils.getFirstPathElement(repositoryName);
+		if (StringUtils.isEmpty(root)) {
 			projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+		} else {
+			projectName = root;
 		}
 		objectId = WicketUtils.getObject(params);
 		
@@ -125,7 +131,6 @@
 
 		// standard links
 		pages.put("repositories", new PageRegistration("gb.repositories", RepositoriesPage.class));
-		pages.put("project", new PageRegistration("gb.project", ProjectPage.class, WicketUtils.newProjectParameter(projectName)));
 		pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
 		pages.put("log", new PageRegistration("gb.log", LogPage.class, params));
 		pages.put("branches", new PageRegistration("gb.branches", BranchesPage.class, params));
@@ -136,6 +141,17 @@
 		Repository r = getRepository();
 		RepositoryModel model = getRepositoryModel();
 
+		// forks list button
+		if (StringUtils.isEmpty(model.originRepository)) {
+			if (!ArrayUtils.isEmpty(model.forks)) {
+				// this origin repository has forks
+				pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params));
+			}
+		} else {
+			// this is a fork of another repository
+			pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params));
+		}
+		
 		// per-repository extra page links
 		if (model.useTickets && TicgitUtils.getTicketsBranch(r) != null) {
 			pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, params));
@@ -168,19 +184,100 @@
 
 	@Override
 	protected void setupPage(String repositoryName, String pageName) {
-		add(new LinkPanel("repositoryName", null, StringUtils.stripDotGit(repositoryName),
-				SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
-		add(new Label("pageName", pageName).setRenderBodyOnly(true));
-		if (getRepositoryModel().isBare) {
-			add(new Label("workingCopy").setVisible(false));
+		String projectName = StringUtils.getFirstPathElement(repositoryName);
+		ProjectModel project = GitBlit.self().getProjectModel(projectName);
+		if (project.isUserProject()) {
+			// user-as-project
+			add(new LinkPanel("projectTitle", null, project.getDisplayName(),
+					UserPage.class, WicketUtils.newUsernameParameter(project.name.substring(1))));
 		} else {
-			Fragment fragment = new Fragment("workingCopy", "workingCopyFragment", this);
+			// project
+			add(new LinkPanel("projectTitle", null, project.name,
+					ProjectPage.class, WicketUtils.newProjectParameter(project.name)));
+		}
+		
+		String name = StringUtils.stripDotGit(repositoryName);
+		if (!StringUtils.isEmpty(projectName) && name.startsWith(projectName)) {
+			name = name.substring(projectName.length() + 1);
+		}
+		add(new LinkPanel("repositoryName", null, name, SummaryPage.class,
+				WicketUtils.newRepositoryParameter(repositoryName)));
+		add(new Label("pageName", pageName).setRenderBodyOnly(true));
+		
+		// indicate origin repository
+		RepositoryModel model = getRepositoryModel();
+		if (StringUtils.isEmpty(model.originRepository)) {
+			add(new Label("originRepository").setVisible(false));
+		} else {
+			Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
+			forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(model.originRepository), 
+					SummaryPage.class, WicketUtils.newRepositoryParameter(model.originRepository)));
+			add(forkFrag);
+		}
+		
+		if (getRepositoryModel().isBare) {
+			add(new Label("workingCopyIndicator").setVisible(false));
+		} else {
+			Fragment wc = new Fragment("workingCopyIndicator", "workingCopyFragment", this);
 			Label lbl = new Label("workingCopy", getString("gb.workingCopy"));
 			WicketUtils.setHtmlTooltip(lbl,  getString("gb.workingCopyWarning"));
-			fragment.add(lbl);
-			add(fragment);
+			wc.add(lbl);
+			add(wc);
 		}
+		
+		if (getRepositoryModel().allowForks) {
+			add(new Label("forksProhibitedIndicator").setVisible(false));
+		} else {
+			Fragment wc = new Fragment("forksProhibitedIndicator", "forksProhibitedFragment", this);
+			Label lbl = new Label("forksProhibited", getString("gb.forksProhibited"));
+			WicketUtils.setHtmlTooltip(lbl,  getString("gb.forksProhibitedWarning"));
+			wc.add(lbl);
+			add(wc);
+		}
+		
+		UserModel user = GitBlitWebSession.get().getUser();
+		
+		// fork button
+		if (user != null) {			
+			final String clonedRepo = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(model.name)));
+			boolean hasClone = GitBlit.self().hasRepository(clonedRepo) && !getRepositoryModel().name.equals(clonedRepo);
+			if (user.canForkRepository(model) && !hasClone) {
+				Link<Void> forkLink = new Link<Void>("forkLink") {
 
+					private static final long serialVersionUID = 1L;
+
+					@Override
+					public void onClick() {
+						RepositoryModel model = getRepositoryModel();
+						if (GitBlit.self().fork(model, GitBlitWebSession.get().getUser())) {
+							throw new RedirectException(SummaryPage.class, WicketUtils.newRepositoryParameter(clonedRepo));
+						} else {
+							error(MessageFormat.format(getString("gb.repositoryForkFailed"), model));
+						}
+					}
+				};
+				forkLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+						getString("gb.forkRepository"), getRepositoryModel())));
+				add(forkLink);
+			} else {
+				// user not allowed to fork or fork already exists or repo forbids forking
+				add(new ExternalLink("forkLink", "").setVisible(false));
+			}
+			
+			if (hasClone) {
+				// user has clone
+				String url = getRequestCycle().urlFor(SummaryPage.class, WicketUtils.newRepositoryParameter(clonedRepo)).toString();
+				add(new ExternalLink("myForkLink", url));
+			} else {
+				// user does not have clone
+				add(new ExternalLink("myForkLink", "").setVisible(false));
+			}
+		} else {
+			// server prohibits forking
+			add(new ExternalLink("forkLink", "").setVisible(false));
+			add(new ExternalLink("myForkLink", "").setVisible(false));
+		}
+		
 		super.setupPage(repositoryName, pageName);
 	}
 
@@ -312,7 +409,7 @@
 	}
 
 	protected void addRefs(Repository r, RevCommit c) {
-		add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r)));
+		add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
 	}
 
 	protected void addFullText(String wicketId, String text, boolean substituteRegex) {
diff --git a/src/com/gitblit/wicket/pages/RootPage.java b/src/com/gitblit/wicket/pages/RootPage.java
index 4858368..1e6f130 100644
--- a/src/com/gitblit/wicket/pages/RootPage.java
+++ b/src/com/gitblit/wicket/pages/RootPage.java
@@ -184,6 +184,9 @@
 				// remove named repository parameter
 				params.remove("r");
 
+				// remove named user parameter
+				params.remove("user");
+
 				// remove days back parameter if it is the default value
 				if (params.containsKey("db")
 						&& params.getInt("db") == GitBlit.getInteger(Keys.web.activityDuration, 14)) {
@@ -327,6 +330,12 @@
 
 		boolean hasParameter = false;
 		String projectName = WicketUtils.getProjectName(params);
+		String userName = WicketUtils.getUsername(params);
+		if (StringUtils.isEmpty(projectName)) {
+			if (!StringUtils.isEmpty(userName)) {
+				projectName = "~" + userName;
+			}
+		}
 		String repositoryName = WicketUtils.getRepositoryName(params);
 		String set = WicketUtils.getSet(params);
 		String regex = WicketUtils.getRegEx(params);
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.java b/src/com/gitblit/wicket/pages/SummaryPage.java
index 39c7269..22b1c1a 100644
--- a/src/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/com/gitblit/wicket/pages/SummaryPage.java
@@ -130,7 +130,7 @@
 		add(new Label("otherUrls", StringUtils.flattenStrings(repositoryUrls, "<br/>"))
 		.setEscapeModelStrings(false));
 
-		add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0));
+		add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0, getRepositoryModel().showRemoteBranches));
 		add(new TagsPanel("tagsPanel", repositoryName, r, numberRefs).hideIfEmpty());
 		add(new BranchesPanel("branchesPanel", getRepositoryModel(), r, numberRefs, false).hideIfEmpty());
 
diff --git a/src/com/gitblit/wicket/pages/UserPage.html b/src/com/gitblit/wicket/pages/UserPage.html
new file mode 100644
index 0000000..5886a3a
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/UserPage.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:extend>
+
+	<div class="row">
+		<div class="span4">
+			<div wicket:id="gravatar"></div>
+			<div style="text-align: left;">
+				<h2><span wicket:id="userDisplayName"></span></h2>
+				<div><i class="icon-user"></i> <span wicket:id="userUsername"></span></div>
+				<div><i class="icon-envelope"></i><span wicket:id="userEmail"></span></div>
+			</div>
+		</div>
+		
+		<div class="span8">
+			<div class="tabbable">
+				<!-- tab titles -->
+				<ul class="nav nav-tabs">
+					<li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
+				</ul>
+	
+				<!-- tab content -->
+				<div class="tab-content">
+
+					<!-- repositories tab -->
+					<div class="tab-pane active" id="repositories">
+						<table width="100%">
+							<tbody>
+								<tr wicket:id="repositoryList"><td style="border-bottom:1px solid #eee;"><span wicket:id="repository"></span></td></tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</wicket:extend>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/UserPage.java b/src/com/gitblit/wicket/pages/UserPage.java
new file mode 100644
index 0000000..cabefb4
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/UserPage.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2012 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.wicket.pages;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.RedirectException;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebApp;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.ProjectRepositoryPanel;
+
+public class UserPage extends RootPage {
+	
+	List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
+
+	public UserPage() {
+		super();
+		throw new RedirectException(GitBlitWebApp.get().getHomePage());
+	}
+
+	public UserPage(PageParameters params) {
+		super(params);
+		setup(params);
+	}
+
+	@Override
+	protected boolean reusePageParameters() {
+		return true;
+	}
+
+	private void setup(PageParameters params) {
+		setupPage("", "");
+		// check to see if we should display a login message
+		boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+		if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
+			authenticationError("Please login");
+			return;
+		}
+
+		String userName = WicketUtils.getUsername(params);
+		if (StringUtils.isEmpty(userName)) {
+			throw new RedirectException(GitBlitWebApp.get().getHomePage());
+		}
+
+		UserModel user = GitBlit.self().getUserModel(userName);
+		if (user == null) {
+			// construct a temporary user model
+			user = new UserModel(userName);
+		}
+		
+		String projectName = "~" + userName;
+		
+		ProjectModel project = GitBlit.self().getProjectModel(projectName);
+		if (project == null) {
+			throw new RedirectException(GitBlitWebApp.get().getHomePage());
+		}
+		
+		add(new Label("userDisplayName", user.getDisplayName()));
+		add(new Label("userUsername", user.username));
+		LinkPanel email = new LinkPanel("userEmail", null, user.emailAddress, "mailto:#");
+		email.setRenderBodyOnly(true);
+		add(email.setVisible(GitBlit.getBoolean(Keys.web.showEmailAddresses, true) && !StringUtils.isEmpty(user.emailAddress)));
+		
+		PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress);
+		add(new GravatarImage("gravatar", person, 210));
+		
+		List<RepositoryModel> repositories = getRepositories(params);
+		
+		Collections.sort(repositories, new Comparator<RepositoryModel>() {
+			@Override
+			public int compare(RepositoryModel o1, RepositoryModel o2) {
+				// reverse-chronological sort
+				return o2.lastChange.compareTo(o1.lastChange);
+			}
+		});
+
+		final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
+		DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
+			private static final long serialVersionUID = 1L;
+
+			public void populateItem(final Item<RepositoryModel> item) {
+				final RepositoryModel entry = item.getModelObject();
+				
+				ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository", 
+						getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
+				item.add(row);
+			}
+		};
+		add(dataView);
+	}
+
+	@Override
+	protected void addDropDownMenus(List<PageRegistration> pages) {
+		PageParameters params = getPageParameters();
+
+		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+				UserPage.class);
+		// preserve time filter option on repository choices
+		menu.menuItems.addAll(getRepositoryFilterItems(params));
+
+		// preserve repository filter option on time choices
+		menu.menuItems.addAll(getTimeFilterItems(params));
+
+		if (menu.menuItems.size() > 0) {
+			// Reset Filter
+			menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+		}
+
+		pages.add(menu);
+	}
+}
diff --git a/src/com/gitblit/wicket/panels/GravatarImage.java b/src/com/gitblit/wicket/panels/GravatarImage.java
index b1c7b65..f26a902 100644
--- a/src/com/gitblit/wicket/panels/GravatarImage.java
+++ b/src/com/gitblit/wicket/panels/GravatarImage.java
@@ -48,7 +48,7 @@
 	public GravatarImage(String id, PersonIdent person, int width) {
 		super(id);
 
-		String email = person.getEmailAddress().toLowerCase();
+		String email = person.getEmailAddress() == null ? person.getName().toLowerCase() : person.getEmailAddress().toLowerCase();
 		String hash = StringUtils.getMD5(email);
 		Link<Void> link = new BookmarkablePageLink<Void>("link", GravatarProfilePage.class,
 				WicketUtils.newObjectParameter(hash));
diff --git a/src/com/gitblit/wicket/panels/HistoryPanel.java b/src/com/gitblit/wicket/panels/HistoryPanel.java
index befd701..14aed91 100644
--- a/src/com/gitblit/wicket/panels/HistoryPanel.java
+++ b/src/com/gitblit/wicket/panels/HistoryPanel.java
@@ -54,7 +54,7 @@
 	private boolean hasMore;
 
 	public HistoryPanel(String wicketId, final String repositoryName, final String objectId,
-			final String path, Repository r, int limit, int pageOffset) {
+			final String path, Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
 		super(wicketId);
 		boolean pageResults = limit <= 0;
 		int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -74,7 +74,7 @@
 		}
 		final boolean isTree = matchingPath == null ? true : matchingPath.isTree();
 
-		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
+		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
 		List<RevCommit> commits;
 		if (pageResults) {
 			// Paging result set
diff --git a/src/com/gitblit/wicket/panels/LogPanel.java b/src/com/gitblit/wicket/panels/LogPanel.java
index f441ba5..a31c3df 100644
--- a/src/com/gitblit/wicket/panels/LogPanel.java
+++ b/src/com/gitblit/wicket/panels/LogPanel.java
@@ -49,7 +49,7 @@
 	private boolean hasMore;
 
 	public LogPanel(String wicketId, final String repositoryName, final String objectId,
-			Repository r, int limit, int pageOffset) {
+			Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
 		super(wicketId);
 		boolean pageResults = limit <= 0;
 		int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -57,7 +57,7 @@
 			itemsPerPage = 50;
 		}
 
-		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
+		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
 		List<RevCommit> commits;
 		if (pageResults) {
 			// Paging result set
diff --git a/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
new file mode 100644
index 0000000..4678153
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<wicket:panel>
+	<wicket:fragment wicket:id="repositoryAdminLinks">
+		<span class="link">
+			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+			| <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
+			| <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a>
+		</span>
+	</wicket:fragment>
+
+	<wicket:fragment wicket:id="repositoryOwnerLinks">
+		<span class="link">
+			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+			| <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
+		</span>
+	</wicket:fragment>
+
+	<wicket:fragment wicket:id="repositoryUserLinks">
+		<span class="link">
+			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+			| <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+		</span>
+	</wicket:fragment>
+
+	<wicket:fragment wicket:id="originFragment">
+		<p class="originRepository" style="margin-left:20px;" ><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
+	</wicket:fragment>
+
+	<div>
+		<div style="padding-top:15px;padding-bottom:15px;margin-right:15px;">
+			<div class="pull-right" style="text-align:right;padding-right:15px;">
+				<span wicket:id="repositoryLinks"></span>
+				<div>
+					<img class="inlineIcon" wicket:id="frozenIcon" />
+					<img class="inlineIcon" wicket:id="federatedIcon" />
+        						
+					<a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets">
+						<img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img>
+					</a>
+					<a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs">
+						<img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img>
+					</a>
+					<a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
+						<img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
+					</a>
+				</div>
+				<span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span>
+			</div>	
+			
+			<div class="pageTitle" style="border:0px;">
+				<div>
+					<span class="repositorySwatch" wicket:id="repositorySwatch"></span>
+					<span class="repository" style="padding-left:3px;color:black;" wicket:id="repositoryName">[repository name]</span>
+					<img class="inlineIcon" style="vertical-align:baseline" wicket:id="accessRestrictionIcon" />
+				</div>
+				<span wicket:id="originRepository">[origin repository]</span>
+			</div>
+			
+			<div style="padding-left:20px;">
+				<div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div>
+
+    			<div style="color: #999;">
+					<wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
+					<span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
+				</div>
+        
+				<div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div>
+			</div>
+		</div>
+	</div>
+</wicket:panel>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
new file mode 100644
index 0000000..f7deaf1
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2012 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.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.Localizer;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.DocsPage;
+import com.gitblit.wicket.pages.EditRepositoryPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TicketsPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class ProjectRepositoryPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public ProjectRepositoryPanel(String wicketId, Localizer localizer, Component owner,
+			final boolean isAdmin, final RepositoryModel entry,
+			final Map<AccessRestrictionType, String> accessRestrictions) {
+		super(wicketId);
+
+		final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
+		final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
+		final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
+
+		// repository swatch
+		Component swatch;
+		if (entry.isBare) {
+			swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
+		} else {
+			swatch = new Label("repositorySwatch", "!");
+			WicketUtils.setHtmlTooltip(swatch, localizer.getString("gb.workingCopyWarning", owner));
+		}
+		WicketUtils.setCssBackground(swatch, entry.toString());
+		add(swatch);
+		swatch.setVisible(showSwatch);
+
+		PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
+		add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(entry.projectPath,
+				StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
+		add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils
+				.isEmpty(entry.description)));
+
+		if (StringUtils.isEmpty(entry.originRepository)) {
+			add(new Label("originRepository").setVisible(false));
+		} else {
+			Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
+			forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(entry.originRepository), 
+					SummaryPage.class, WicketUtils.newRepositoryParameter(entry.originRepository)));
+			add(forkFrag);
+		}
+
+		add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
+		add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
+
+		if (entry.isFrozen) {
+			add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", localizer.getString("gb.isFrozen", owner)));
+		} else {
+			add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
+		}
+
+		if (entry.isFederated) {
+			add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", localizer.getString("gb.isFederated", owner)));
+		} else {
+			add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
+		}
+		switch (entry.accessRestriction) {
+		case NONE:
+			add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
+			break;
+		case PUSH:
+			add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
+					accessRestrictions.get(entry.accessRestriction)));
+			break;
+		case CLONE:
+			add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
+					accessRestrictions.get(entry.accessRestriction)));
+			break;
+		case VIEW:
+			add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
+					accessRestrictions.get(entry.accessRestriction)));
+			break;
+		default:
+			add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+		}
+
+		add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " ("
+				+ localizer.getString("gb.owner", owner) + ")")));
+
+		UserModel user = GitBlitWebSession.get().getUser();
+		Fragment repositoryLinks;
+		boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
+		// owner of personal repository gets admin powers
+		boolean showAdmin = isAdmin || entry.isUsersPersonalRepository(user.username);
+
+		if (showAdmin || showOwner) {
+			repositoryLinks = new Fragment("repositoryLinks", showAdmin ? "repositoryAdminLinks"
+					: "repositoryOwnerLinks", this);
+			repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class,
+					WicketUtils.newRepositoryParameter(entry.name)));
+			if (showAdmin) {
+				Link<Void> deleteLink = new Link<Void>("deleteRepository") {
+
+					private static final long serialVersionUID = 1L;
+
+					@Override
+					public void onClick() {
+						if (GitBlit.self().deleteRepositoryModel(entry)) {
+							info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
+							// TODO dp.remove(entry);
+						} else {
+							error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
+						}
+					}
+				};
+				deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+						localizer.getString("gb.deleteRepository", owner), entry)));
+				repositoryLinks.add(deleteLink);
+			}
+		} else {
+			repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
+		}
+
+		repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+				.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
+
+		repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
+				.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
+
+		add(repositoryLinks);
+
+		String lastChange;
+		if (entry.lastChange.getTime() == 0) {
+			lastChange = "--";
+		} else {
+			lastChange = getTimeUtils().timeAgo(entry.lastChange);
+		}
+		Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
+		add(lastChangeLabel);
+		WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
+
+		if (entry.hasCommits) {
+			// Existing repository
+			add(new Label("repositorySize", entry.size).setVisible(showSize));
+		} else {
+			// New repository
+			add(new Label("repositorySize", localizer.getString("gb.empty", owner)).setEscapeModelStrings(false));
+		}
+
+		add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0)));
+
+		List<String> repositoryUrls = new ArrayList<String>();
+		if (gitServlet) {
+			// add the Gitblit repository url
+			repositoryUrls.add(BasePage.getRepositoryUrl(entry));
+		}
+		repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
+
+		String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
+		add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
+	}
+}
diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/com/gitblit/wicket/panels/RepositoriesPanel.html
index 5da43e0..6c19fc5 100644
--- a/src/com/gitblit/wicket/panels/RepositoriesPanel.html
+++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -72,7 +72,7 @@
 	
 	<wicket:fragment wicket:id="groupRepositoryRow">
         <td colspan="1"><span wicket:id="groupName">[group name]</span></td>
-        <td colspan="6"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
+        <td colspan="6" style="padding: 2px;"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
 	</wicket:fragment>
 		
 	<wicket:fragment wicket:id="repositoryRow">
diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
index a113e00..0855780 100644
--- a/src/com/gitblit/wicket/panels/RepositoriesPanel.java
+++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -58,6 +58,7 @@
 import com.gitblit.wicket.pages.ProjectPage;
 import com.gitblit.wicket.pages.RepositoriesPage;
 import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.UserPage;
 
 public class RepositoriesPanel extends BasePanel {
 
@@ -116,7 +117,7 @@
 			}
 						
 			Map<String, ProjectModel> projects = new HashMap<String, ProjectModel>();
-			for (ProjectModel project : GitBlit.self().getProjectModels(user)) {
+			for (ProjectModel project : GitBlit.self().getProjectModels(user, true)) {
 				projects.put(project.name, project);
 			}
 			List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>();
@@ -138,7 +139,7 @@
 
 		final String baseUrl = WicketUtils.getGitblitURL(getRequest());
 		final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
-
+		
 		DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
 			private static final long serialVersionUID = 1L;
 			int counter;
@@ -156,8 +157,19 @@
 					currGroupName = entry.name;
 					Fragment row = new Fragment("rowContent", "groupRepositoryRow", this);
 					item.add(row);
-					row.add(new LinkPanel("groupName", null, entry.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name)));
-					row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
+					
+					String name = entry.toString();
+					if (name.charAt(0) == '~') {
+						// user page
+						String username = name.substring(1);
+						UserModel user = GitBlit.self().getUserModel(username);
+						row.add(new LinkPanel("groupName", null, user == null ? username : user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(username)));
+						row.add(new Label("groupDescription", getString("gb.personalRepositories")));
+					} else {
+						// project page
+						row.add(new LinkPanel("groupName", null, name, ProjectPage.class, WicketUtils.newProjectParameter(name)));
+						row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
+					}
 					WicketUtils.setCssClass(item, "group");
 					// reset counter so that first row is light background
 					counter = 0;
@@ -272,7 +284,8 @@
 				WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
 
 				boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
-				if (showAdmin) {
+				boolean myPersonalRepository = showOwner && entry.isUsersPersonalRepository(user.username);
+				if (showAdmin || myPersonalRepository) {
 					Fragment repositoryLinks = new Fragment("repositoryLinks",
 							"repositoryAdminLinks", this);
 					repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
diff --git a/src/com/gitblit/wicket/panels/SearchPanel.java b/src/com/gitblit/wicket/panels/SearchPanel.java
index eab3aea..9d38ab0 100644
--- a/src/com/gitblit/wicket/panels/SearchPanel.java
+++ b/src/com/gitblit/wicket/panels/SearchPanel.java
@@ -47,7 +47,8 @@
 	private boolean hasMore;
 
 	public SearchPanel(String wicketId, final String repositoryName, final String objectId,
-			final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset) {
+			final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset,
+			boolean showRemoteRefs) {
 		super(wicketId);
 		boolean pageResults = limit <= 0;
 		int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -57,7 +58,7 @@
 
 		RevCommit commit = JGitUtils.getCommit(r, objectId);
 
-		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
+		final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
 		List<RevCommit> commits;
 		if (pageResults) {
 			// Paging result set

--
Gitblit v1.9.1