Implemented application menus for repository url panel
diff --git a/src/main/distrib/data/clientapps.json b/src/main/distrib/data/clientapps.json
index 95e45f9..9aa1e58 100644
--- a/src/main/distrib/data/clientapps.json
+++ b/src/main/distrib/data/clientapps.json
@@ -1,41 +1,63 @@
 [

 	{

+		"name": "Git",

+		"title": "Git",

+		"description": "a fast, open-source, distributed VCS",

+		"legal": "released under the GPLv2 open source license",

+		"command": "git clone {0}",

+		"productUrl": "http://git-scm.com",

+		"icon": "git-black_32x32.png",

+		"isActive": true

+	},

+	{

 		"name": "SmartGit/Hg",

+		"title": "syntevo SmartGit/Hg\u2122",

+		"description": "a Git client for Windows, Mac, & Linux",

+		"legal": "\u00a9 2013 syntevo GmbH. All rights reserved.",

 		"cloneUrl": "smartgit://cloneRepo/{0}",

 		"productUrl": "http://www.syntevo.com/smartgithg",

-		"attribution": "Syntevo SmartGit/Hg\u2122",

 		"platforms": [ "windows", "macintosh", "linux" ],

+		"icon": "smartgithg_32x32.png",

 		"isActive": false

 	},

 	{

 		"name": "SourceTree",

+		"title": "Atlassian SourceTree\u2122",

+		"description": "a free Git client for Windows or Mac",

+		"legal": "\u00a9 2013 Atlassian. All rights reserved.",

 		"cloneUrl": "sourcetree://cloneRepo/{0}",

 		"productUrl": "http://sourcetreeapp.com",

-		"attribution": "Atlassian SourceTree\u2122",

 		"platforms": [ "windows", "macintosh" ],

+		"icon": "sourcetree_32x32.png",

 		"isActive": true

 	},

 	{

 		"name": "Tower",

+		"title": "fournova Tower\u2122",

+		"description": "a Git client for Mac",

+		"legal": "\u00a9 2013 fournova Software GmbH. All rights reserved.",

 		"cloneUrl": "gittower://openRepo/{0}",

 		"productUrl": "http://www.git-tower.com",

-		"attribution": "fournova Tower\u2122",

 		"platforms": [ "macintosh" ],

 		"isActive": true

 	},

 	{

-		"name": "GitHub for Macintosh",

+		"name": "GitHub",

+		"title": "GitHub\u2122 for Macintosh",

+		"description": "a free Git client for Mac OS X",

+		"legal": "\u00a9 2013 GitHub. All rights reserved.",

 		"cloneUrl": "github-mac://openRepo/{0}",

 		"productUrl": "http://mac.github.com",

-		"attribution": "GitHub\u2122 for Macintosh",

 		"platforms": [ "macintosh" ],

 		"isActive": false

 	},

 	{

-		"name": "GitHub for Windows",

+		"name": "GitHub",

+		"title": "GitHub\u2122 for Windows",

+		"description": "a free Git client for Windows",

+		"legal": "\u00a9 2013 GitHub. All rights reserved.",

 		"cloneUrl": "github-windows://openRepo/{0}",

 		"productUrl": "http://windows.github.com",

-		"attribution": "GitHub\u2122 for Windows",

 		"platforms": [ "windows" ],

 		"isActive": false

 	}

diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 42a1434..9346e0a 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -91,15 +91,16 @@
 import com.gitblit.fanout.FanoutService;

 import com.gitblit.fanout.FanoutSocketService;

 import com.gitblit.git.GitDaemon;

-import com.gitblit.models.GitClientApplication;

 import com.gitblit.models.FederationModel;

 import com.gitblit.models.FederationProposal;

 import com.gitblit.models.FederationSet;

 import com.gitblit.models.ForkModel;

+import com.gitblit.models.GitClientApplication;

 import com.gitblit.models.Metric;

 import com.gitblit.models.ProjectModel;

 import com.gitblit.models.RegistrantAccessPermission;

 import com.gitblit.models.RepositoryModel;

+import com.gitblit.models.RepositoryUrl;

 import com.gitblit.models.SearchResult;

 import com.gitblit.models.ServerSettings;

 import com.gitblit.models.ServerStatus;

@@ -201,7 +202,7 @@
 	private FanoutService fanoutService;

 

 	private GitDaemon gitDaemon;

-	

+

 	public GitBlit() {

 		if (gitblit == null) {

 			// set the static singleton reference

@@ -460,23 +461,106 @@
 		serverStatus.heapFree = Runtime.getRuntime().freeMemory();

 		return serverStatus;

 	}

-

+	

 	/**

-	 * Returns the list of non-Gitblit clone urls. This allows Gitblit to

-	 * advertise alternative urls for Git client repository access.

+	 * Returns a list of repository URLs and the user access permission.

 	 * 

-	 * @param repositoryName

-	 * @param userName

-	 * @return list of non-gitblit clone urls

+	 * @param request

+	 * @param user

+	 * @param repository

+	 * @return a list of repository urls

 	 */

-	public List<String> getOtherCloneUrls(String repositoryName, String username) {

-		List<String> cloneUrls = new ArrayList<String>();

-		for (String url : settings.getStrings(Keys.web.otherUrls)) {

-			cloneUrls.add(MessageFormat.format(url, repositoryName, username));

+	public List<RepositoryUrl> getRepositoryUrls(HttpServletRequest request, UserModel user, RepositoryModel repository) {

+		if (user == null) {

+			user = UserModel.ANONYMOUS;

 		}

-		return cloneUrls;

+		String username = UserModel.ANONYMOUS.equals(user) ? "" : user.username;

+

+		List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();

+		// http/https url

+		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {

+			AccessPermission permission = user.getRepositoryPermission(repository).permission;

+			if (permission.exceeds(AccessPermission.NONE)) {

+				list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission));

+			}

+		}

+

+		// git daemon url

+		String gitDaemonUrl = getGitDaemonUrl(request, user, repository);

+		if (!StringUtils.isEmpty(gitDaemonUrl)) {

+			AccessPermission permission = getGitDaemonAccessPermission(user, repository);

+			if (permission.exceeds(AccessPermission.NONE)) {

+				list.add(new RepositoryUrl(gitDaemonUrl, permission));

+			}

+		}

+

+		// add all other urls

+		// {0} = repository

+		// {1} = username

+		for (String url : settings.getStrings(Keys.web.otherUrls)) {

+			if (url.contains("{1}")) {

+				// external url requires username, only add url IF we have one

+				if(!StringUtils.isEmpty(username)) {

+					list.add(new RepositoryUrl(MessageFormat.format(url, repository.name, username), null));

+				}

+			} else {

+				// external url does not require username

+				list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null));

+			}

+		}

+		return list;

 	}

 	

+	protected String getRepositoryUrl(HttpServletRequest request, String username, RepositoryModel repository) {

+		StringBuilder sb = new StringBuilder();

+		sb.append(HttpUtils.getGitblitURL(request));

+		sb.append(Constants.GIT_PATH);

+		sb.append(repository.name);

+		

+		// inject username into repository url if authentication is required

+		if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE)

+				&& !StringUtils.isEmpty(username)) {

+			sb.insert(sb.indexOf("://") + 3, username + "@");

+		}

+		return sb.toString();

+	}

+	

