James Moger
2013-05-09 828add77e71a567be98490403bb92d6f1afb9930
Implemented application menus for repository url panel
4 files added
15 files modified
2 files deleted
1266 ■■■■ changed files
src/main/distrib/data/clientapps.json 36 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/GitBlit.java 112 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/GitDaemon.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/GitClientApplication.java 13 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/RepositoryUrl.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties 7 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/WicketUtils.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/BasePage.java 59 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/SummaryPage.html 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/SummaryPage.java 28 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.html 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.java 119 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 21 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html 85 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java 536 ●●●●● patch | view | raw | blame | history
src/main/resources/git-black_32x32.png patch | view | raw | blame | history
src/main/resources/gitblit.css 112 ●●●● patch | view | raw | blame | history
src/main/resources/smartgithg_32x32.png patch | view | raw | blame | history
src/main/resources/sourcetree_32x32.png patch | view | raw | blame | history
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
    }
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());
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() {
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;
    }
}
src/main/java/com/gitblit/models/RepositoryUrl.java
New file
@@ -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;
    }
}
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
gb.externalPermissions = {0} access permissions are externally maintained
gb.viewAccess = You do not have Gitblit read or write access
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";
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);
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));
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>
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());
src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.html
File was deleted
src/main/java/com/gitblit/wicket/panels/DetailedRepositoryUrlPanel.java
File was deleted
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>
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));
    }
}
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 -->
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;
    }
}
src/main/resources/git-black_32x32.png
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;
}
src/main/resources/smartgithg_32x32.png
src/main/resources/sourcetree_32x32.png