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; @@ -462,19 +463,102 @@ } /** * 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; } /** @@ -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.externalPermissions = {0} access permissions 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 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; @@ -108,6 +109,29 @@ } } 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"; setCssClass(c, clazz); 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 { @@ -256,60 +251,6 @@ ServletWebRequest servletWebRequest = (ServletWebRequest) getRequest(); HttpServletRequest req = servletWebRequest.getHttpServletRequest(); 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() { 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> <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> <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"> <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> <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 style="border-top: 1px solid #eee; margin-top:5px;padding-top:5px;"><span wicket:id="productLink"></span></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: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> <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; @@ -56,255 +58,309 @@ private static final long serialVersionUID = 1L; private final RepoUrl primaryUrl; private final String externalPermission = "?"; public RepositoryUrlPanel(String wicketId, boolean onlyPrimary, UserModel user, final RepositoryModel repository, Localizer localizer, Component owner) { private boolean onlyUrls; private UserModel user; private RepositoryModel repository; private RepositoryUrl primaryUrl; private Map<String, String> urlPermissionsMap; private Map<AccessRestrictionType, String> accessRestrictionsMap; public RepositoryUrlPanel(String wicketId, boolean onlyUrls, UserModel user, RepositoryModel repository) { super(wicketId); if (user == null) { user = UserModel.ANONYMOUS; } List<RepoUrl> repositoryUrls = new ArrayList<RepoUrl>(); // 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)); } this.onlyUrls = onlyUrls; this.user = user == null ? UserModel.ANONYMOUS : user; this.repository = repository; this.urlPermissionsMap = new HashMap<String, String>(); } // 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)); } } @Override protected void onInitialize() { super.onInitialize(); // 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)); boolean canClone = ((primaryUrl.permission == null) || primaryUrl.permission.atLeast(AccessPermission.CLONE)); if (onlyPrimary) { // only displaying the primary url add(new Label("urlMenus").setVisible(false)); 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); // display primary url add(createPrimaryUrlPanel("repositoryUrlPanel", repository, repositoryUrls)); 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(); 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); if (onlyUrls || !canClone || !allowAppLinks) { // only display the url(s) add(new Label("applicationMenusPanel").setVisible(false)); return; } } // 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); } } 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) { // 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(); } Fragment urlPanel = new Fragment(wicketId, "repositoryUrlFragment", this); urlPanel.setRenderBodyOnly(true); 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); if (repositoryUrls.size() == 1) { // // Single repository url, no dropdown menu // urlPanel.add(new Label("menu").setVisible(false)); } else { // non-standard port gitDaemonUrl = MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, gitDaemonPort, repository.name); // // 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)); } return gitDaemonUrl; } return null; }; Fragment urlMenuFragment = new Fragment("menu", "urlProtocolMenuFragment", this); urlMenuFragment.setRenderBodyOnly(true); urlMenuFragment.add(new Label("menuText", getString("gb.url"))); urlMenuFragment.add(repoUrlMenuItems); urlPanel.add(urlMenuFragment); } 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; // 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)); } } else { // normal user permission } } return gitDaemonPermission; } return AccessPermission.NONE; urlPanel.add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false)); } protected String getSparkleShareInviteUrl(UserModel user, RepositoryModel repository) { 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 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); } } 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("://")); 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 { // 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.permission == null) { note = MessageFormat.format(getString("gb.externalPermissions"), protocol, repository.name); if (repoUrl.isExternal()) { String protocol = repoUrl.url.substring(0, repoUrl.url.indexOf("://")); note = MessageFormat.format(getString("gb.externalPermissions"), protocol); } else { note = null; String key; @@ -334,28 +390,34 @@ 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); note = description; } } return note; urlPermissionsMap.put(repoUrl.url, 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