+	protected String getGitDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {

+		if (gitDaemon != null) {

+			String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");

+			if (bindInterface.equals("localhost")

+					&& (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) {

+				// git daemon is bound to localhost and the request is from elsewhere

+				return null;

+			}

+			if (user.canClone(repository)) {

+				String servername = request.getServerName();

+				String url = gitDaemon.formatUrl(servername, repository.name);

+				return url;

+			}

+		}

+		return null;

+	}

+	

+	protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {

+		if (gitDaemon != null && user.canClone(repository)) {

+			AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;

+			if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {

+				if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {

+					// can not authenticate clone via anonymous git protocol

+					gitDaemonPermission = AccessPermission.NONE;

+				} else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {

+					// can not authenticate push via anonymous git protocol

+					gitDaemonPermission = AccessPermission.CLONE;

+				} else {

+					// normal user permission

+				}

+			}

+			return gitDaemonPermission;

+		}

+		return AccessPermission.NONE;

+	}

+

 	/**

 	 * Returns the list of custom client applications to be used for the

 	 * repository url panel;

@@ -3283,8 +3367,8 @@
 	}

 	

 	protected void configureGitDaemon() {

-		String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");

 		int port = settings.getInteger(Keys.git.daemonPort, 0);

+		String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");

 		if (port > 0) {

 			try {

 				gitDaemon = new GitDaemon(bindInterface, port, getRepositoriesFolder());

diff --git a/src/main/java/com/gitblit/git/GitDaemon.java b/src/main/java/com/gitblit/git/GitDaemon.java
index 7050f87..3c45171 100644
--- a/src/main/java/com/gitblit/git/GitDaemon.java
+++ b/src/main/java/com/gitblit/git/GitDaemon.java
@@ -177,6 +177,20 @@
 					}

 				} };

 	}

+	

+	public int getPort() {

+		return myAddress.getPort();

+	}

+	

+	public String formatUrl(String servername, String repository) {

+		if (getPort() == 9418) {

+			// standard port

+			return MessageFormat.format("git://{0}/{1}", servername, repository);

+		} else {

+			// non-standard port

+			return MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, getPort(), repository);

+		}

+	}

 

 	/** @return timeout (in seconds) before aborting an IO operation. */

 	public int getTimeout() {

diff --git a/src/main/java/com/gitblit/models/GitClientApplication.java b/src/main/java/com/gitblit/models/GitClientApplication.java
index dbdfa39..fd53059 100644
--- a/src/main/java/com/gitblit/models/GitClientApplication.java
+++ b/src/main/java/com/gitblit/models/GitClientApplication.java
@@ -31,13 +31,15 @@
 	private static final long serialVersionUID = 1L;

 

 	public String name;

+	public String title;

+	public String description;

+	public String legal;

+	public String icon;

 	public String cloneUrl;

 	public String command;

 	public String productUrl;

-	public String attribution;

-	public boolean isApplication = true;

-	public boolean isActive = true;

 	public String[] platforms;

+	public boolean isActive;

 

 	public boolean allowsPlatform(String p) {

 		if (ArrayUtils.isEmpty(platforms)) {

@@ -55,4 +57,9 @@
 		}

 		return false;

 	}

+	

+	@Override

+	public String toString() {

+		return StringUtils.isEmpty(title) ? name : title;

+	}

 }
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RepositoryUrl.java b/src/main/java/com/gitblit/models/RepositoryUrl.java
new file mode 100644
index 0000000..d72959a
--- /dev/null
+++ b/src/main/java/com/gitblit/models/RepositoryUrl.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2013 gitblit.com.

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *     http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+package com.gitblit.models;

+

+import java.io.Serializable;

+

+import com.gitblit.Constants.AccessPermission;

+

+/**

+ * Represents a git repository url and it's associated access permission for the

+ * current user.

+ *  

+ * @author James Moger

+ *

+ */

+public class RepositoryUrl implements Serializable {

+

+	private static final long serialVersionUID = 1L;

+

+	public final String url;

+	public final AccessPermission permission;

+

+	public RepositoryUrl(String url, AccessPermission permission) {

+		this.url = url;

+		this.permission = permission;

+	}

+	

+	public boolean isExternal() {

+		return permission == null;

+	}

+

+	@Override

+	public String toString() {

+		return url;

+	}

+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 9355b80..7ebea4e 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -449,8 +449,5 @@
 gb.enableIncrementalPushTags = enable incremental push tags

 gb.useIncrementalPushTagsDescription = on push, automatically tag each branch tip with an incremental revision number

 gb.incrementalPushTagMessage = Auto-tagged [{0}] branch on push

-gb.externalPermissions = {0} access permissions for {1} are externally maintained

-gb.viewAccess = You do not have Gitblit read or write access

-gb.yourProtocolPermissionIs = Your {0} access permission for {1} is {2}

-gb.cloneUrl = clone {0}

-gb.visitSite = visit {0} website
\ No newline at end of file
+gb.externalPermissions = {0} access permissions are externally maintained

+gb.viewAccess = You do not have Gitblit read or write access
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
index 6e03032..2170d0b 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -41,6 +41,7 @@
 import org.wicketstuff.googlecharts.IChartData;

 

 import com.gitblit.Constants;

+import com.gitblit.Constants.AccessPermission;

 import com.gitblit.Constants.FederationPullStatus;

 import com.gitblit.GitBlit;

 import com.gitblit.Keys;

@@ -107,6 +108,29 @@
 			setCssClass(container, css);

 		}

 	}

+	

+	public static void setPermissionClass(Component container, AccessPermission permission) {

+		if (permission == null) {

+			setCssClass(container, "badge");

+			return;

+		}

+		switch (permission) {

+		case REWIND:

+		case DELETE:

+		case CREATE:

+			setCssClass(container, "badge badge-success");

+			break;

+		case PUSH:

+			setCssClass(container, "badge badge-info");

+			break;

+		case CLONE:

+			setCssClass(container, "badge badge-inverse");

+			break;

+		default:

+			setCssClass(container, "badge");

+			break;

+		}	

+	}

 

 	public static void setAlternatingBackground(Component c, int i) {

 		String clazz = i % 2 == 0 ? "light" : "dark";

diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.java b/src/main/java/com/gitblit/wicket/pages/BasePage.java
index 19fa749..ca3ea90 100644
--- a/src/main/java/com/gitblit/wicket/pages/BasePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/BasePage.java
@@ -32,11 +32,9 @@
 import javax.servlet.http.HttpServletRequest;

 

 import org.apache.wicket.Application;

-import org.apache.wicket.Component;

 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.basic.Label;

@@ -45,7 +43,6 @@
 import org.apache.wicket.markup.html.panel.FeedbackPanel;

 import org.apache.wicket.markup.html.panel.Fragment;

 import org.apache.wicket.protocol.http.RequestUtils;

-import org.apache.wicket.protocol.http.WebRequest;

 import org.apache.wicket.protocol.http.servlet.ServletWebRequest;

 import org.slf4j.Logger;

 import org.slf4j.LoggerFactory;

@@ -58,14 +55,12 @@
 import com.gitblit.GitBlit;

 import com.gitblit.Keys;

 import com.gitblit.models.ProjectModel;

-import com.gitblit.models.RepositoryModel;

 import com.gitblit.models.TeamModel;

 import com.gitblit.models.UserModel;

 import com.gitblit.utils.StringUtils;

 import com.gitblit.utils.TimeUtils;

 import com.gitblit.wicket.GitBlitWebSession;

 import com.gitblit.wicket.WicketUtils;

-import com.gitblit.wicket.panels.DetailedRepositoryUrlPanel;

 import com.gitblit.wicket.panels.LinkPanel;

 

 public abstract class BasePage extends SessionPage {

@@ -258,60 +253,6 @@
 		return req.getServerName();

 	}

 	

-	public static String getRepositoryUrl(RepositoryModel repository) {

-		StringBuilder sb = new StringBuilder();

-		sb.append(WicketUtils.getGitblitURL(RequestCycle.get().getRequest()));

-		sb.append(Constants.GIT_PATH);

-		sb.append(repository.name);

-		

-		// inject username into repository url if authentication is required

-		if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE)

-				&& GitBlitWebSession.get().isLoggedIn()) {

-			String username = GitBlitWebSession.get().getUsername();

-			sb.insert(sb.indexOf("://") + 3, username + "@");

-		}

-		return sb.toString();

-	}

-	

-	protected Component createGitDaemonUrlPanel(String wicketId, UserModel user, RepositoryModel repository) {

-		int gitDaemonPort = GitBlit.getInteger(Keys.git.daemonPort, 0);

-		if (gitDaemonPort > 0 && user.canClone(repository)) {

-			String servername = ((WebRequest) getRequest()).getHttpServletRequest().getServerName();

-			String gitDaemonUrl;

-			if (gitDaemonPort == 9418) {

-				// standard port

-				gitDaemonUrl = MessageFormat.format("git://{0}/{1}", servername, repository.name);

-			} else {

-				// non-standard port

-				gitDaemonUrl = MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, gitDaemonPort, repository.name);

-			}

-			

-			AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;;

-			if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {

-				if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {

-					// can not authenticate clone via anonymous git protocol

-					gitDaemonPermission = AccessPermission.NONE;

-				} else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {

-					// can not authenticate push via anonymous git protocol

-					gitDaemonPermission = AccessPermission.CLONE;

-				} else {

-					// normal user permission

-				}

-			}

-			

-			if (AccessPermission.NONE.equals(gitDaemonPermission)) {

-				// repository prohibits all anonymous access

-				return new Label(wicketId).setVisible(false);

-			} else {

-				// repository allows some form of anonymous access

-				return new DetailedRepositoryUrlPanel(wicketId, getLocalizer(), this, repository.name, gitDaemonUrl, gitDaemonPermission);

-			}

-		} else {

-			// git daemon is not running

-			return new Label(wicketId).setVisible(false);

-		}

-	}

-

 	protected List<ProjectModel> getProjectModels() {

 		final UserModel user = GitBlitWebSession.get().getUser();

 		List<ProjectModel> projects = GitBlit.self().getProjectModels(user, true);

diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
index f7e8848..f704203 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
@@ -53,7 +53,7 @@
 			user = UserModel.ANONYMOUS;

 		}

 		

-		RepositoryUrlPanel urlPanel = new RepositoryUrlPanel("pushurl", false, user, repository, getLocalizer(), this);

+		RepositoryUrlPanel urlPanel = new RepositoryUrlPanel("pushurl", false, user, repository);

 		String primaryUrl = urlPanel.getPrimaryUrl();

 		

 		add(new Label("repository", repositoryName));

diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html
index 1527436..78c8f4d 100644
--- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html
@@ -19,10 +19,8 @@
 				<tr><th><wicket:message key="gb.owners">[owner]</wicket:message></th><td><span wicket:id="repositoryOwners"><span wicket:id="owner"></span><span wicket:id="comma"></span></span></td></tr>

 				<tr><th><wicket:message key="gb.lastChange">[last change]</wicket:message></th><td><span wicket:id="repositoryLastChange">[repository last change]</span></td></tr>

 				<tr><th><wicket:message key="gb.stats">[stats]</wicket:message></th><td><span wicket:id="branchStats">[branch stats]</span> <span class="link"><a wicket:id="metrics"><wicket:message key="gb.metrics">[metrics]</wicket:message></a></span></td></tr>

-				<tr><th style="vertical-align:top;padding-top:4px;"><wicket:message key="gb.repositoryUrl">[URL]</wicket:message>&nbsp;<img style="vertical-align: top;padding-left:3px;" wicket:id="accessRestrictionIcon" /></th>

-				    <td style="padding-top:4px;">

-				    	<div wicket:id="repositoryUrlPanel">[repository url panel]</div>

-				    </td>

+				<tr><th style="vertical-align:top;padding-top:4px;"><wicket:message key="gb.repositoryUrl">[URL]</wicket:message></th>

+				    <td><div wicket:id="repositoryUrlPanel">[repository url panel]</div></td>

 				</tr>

 			</table>

 		</div>

diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
index 54445f8..321dff3 100644
--- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
@@ -42,7 +42,6 @@
 import org.wicketstuff.googlecharts.MarkerType;

 import org.wicketstuff.googlecharts.ShapeMarker;

 

-import com.gitblit.Constants.AccessRestrictionType;

 import com.gitblit.GitBlit;

 import com.gitblit.Keys;

 import com.gitblit.models.Metric;

@@ -124,32 +123,7 @@
 		add(new BookmarkablePageLink<Void>("metrics", MetricsPage.class,

 				WicketUtils.newRepositoryParameter(repositoryName)));

 

-		if (GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {			

-			AccessRestrictionType accessRestriction = getRepositoryModel().accessRestriction;

-			switch (accessRestriction) {

-			case NONE:

-				add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

-				break;

-			case PUSH:

-				add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",

-						getAccessRestrictions().get(accessRestriction)));

-				break;

-			case CLONE:

-				add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",

-						getAccessRestrictions().get(accessRestriction)));

-				break;

-			case VIEW:

-				add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",

-						getAccessRestrictions().get(accessRestriction)));

-				break;

-			default:

-				add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

-			}

-		} else {

-			add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

-		}

-		

-		add(new RepositoryUrlPanel("repositoryUrlPanel", false, user, model, getLocalizer(), this));

+		add(new RepositoryUrlPanel("repositoryUrlPanel", false, user, model));

 				

 		add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0, getRepositoryModel().showRemoteBranches));

 		add(new TagsPanel("tagsPanel", repositoryName, r, numberRefs).hideIfEmpty());

diff --git a/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.html
deleted file mode 100644
index e671435..0000000
--- a/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!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>

-	<span wicket:id="urlPanel"></span>

-    

-    <!--  Repository url panel -->

-    <wicket:fragment wicket:id="repositoryUrlPanel">

-    	<span class="repositoryUrlContainer">

-			<span wicket:id="repositoryProtocol" class="repositoryUrlEndCap">[protocol]</span>

-			<span class="repositoryUrl">

-				<span wicket:id="repositoryUrl">[repository url]</span>

-				<span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>

-			</span>

-			<span class="hidden-phone hidden-tablet repositoryUrlEndCap" wicket:id="repositoryUrlPermission">[repository url permission]</span>

-		</span>

-	</wicket:fragment>

-	

-    <!-- Plain JavaScript manual copy & paste -->

-    <wicket:fragment wicket:id="jsPanel">

-    	<span style="vertical-align:baseline;">

-    		<img wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard"></img>

-    	</span>

-    </wicket:fragment>

-    

-    <!-- flash-based button-press copy & paste -->

-    <wicket:fragment wicket:id="clippyPanel">

-   		<object wicket:message="title:gb.copyToClipboard" style="vertical-align:middle;"

-   			wicket:id="clippy"

-   			width="14" 

-   			height="14"

-   			bgcolor="#ffffff" 

-       		quality="high"

-       		wmode="transparent"

-       		scale="noscale"

-       		allowScriptAccess="always"></object>

-	</wicket:fragment>

-</wicket:panel>

-</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.java
deleted file mode 100644
index 92c51e4..0000000
--- a/src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*

- * Copyright 2013 gitblit.com.

- *

- * Licensed under the Apache License, Version 2.0 (the "License");

- * you may not use this file except in compliance with the License.

- * You may obtain a copy of the License at

- *

- *     http://www.apache.org/licenses/LICENSE-2.0

- *

- * Unless required by applicable law or agreed to in writing, software

- * distributed under the License is distributed on an "AS IS" BASIS,

- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

- * See the License for the specific language governing permissions and

- * limitations under the License.

- */

-package com.gitblit.wicket.panels;

-

-import java.text.MessageFormat;

-

-import org.apache.wicket.Component;

-import org.apache.wicket.Localizer;

-import org.apache.wicket.markup.html.basic.Label;

-import org.apache.wicket.markup.html.image.ContextImage;

-import org.apache.wicket.markup.html.panel.Fragment;

-

-import com.gitblit.Constants.AccessPermission;

-import com.gitblit.GitBlit;

-import com.gitblit.Keys;

-import com.gitblit.utils.StringUtils;

-import com.gitblit.wicket.WicketUtils;

-

-public class DetailedRepositoryUrlPanel extends BasePanel {

-

-	private static final long serialVersionUID = 1L;

-	public DetailedRepositoryUrlPanel(String wicketId, Localizer localizer, Component parent, String repository, String url) {

-		this(wicketId, localizer, parent, repository, url, null);

-	}

-	

-	public DetailedRepositoryUrlPanel(String wicketId, Localizer localizer, Component parent, String repository, String url, AccessPermission ap) {

-		super(wicketId);

-		

-		String protocol = url.substring(0, url.indexOf(':'));

-		String note;

-		String permission;

-		

-		if (ap == null) {

-			note = MessageFormat.format(localizer.getString("gb.externalPermissions", parent), protocol, repository);

-			permission = "";

-		} else {

-			note = null;

-			permission = ap.toString();

-			String key;

-			switch (ap) {

-				case OWNER:

-				case REWIND:

-					key = "gb.rewindPermission";

-					break;

-				case DELETE:

-					key = "gb.deletePermission";

-					break;

-				case CREATE:

-					key = "gb.createPermission";

-					break;

-				case PUSH:

-					key = "gb.pushPermission";

-					break;

-				case CLONE:

-					key = "gb.clonePermission";

-					break;

-				default:

-					key = null;

-					note = localizer.getString("gb.viewAccess", parent);

-					break;

-			}

-			

-			if (note == null) {

-				String pattern = localizer.getString(key, parent);

-				String description = MessageFormat.format(pattern, permission);

-				String permissionPattern = localizer.getString("gb.yourProtocolPermissionIs", parent);

-				note = MessageFormat.format(permissionPattern, protocol.toUpperCase(), repository, description);

-			}

-		}

-		

-		if (!StringUtils.isEmpty(url) && ((ap == null) || ap.atLeast(AccessPermission.CLONE))) {

-			// valid repository url

-			Fragment fragment = new Fragment("urlPanel", "repositoryUrlPanel", this);

-			add(fragment);

-			Label protocolLabel = new Label("repositoryProtocol", protocol + "://");

-			WicketUtils.setHtmlTooltip(protocolLabel, note);

-			fragment.add(protocolLabel);

-			fragment.add(new Label("repositoryUrl", url.substring(url.indexOf("://") + 3)));

-			Label permissionLabel = new Label("repositoryUrlPermission", permission);

-			WicketUtils.setHtmlTooltip(permissionLabel, note);

-			fragment.add(permissionLabel);

-

-			if (StringUtils.isEmpty(url)) {

-				fragment.add(new Label("copyFunction").setVisible(false));

-			} else if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {

-				// clippy: flash-based copy & paste

-				Fragment copyFragment = new Fragment("copyFunction", "clippyPanel", this);

-				String baseUrl = WicketUtils.getGitblitURL(getRequest());

-				ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");

-				clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(url));

-				copyFragment.add(clippy);

-				fragment.add(copyFragment);

-			} else {

-				// javascript: manual copy & paste with modal browser prompt dialog

-				Fragment copyFragment = new Fragment("copyFunction", "jsPanel", this);

-				ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");

-				img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url));

-				copyFragment.add(img);

-				fragment.add(copyFragment);

-			}

-		} else {

-			// no Git url, there may be a message

-			add(new Label("urlPanel", MessageFormat.format("<i>{0}</i>", note)).setEscapeModelStrings(false).setVisible(!StringUtils.isEmpty(note)));

-		}

-	}

-}

diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
index e67e641..e9196cd 100644
--- a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -59,7 +59,6 @@
 				<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>

diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
index 7cce74f..e7fe017 100644
--- a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -103,25 +103,6 @@
 		} 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"));

-		}

 

 		if (ArrayUtils.isEmpty(entry.owners)) {

 			add(new Label("repositoryOwner").setVisible(false));

@@ -212,6 +193,6 @@
 

 		add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0)));

 

-		add(new RepositoryUrlPanel("repositoryPrimaryUrl", true, user, entry, localizer, parent));

+		add(new RepositoryUrlPanel("repositoryPrimaryUrl", true, user, entry));

 	}

 }

diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
index 675ebb5..b22aa71 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
@@ -5,33 +5,66 @@
       lang="en"> 

 

 <wicket:panel>

-	<div wicket:id="repositoryPrimaryUrl">[repository primary url]</div>

-	<div class="btn-toolbar" style="margin-bottom: 0px;">

-		<div class="btn-group" wicket:id="urlMenus">

-   			<a class="btn btn-mini btn-action" data-toggle="dropdown" href="#">

-   				<i class="icon-download icon-black"></i>

-    			<span wicket:id="productName"></span>

-    			<span class="caret"></span>

-   			</a>

-   			<ul class="dropdown-menu">

-   				<li><div style="padding-left: 15px; font-style: italic;" wicket:id="productAttribution"></div></li>

-   				<li class="divider"></li>

-   				

-   				<li wicket:id="repoLinks">

-   					<span wicket:id="repoLink"></span>

-   				</li>

-   				   				

-   				<li style="border-top: 1px solid #eee; margin-top:5px;padding-top:5px;"><span wicket:id="productLink"></span></li>

-   			</ul>

-   		</div>

-	</div>

-	

-	<wicket:fragment wicket:id="commandFragment">

-		<span wicket:id="content"></span><span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>

-	</wicket:fragment>

 

-	<wicket:fragment wicket:id="linkFragment">

-		<span wicket:id="content"></span>

+	<div wicket:id="repositoryUrlPanel"></div>

+	<div wicket:id="applicationMenusPanel"></div>

+

+	

+	<wicket:fragment wicket:id="repositoryUrlFragment">

+		<div class="btn-toolbar" style="margin: 0px;">

+			<div class="btn-group repositoryUrlContainer">

+				<img style="vertical-align: middle;padding: 0px 0px 1px 3px;" wicket:id="accessRestrictionIcon"></img>

+				<span wicket:id="menu"></span>

+   				<span class="repositoryUrl">

+   					<span wicket:id="primaryUrl">[repository primary url]</span>

+   					<span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>

+   				</span>

+   				<span class="hidden-phone hidden-tablet repositoryUrlRightCap" wicket:id="primaryUrlPermission">[repository primary url permission]</span>

+   			</div>

+		</div>

+	</wicket:fragment>

+	

+	<wicket:fragment wicket:id="applicationMenusFragment">

+		<div class="btn-toolbar" style="margin: 4px 0px 0px 0px;">

+			<div class="btn-group" wicket:id="appMenus">

+   				<a class="btn btn-mini btn-inverse" data-toggle="dropdown" href="#">   				

+	    			<span wicket:id="applicationName"></span>

+    				<span class="caret"></span>

+   				</a>

+   				<ul class="dropdown-menu applicationMenu">

+   					<li>

+   						<div class="applicationHeaderMenuItem">

+   							<div style="float:right">

+   								<img style="padding-right: 5px;vertical-align: middle;" wicket:id="applicationIcon"></img>

+   							</div>

+   							<span class="applicationTitle" wicket:id="applicationTitle"></span>

+   						</div>

+   					</li>

+	   				<li><div class="applicationHeaderMenuItem"><span wicket:id="applicationDescription"></span></div></li>

+   					<li><div class="applicationLegalMenuItem"><span wicket:id="applicationLegal"></span></div></li>

+   					

+   					<li class="divider" style="margin: 5px 1px 0px 1px;clear:both;" ></li>

+   				

+   					<li class="action" wicket:id="actionItems">

+   						<span wicket:id="actionItem"></span>

+   					</li>

+   				</ul>

+   			</div>

+		</div>

+	</wicket:fragment>

+	

+	<wicket:fragment wicket:id="urlProtocolMenuFragment">

+		<a class="" data-toggle="dropdown" href="#">   				

+    		<span class="repositoryUrlLeftCap" wicket:id="menuText">URLs</span>

+	   		<span class="caret" style="vertical-align: middle;"></span>

+   		</a>

+   		<ul class="dropdown-menu urlMenu">

+   			<li class="url" wicket:id="repoUrls"><span wicket:id="repoUrl"></span></li>

+	   	</ul>

+	</wicket:fragment>

+	

+	<wicket:fragment wicket:id="actionFragment">

+		<span wicket:id="permission" style="margin: 0px 10px 0px 5px;"></span><span wicket:id="content"></span><span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>

 	</wicket:fragment>

 

     <!-- Plain JavaScript manual copy & paste -->

diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
index 9640ab0..00c7cf7 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -15,13 +15,15 @@
  */

 package com.gitblit.wicket.panels;

 

-import java.io.Serializable;

 import java.text.MessageFormat;

 import java.util.ArrayList;

+import java.util.HashMap;

 import java.util.List;

+import java.util.Map;

+

+import javax.servlet.http.HttpServletRequest;

 

 import org.apache.wicket.Component;

-import org.apache.wicket.Localizer;

 import org.apache.wicket.RequestCycle;

 import org.apache.wicket.markup.html.basic.Label;

 import org.apache.wicket.markup.html.image.ContextImage;

@@ -32,7 +34,6 @@
 import org.apache.wicket.protocol.http.WebRequest;

 import org.apache.wicket.protocol.http.request.WebClientInfo;

 

-import com.gitblit.Constants;

 import com.gitblit.Constants.AccessPermission;

 import com.gitblit.Constants.AccessRestrictionType;

 import com.gitblit.GitBlit;

@@ -40,6 +41,7 @@
 import com.gitblit.SparkleShareInviteServlet;

 import com.gitblit.models.GitClientApplication;

 import com.gitblit.models.RepositoryModel;

+import com.gitblit.models.RepositoryUrl;

 import com.gitblit.models.UserModel;

 import com.gitblit.utils.StringUtils;

 import com.gitblit.wicket.GitBlitWebSession;

@@ -55,260 +57,314 @@
 public class RepositoryUrlPanel extends BasePanel {

 

 	private static final long serialVersionUID = 1L;

+

+	private final String externalPermission = "?";

+

+	private boolean onlyUrls;

+	private UserModel user; 

+	private RepositoryModel repository;

+	private RepositoryUrl primaryUrl;

+	private Map<String, String> urlPermissionsMap;

+	private Map<AccessRestrictionType, String> accessRestrictionsMap;

 	

-	private final RepoUrl primaryUrl;

-

-	public RepositoryUrlPanel(String wicketId, boolean onlyPrimary, UserModel user, 

-			final RepositoryModel repository, Localizer localizer, Component owner) {

+	public RepositoryUrlPanel(String wicketId, boolean onlyUrls, UserModel user, RepositoryModel repository) {

 		super(wicketId);

-		if (user == null) {

-			user = UserModel.ANONYMOUS;

-		}

-		List<RepoUrl> repositoryUrls = new ArrayList<RepoUrl>();

+		this.onlyUrls = onlyUrls;

+		this.user = user == null ? UserModel.ANONYMOUS : user;

+		this.repository = repository;

+		this.urlPermissionsMap = new HashMap<String, String>();

+	}

+	

+	@Override

+	protected void onInitialize() {

+		super.onInitialize();

 

-		// http/https url

-		if (GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {

-			AccessPermission permission = user.getRepositoryPermission(repository).permission;

-			if (permission.exceeds(AccessPermission.NONE)) {

-				repositoryUrls.add(new RepoUrl(getRepositoryUrl(repository), permission));

-			}

-		}

-		

-		// git daemon url

-		String gitDaemonUrl = getGitDaemonUrl(user, repository);

-		if (!StringUtils.isEmpty(gitDaemonUrl)) {

-			AccessPermission permission = getGitDaemonAccessPermission(user, repository);

-			if (permission.exceeds(AccessPermission.NONE)) {

-				repositoryUrls.add(new RepoUrl(gitDaemonUrl, permission));

-			}

-		}

-		

-		// add all other urls

-		for (String url : GitBlit.self().getOtherCloneUrls(repository.name, UserModel.ANONYMOUS.equals(user) ? "" : user.username)) {

-			repositoryUrls.add(new RepoUrl(url, null));

-		}

-		

+		HttpServletRequest req = ((WebRequest) getRequest()).getHttpServletRequest();

+

+		List<RepositoryUrl> repositoryUrls = GitBlit.self().getRepositoryUrls(req, user, repository);

 		// grab primary url from the top of the list

 		primaryUrl = repositoryUrls.size() == 0 ? null : repositoryUrls.get(0);

 

-		add(new DetailedRepositoryUrlPanel("repositoryPrimaryUrl", localizer, owner, 

-				repository.name, primaryUrl == null ? "" : primaryUrl.url,

-				primaryUrl == null ? null : primaryUrl.permission));

-		

-		if (onlyPrimary) {

-			// only displaying the primary url

-			add(new Label("urlMenus").setVisible(false));

+		boolean canClone = ((primaryUrl.permission == null) || primaryUrl.permission.atLeast(AccessPermission.CLONE));

+

+		if (repositoryUrls.size() == 0 || !canClone) {

+			// no urls, nothing to show.

+			add(new Label("repositoryUrlPanel").setVisible(false));

+			add(new Label("applicationMenusPanel").setVisible(false));

 			return;

 		}

 		

-		final String clonePattern = localizer.getString("gb.cloneUrl", owner);

-		final String visitSitePattern = localizer.getString("gb.visitSite", owner);

-		

-		GitClientApplication URLS = new GitClientApplication();

-		URLS.name = "URLs";

-		URLS.command = "{0}";

-		URLS.attribution = "Repository URLs";

-		URLS.isApplication = false;

-		URLS.isActive = true;

-		

-		GitClientApplication GIT = new GitClientApplication();

-		GIT.name = "Git";

-		GIT.command = "git clone {0}";

-		GIT.productUrl = "http://git-scm.org";

-		GIT.attribution = "Git Syntax";

-		GIT.isApplication = false;

-		GIT.isActive = true;

-		

-		final List<GitClientApplication> clientApps = new ArrayList<GitClientApplication>();

-		clientApps.add(URLS);

-		clientApps.add(GIT);

-		

-		final String userAgent = ((WebClientInfo) GitBlitWebSession.get().getClientInfo()).getUserAgent();

+		// display primary url

+		add(createPrimaryUrlPanel("repositoryUrlPanel", repository, repositoryUrls));

+

 		boolean allowAppLinks = GitBlit.getBoolean(Keys.web.allowAppCloneLinks, true);

-		if (user.canClone(repository)) {

-			for (GitClientApplication app : GitBlit.self().getClientApplications()) {

-				if (app.isActive && app.allowsPlatform(userAgent) && (!app.isApplication || (app.isApplication && allowAppLinks))) {

-					clientApps.add(app);

-				}

-			}

-

-			// sparkleshare invite url

-			String sparkleshareUrl = getSparkleShareInviteUrl(user, repository);

-			if (!StringUtils.isEmpty(sparkleshareUrl) && allowAppLinks) {

-				GitClientApplication link = new GitClientApplication();

-				link.name = "SparkleShare";

-				link.cloneUrl = sparkleshareUrl;

-				link.attribution = "SparkleShare\u2122";

-				link.platforms = new String [] { "windows", "macintosh", "linux" };

-				link.productUrl = "http://sparkleshare.org";

-				link.isApplication = true;

-				link.isActive = true;

-				clientApps.add(link);

-			}

+		if (onlyUrls || !canClone || !allowAppLinks) {

+			// only display the url(s)

+			add(new Label("applicationMenusPanel").setVisible(false));

+			return;

 		}

-		

-		final ListDataProvider<RepoUrl> repoUrls = new ListDataProvider<RepoUrl>(repositoryUrls);

-

-		// app clone links

-		ListDataProvider<GitClientApplication> appLinks = new ListDataProvider<GitClientApplication>(clientApps);

-		DataView<GitClientApplication> urlMenus = new DataView<GitClientApplication>("urlMenus", appLinks) {

-			private static final long serialVersionUID = 1L;

-			

-			public void populateItem(final Item<GitClientApplication> item) {

-				final GitClientApplication cloneLink = item.getModelObject();

-				item.add(new Label("productName", cloneLink.name));

-				

-				// a nested repeater for all repo links

-				DataView<RepoUrl> repoLinks = new DataView<RepoUrl>("repoLinks", repoUrls) {

-					private static final long serialVersionUID = 1L;

-

-					public void populateItem(final Item<RepoUrl> repoLinkItem) {

-						RepoUrl repoUrl = repoLinkItem.getModelObject();

-						if (!StringUtils.isEmpty(cloneLink.cloneUrl)) {

-							// custom registered url

-							Fragment fragment = new Fragment("repoLink", "linkFragment", this);

-							String name;

-							if (repoUrl.permission != null) {

-								name = MessageFormat.format("{0} ({1})", repoUrl.url, repoUrl.permission);

-							} else {

-								name = repoUrl.url;

-							}

-							String url = MessageFormat.format(cloneLink.cloneUrl, repoUrl);

-							fragment.add(new LinkPanel("content", null, MessageFormat.format(clonePattern, name), url));

-							repoLinkItem.add(fragment);

-							String tooltip = getProtocolPermissionDescription(repository, repoUrl);

-							WicketUtils.setHtmlTooltip(fragment, tooltip);

-						} else if (!StringUtils.isEmpty(cloneLink.command)) {

-							// command-line

-							Fragment fragment = new Fragment("repoLink", "commandFragment", this);

-							WicketUtils.setCssClass(fragment, "repositoryUrlMenuItem");

-							String command = MessageFormat.format(cloneLink.command, repoUrl);

-							fragment.add(new Label("content", command));

-							repoLinkItem.add(fragment);

-							String tooltip = getProtocolPermissionDescription(repository, repoUrl);

-							WicketUtils.setHtmlTooltip(fragment, tooltip);

-							

-							// copy function for command

-							if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {

-								// clippy: flash-based copy & paste

-								Fragment copyFragment = new Fragment("copyFunction", "clippyPanel", this);

-								String baseUrl = WicketUtils.getGitblitURL(getRequest());

-								ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");

-								clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(command));

-								copyFragment.add(clippy);

-								fragment.add(copyFragment);

-							} else {

-								// javascript: manual copy & paste with modal browser prompt dialog

-								Fragment copyFragment = new Fragment("copyFunction", "jsPanel", this);

-								ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");

-								img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", command));

-								copyFragment.add(img);

-								fragment.add(copyFragment);

-							}

-						}

-					}};

-				item.add(repoLinks);

-				

-				item.add(new Label("productAttribution", cloneLink.attribution));

-				if (!StringUtils.isEmpty(cloneLink.productUrl)) {

-					LinkPanel productlinkPanel = new LinkPanel("productLink", null,

-							MessageFormat.format(visitSitePattern, cloneLink.name), cloneLink.productUrl, true);

-					item.add(productlinkPanel);

-				} else {

-					item.add(new Label("productLink").setVisible(false));

-				}

-			}

-		};

-		add(urlMenus);

+		// create the git client application menus

+		add(createApplicationMenus("applicationMenusPanel", user, repository, repositoryUrls));

 	}

-	

+

 	public String getPrimaryUrl() {

 		return primaryUrl == null ? "" : primaryUrl.url;

 	}

-	

-	protected String getRepositoryUrl(RepositoryModel repository) {

-		StringBuilder sb = new StringBuilder();

-		sb.append(WicketUtils.getGitblitURL(RequestCycle.get().getRequest()));

-		sb.append(Constants.GIT_PATH);

-		sb.append(repository.name);

+

+	protected Fragment createPrimaryUrlPanel(String wicketId, final RepositoryModel repository, List<RepositoryUrl> repositoryUrls) {

+

+		Fragment urlPanel = new Fragment(wicketId, "repositoryUrlFragment", this);

+		urlPanel.setRenderBodyOnly(true);

 		

-		// inject username into repository url if authentication is required

-		if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE)

-				&& GitBlitWebSession.get().isLoggedIn()) {

-			String username = GitBlitWebSession.get().getUsername();

-			sb.insert(sb.indexOf("://") + 3, username + "@");

+		if (repositoryUrls.size() == 1) {

+			//

+			// Single repository url, no dropdown menu

+			//

+			urlPanel.add(new Label("menu").setVisible(false));

+		} else {

+			//

+			// Multiple repository urls, show url drop down menu

+			//

+			ListDataProvider<RepositoryUrl> urlsDp = new ListDataProvider<RepositoryUrl>(repositoryUrls);

+			DataView<RepositoryUrl> repoUrlMenuItems = new DataView<RepositoryUrl>("repoUrls", urlsDp) {

+				private static final long serialVersionUID = 1L;

+

+				public void populateItem(final Item<RepositoryUrl> item) {

+					RepositoryUrl repoUrl = item.getModelObject();

+					// repository url

+					Fragment fragment = new Fragment("repoUrl", "actionFragment", this);					

+					Component content = new Label("content", repoUrl.url).setRenderBodyOnly(true);

+					WicketUtils.setCssClass(content, "commandMenuItem");

+					fragment.add(content);

+					item.add(fragment);

+					

+					Label permissionLabel = new Label("permission", repoUrl.isExternal() ? externalPermission : repoUrl.permission.toString());

+					WicketUtils.setPermissionClass(permissionLabel, repoUrl.permission);

+					String tooltip = getProtocolPermissionDescription(repository, repoUrl);

+					WicketUtils.setHtmlTooltip(permissionLabel, tooltip);

+					fragment.add(permissionLabel);

+					fragment.add(createCopyFragment(repoUrl.url));

+				}

+			};

+

+			Fragment urlMenuFragment = new Fragment("menu", "urlProtocolMenuFragment", this);

+			urlMenuFragment.setRenderBodyOnly(true);

+			urlMenuFragment.add(new Label("menuText", getString("gb.url")));

+			urlMenuFragment.add(repoUrlMenuItems);

+			urlPanel.add(urlMenuFragment);

 		}

-		return sb.toString();

-	}

-	

-	protected String getGitDaemonUrl(UserModel user, RepositoryModel repository) {

-		int gitDaemonPort = GitBlit.getInteger(Keys.git.daemonPort, 0);

-		if (gitDaemonPort > 0 && user.canClone(repository)) {

-			String servername = ((WebRequest) getRequest()).getHttpServletRequest().getServerName();

-			String gitDaemonUrl;

-			if (gitDaemonPort == 9418) {

-				// standard port

-				gitDaemonUrl = MessageFormat.format("git://{0}/{1}", servername, repository.name);

-			} else {

-				// non-standard port

-				gitDaemonUrl = MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, gitDaemonPort, repository.name);

+

+		// access restriction icon and tooltip

+		if (isGitblitServingRepositories()) {

+			switch (repository.accessRestriction) {

+			case NONE:

+				urlPanel.add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

+				break;

+			case PUSH:

+				urlPanel.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",

+						getAccessRestrictions().get(repository.accessRestriction)));

+				break;

+			case CLONE:

+				urlPanel.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",

+						getAccessRestrictions().get(repository.accessRestriction)));

+				break;

+			case VIEW:

+				urlPanel.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",

+						getAccessRestrictions().get(repository.accessRestriction)));

+				break;

+			default:

+				urlPanel.add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

 			}

-			return gitDaemonUrl;

+		} else {

+			urlPanel.add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));

 		}

-		return null;

+		

+		urlPanel.add(new Label("primaryUrl", primaryUrl.url).setRenderBodyOnly(true));

+

+		Label permissionLabel = new Label("primaryUrlPermission", primaryUrl.isExternal() ? externalPermission : primaryUrl.permission.toString());		

+		String tooltip = getProtocolPermissionDescription(repository, primaryUrl);

+		WicketUtils.setHtmlTooltip(permissionLabel, tooltip);

+		urlPanel.add(permissionLabel);

+		urlPanel.add(createCopyFragment(primaryUrl.url));

+		

+		return urlPanel;

 	}

 	

-	protected AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {

-		int gitDaemonPort = GitBlit.getInteger(Keys.git.daemonPort, 0);

-		if (gitDaemonPort > 0 && user.canClone(repository)) {

-			AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;;

-			if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {

-				if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {

-					// can not authenticate clone via anonymous git protocol

-					gitDaemonPermission = AccessPermission.NONE;

-				} else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {

-					// can not authenticate push via anonymous git protocol

-					gitDaemonPermission = AccessPermission.CLONE;

-				} else {

-					// normal user permission

+	protected Fragment createApplicationMenus(String wicketId, UserModel user, final RepositoryModel repository, List<RepositoryUrl> repositoryUrls) {

+		final List<GitClientApplication> displayedApps = new ArrayList<GitClientApplication>();

+		final String userAgent = ((WebClientInfo) GitBlitWebSession.get().getClientInfo()).getUserAgent();

+		

+		if (user.canClone(repository)) {

+			for (GitClientApplication app : GitBlit.self().getClientApplications()) {

+				if (app.isActive && app.allowsPlatform(userAgent)) {

+					displayedApps.add(app);

 				}

 			}

-			return gitDaemonPermission;

-		}

-		return AccessPermission.NONE;

-	}

 

-	protected String getSparkleShareInviteUrl(UserModel user, RepositoryModel repository) {

+			GitClientApplication sparkleshare = getSparkleShareAppMenu(user, repository);

+			if (sparkleshare != null) {

+				displayedApps.add(sparkleshare);

+			}

+		}

+

+		final ListDataProvider<RepositoryUrl> urlsDp = new ListDataProvider<RepositoryUrl>(repositoryUrls);

+		ListDataProvider<GitClientApplication> displayedAppsDp = new ListDataProvider<GitClientApplication>(displayedApps);

+		DataView<GitClientApplication> appMenus = new DataView<GitClientApplication>("appMenus", displayedAppsDp) {

+			private static final long serialVersionUID = 1L;

+

+			public void populateItem(final Item<GitClientApplication> item) {

+				final GitClientApplication clientApp = item.getModelObject();

+

+				// menu button

+				item.add(new Label("applicationName", clientApp.name));

+				

+				// application icon

+				Component img;

+				if (StringUtils.isEmpty(clientApp.icon)) {

+					img = WicketUtils.newClearPixel("applicationIcon").setVisible(false);	

+				} else {

+					img = WicketUtils.newImage("applicationIcon", clientApp.icon);	

+				}				

+				item.add(img);

+				

+				// application menu title, may be a link

+				if (StringUtils.isEmpty(clientApp.productUrl)) {

+					item.add(new Label("applicationTitle", clientApp.toString()));

+				} else {

+					item.add(new LinkPanel("applicationTitle", null, clientApp.toString(), clientApp.productUrl, true));

+				}

+				

+				// brief application description

+				if (StringUtils.isEmpty(clientApp.description)) {

+					item.add(new Label("applicationDescription").setVisible(false));

+				} else {

+					item.add(new Label("applicationDescription", clientApp.description));

+				}

+				

+				// brief application legal info, copyright, license, etc

+				if (StringUtils.isEmpty(clientApp.legal)) {

+					item.add(new Label("applicationLegal").setVisible(false));

+				} else {

+					item.add(new Label("applicationLegal", clientApp.legal));

+				}

+				

+				// a nested repeater for all action items

+				DataView<RepositoryUrl> actionItems = new DataView<RepositoryUrl>("actionItems", urlsDp) {

+					private static final long serialVersionUID = 1L;

+

+					public void populateItem(final Item<RepositoryUrl> repoLinkItem) {

+						RepositoryUrl repoUrl = repoLinkItem.getModelObject();

+						

+						Fragment fragment = new Fragment("actionItem", "actionFragment", this);

+						fragment.add(createPermissionBadge("permission", repoUrl));

+

+						if (!StringUtils.isEmpty(clientApp.cloneUrl)) {

+							// custom registered url

+							String url = MessageFormat.format(clientApp.cloneUrl, repoUrl);

+							fragment.add(new LinkPanel("content", "applicationMenuItem", getString("gb.clone") + " " + repoUrl.url, url));

+							repoLinkItem.add(fragment);

+							fragment.add(new Label("copyFunction").setVisible(false));

+						} else if (!StringUtils.isEmpty(clientApp.command)) {

+							// command-line

+							String command = MessageFormat.format(clientApp.command, repoUrl);

+							Label content = new Label("content", command);

+							WicketUtils.setCssClass(content, "commandMenuItem");

+							fragment.add(content);

+							repoLinkItem.add(fragment);

+							

+							// copy function for command

+							fragment.add(createCopyFragment(command));

+						}

+					}};

+					item.add(actionItems);

+			}

+		};

+		

+		Fragment applicationMenus = new Fragment(wicketId, "applicationMenusFragment", this);

+		applicationMenus.add(appMenus);

+		return applicationMenus;

+	}

+	

+	protected GitClientApplication getSparkleShareAppMenu(UserModel user, RepositoryModel repository) {

+		String url = null;

 		if (repository.isBare && repository.isSparkleshared()) {

 			String username = null;

 			if (UserModel.ANONYMOUS != user) {

 				username = user.username;

 			}

-			if (GitBlit.getBoolean(Keys.git.enableGitServlet, true) || (GitBlit.getInteger(Keys.git.daemonPort, 0) > 0)) {

+			if (isGitblitServingRepositories()) {

 				// Gitblit as server

 				// ensure user can rewind

 				if (user.canRewindRef(repository)) {

 					String baseURL = WicketUtils.getGitblitURL(RequestCycle.get().getRequest());

-					return SparkleShareInviteServlet.asLink(baseURL, repository.name, username);

+					url = SparkleShareInviteServlet.asLink(baseURL, repository.name, username);

 				}

 			} else {

 				// Gitblit as viewer, assume RW+ permission

 				String baseURL = WicketUtils.getGitblitURL(RequestCycle.get().getRequest());

-				return SparkleShareInviteServlet.asLink(baseURL, repository.name, username);

+				url = SparkleShareInviteServlet.asLink(baseURL, repository.name, username);

 			}

 		}

+

+		// sparkleshare invite url

+		if (!StringUtils.isEmpty(url)) {

+			GitClientApplication app = new GitClientApplication();

+			app.name = "SparkleShare";

+			app.title = "SparkleShare\u2122";

+			app.description = "an open source collaboration and sharing tool";

+			app.legal = "released under the GPLv3 open source license";

+			app.cloneUrl = url;

+			app.platforms = new String [] { "windows", "macintosh", "linux" };

+			app.productUrl = "http://sparkleshare.org";

+			app.icon = "star_32x32.png";

+			app.isActive = true;

+			return app;

+		}

 		return null;

 	}

 	

-	protected String getProtocolPermissionDescription(RepositoryModel repository, RepoUrl repoUrl) {

-		String protocol = repoUrl.url.substring(0, repoUrl.url.indexOf("://"));

-		String note;

-		if (repoUrl.permission == null) {

-			note = MessageFormat.format(getString("gb.externalPermissions"), protocol, repository.name);			

+	protected boolean isGitblitServingRepositories() {

+		return GitBlit.getBoolean(Keys.git.enableGitServlet, true) || (GitBlit.getInteger(Keys.git.daemonPort, 0) > 0);

+	}

+	

+	protected Label createPermissionBadge(String wicketId, RepositoryUrl repoUrl) {

+		Label permissionLabel = new Label(wicketId, repoUrl.isExternal() ? externalPermission : repoUrl.permission.toString());

+		WicketUtils.setPermissionClass(permissionLabel, repoUrl.permission);

+		String tooltip = getProtocolPermissionDescription(repository, repoUrl);

+		WicketUtils.setHtmlTooltip(permissionLabel, tooltip);

+		return permissionLabel;

+	}

+	

+	protected Fragment createCopyFragment(String text) {

+		if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {

+			// clippy: flash-based copy & paste

+			Fragment copyFragment = new Fragment("copyFunction", "clippyPanel", this);

+			String baseUrl = WicketUtils.getGitblitURL(getRequest());

+			ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");

+			clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text));

+			copyFragment.add(clippy);

+			return copyFragment;

 		} else {

-			note = null;			

-			String key;

-			switch (repoUrl.permission) {

+			// javascript: manual copy & paste with modal browser prompt dialog

+			Fragment copyFragment = new Fragment("copyFunction", "jsPanel", this);

+			ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");

+			img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", text));

+			copyFragment.add(img);

+			return copyFragment;

+		}

+	}

+	

+	protected String getProtocolPermissionDescription(RepositoryModel repository,

+			RepositoryUrl repoUrl) {

+		if (!urlPermissionsMap.containsKey(repoUrl.url)) {

+			String note;

+			if (repoUrl.isExternal()) {

+				String protocol = repoUrl.url.substring(0, repoUrl.url.indexOf("://"));

+				note = MessageFormat.format(getString("gb.externalPermissions"), protocol);			

+			} else {

+				note = null;			

+				String key;

+				switch (repoUrl.permission) {

 				case OWNER:

 				case REWIND:

 					key = "gb.rewindPermission";

@@ -329,33 +385,39 @@
 					key = null;

 					note = getString("gb.viewAccess");

 					break;

+				}

+

+				if (note == null) {

+					String pattern = getString(key);

+					String description = MessageFormat.format(pattern, repoUrl.permission.toString());

+					note = description;

+				}

 			}

-			

-			if (note == null) {

-				String pattern = getString(key);

-				String description = MessageFormat.format(pattern, repoUrl.permission.toString());

-				String permissionPattern = getString("gb.yourProtocolPermissionIs");

-				note = MessageFormat.format(permissionPattern, protocol.toUpperCase(), repository, description);

-			}

+			urlPermissionsMap.put(repoUrl.url, note);

 		}

-		return note;

+		return urlPermissionsMap.get(repoUrl.url);

 	}

 	

-	private class RepoUrl implements Serializable {

-		

-		private static final long serialVersionUID = 1L;

-		

-		final String url;

-		final AccessPermission permission;

-		

-		RepoUrl(String url, AccessPermission permission) {

-			this.url = url;

-			this.permission = permission;

+	protected Map<AccessRestrictionType, String> getAccessRestrictions() {

+		if (accessRestrictionsMap == null) {

+			accessRestrictionsMap = new HashMap<AccessRestrictionType, String>();

+			for (AccessRestrictionType type : AccessRestrictionType.values()) {

+				switch (type) {

+				case NONE:

+					accessRestrictionsMap.put(type, getString("gb.notRestricted"));

+					break;

+				case PUSH:

+					accessRestrictionsMap.put(type, getString("gb.pushRestricted"));

+					break;

+				case CLONE:

+					accessRestrictionsMap.put(type, getString("gb.cloneRestricted"));

+					break;

+				case VIEW:

+					accessRestrictionsMap.put(type, getString("gb.viewRestricted"));

+					break;

+				}

+			}

 		}

-		

-		@Override

-		public String toString() {

-			return url;

-		}

+		return accessRestrictionsMap;

 	}

 }

diff --git a/src/main/resources/git-black_32x32.png b/src/main/resources/git-black_32x32.png
new file mode 100644
index 0000000..037f61a
--- /dev/null
+++ b/src/main/resources/git-black_32x32.png
Binary files differ
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index c22793d..cfb6cf7 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -117,6 +117,19 @@
 	color: #ffffff !important;

 }

 

+.btn:first-child {

+	border-radius: 4px;

+}

+

+.btn-appmenu {

+    /*background-color: rgb(73, 175, 205);

+    background-image: -moz-linear-gradient(center top , rgb(91, 192, 222), rgb(47, 150, 180));*/

+    background-color: rgb(73, 175, 205);

+    background-image: -moz-linear-gradient(center top , rgb(91, 192, 222), rgb(47, 150, 180));

+    background-repeat: repeat-x;

+    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);

+}

+

 .breadcrumb {

 	margin-top: 5px !important;

 	margin-bottom: 5px !important;

@@ -179,33 +192,106 @@
 	vertical-align: middle;

 }

 

-span.repositoryUrlContainer {

-	color: black;

-	background-color: whiteSmoke; 

-	padding: 4px;

-	border: 1px solid #ddd;

-	border-radius: 3px 

+div.repositoryUrlContainer {

+	padding: 2px;

+	background-color: #F5F5F5;

+    background-image: -moz-linear-gradient(center top , #FFFFFF, #E6E6E6);

+    background-repeat: repeat-x;

+    border-color: #E6E6E6 #E6E6E6 #B3B3B3;

+    border-image: none;

+    border-radius: 4px;

+    border-style: solid;

+    border-width: 1px;

+    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 1px 2px rgba(0, 0, 0, 0.05);

+    color: #333333;    

+    vertical-align: middle;

+    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);

 }

 

-span.repositoryUrlEndCap {	

-	padding: 4px;

+div.repositoryUrlContainer:hover {

+	background-color: #E6E6E6;

+    background-position: 0 -15px;

+    color: #333333;

+    text-decoration: none;

+    transition: background-position 0.1s linear 0s;

+}

+

+div.repositoryUrlContainer:hover .caret {

+    opacity: 1;

+}

+

+div.repositoryUrlContainer:hover a:hover {

+	text-decoration: none;

+}

+

+span.repositoryUrlLeftCap, span.repositoryUrlRightCap {	

+	text-align: center;

+	color: black;

+	padding: 3px;

+	font-size: 11px;

+}

+

+span.repositoryUrlRightCap {	

 	font-weight: bold;

-	font-size: 0.85em;

 	font-family:menlo,consolas,monospace;

 }

 

 span.repositoryUrl {

 	font-size: 1em;

-	padding: 4px;

-	color: blue;

+	padding: 2px 4px 3px 4px;	

 	background-color: #fff;

 	border-left: 1px solid #ddd;

 	border-right: 1px solid #ddd;

 }

 

-span.repositoryUrlMenuItem {

+ul.urlMenu {

+	min-width: 350px;

+}

+

+ul.urlMenu li.url {

+	background-color: white;

+	padding: 0px 5px;

 	line-height: 24px;

-	padding: 3px 15px;

+}

+

+ul.applicationMenu {

+	background-color: whiteSmoke;

+	min-width: 400px;

+}

+

+ul.applicationMenu li.action {

+	background-color: white;

+	padding: 0px 5px;

+	line-height: 24px;

+}

+

+span.applicationTitle, span.applicationTitle a {

+	display: inline;

+	font-weight: bold;

+	font-size:1.1em;

+	color: black !important;

+	padding: 0px;

+}

+

+div.applicationHeaderMenuItem {

+	padding-left: 10px;

+	color: black;

+}

+

+div.applicationLegalMenuItem {

+	padding-left: 10px;

+	color: #999;

+	font-size: 0.85em;

+}

+

+a.applicationMenuItem, span.commandMenuItem {

+	padding: 3px 10px;	

+	color: black;

+	display: inline;

+	padding: 0px;

+}

+

+span.commandMenuItem {

 	font-size: 0.85em;

 	font-family: menlo,consolas,monospace;

 }

diff --git a/src/main/resources/smartgithg_32x32.png b/src/main/resources/smartgithg_32x32.png
new file mode 100644
index 0000000..63d8692
--- /dev/null
+++ b/src/main/resources/smartgithg_32x32.png
Binary files differ
diff --git a/src/main/resources/sourcetree_32x32.png b/src/main/resources/sourcetree_32x32.png
new file mode 100644
index 0000000..a5dd96f
--- /dev/null
+++ b/src/main/resources/sourcetree_32x32.png
Binary files differ