.gitignore
@@ -26,3 +26,4 @@ /users.conf *.directory /.gradle /projects.conf distrib/gitblit.properties
@@ -290,6 +290,11 @@ # SINCE 0.5.0 web.allowCookieAuthentication = true # Config file for storing project metadata # # SINCE 1.2.0 web.projectsFile = projects.conf # Either the full path to a user config file (users.conf) # OR the full path to a simple user properties file (users.properties) # OR a fully qualified class name that implements the IUserService interface. docs/01_setup.mkd
@@ -9,6 +9,7 @@ - <context-parameter> *git.repositoryFolder* (set the full path to your repositories folder) - <context-parameter> *groovy.scriptsFolder* (set the full path to your Groovy hook scripts folder) - <context-parameter> *groovy.grapeFolder* (set the full path to your Groovy Grape artifact cache) - <context-parameter> *web.projectsFile* (set the full path to your projects metadata file) - <context-parameter> *realm.userService* (set the full path to `users.conf`) - <context-parameter> *git.packedGitLimit* (set larger than the size of your largest repository) - <context-parameter> *git.streamFileThreshold* (set larger than the size of your largest committed file) docs/05_roadmap.mkd
@@ -30,7 +30,5 @@ * Gitblit: diff should highlight inserted/removed fragment compared to original line * Gitblit: implement branch permission controls as Groovy pre-receive script. *Maintain permissions text file similar to a gitolite configuration file or svn authz file.* * Gitblit: aggregate RSS feeds by tag or subfolder * Gitblit: Consider creating more Git model objects and exposing them via the JSON RPC interface to allow inspection/retrieval of Git commits, Git trees, etc from Gitblit. * Gitblit: Blame coloring by author (issue 2) * Gitblit: View binary files in blob page (issue 6) resources/clippy.png
resources/gitblit.css
@@ -734,6 +734,10 @@ border-bottom: 1px solid #aaa; } table.repositories tr.group td a { color: black; } table.palette { border:0; width: 0 !important; } table.palette td.header { font-weight: bold; src/com/gitblit/GitBlit.java
@@ -37,6 +37,7 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; @@ -79,6 +80,7 @@ import com.gitblit.models.FederationProposal; import com.gitblit.models.FederationSet; import com.gitblit.models.Metric; import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.SearchResult; import com.gitblit.models.ServerSettings; @@ -132,6 +134,8 @@ private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>(); private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>(); private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>(""); private RepositoryResolver<Void> repositoryResolver; @@ -153,6 +157,8 @@ private LuceneExecutor luceneExecutor; private TimeZone timezone; private FileBasedConfig projectConfigs; public GitBlit() { if (gitblit == null) { @@ -1018,6 +1024,130 @@ // return a copy of the cached model return DeepCopier.copy(model); } /** * Returns the map of project config. This map is cached and reloaded if * the underlying projects.conf file changes. * * @return project config map */ private Map<String, ProjectModel> getProjectConfigs() { if (projectConfigs.isOutdated()) { try { projectConfigs.load(); } catch (Exception e) { } // project configs String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main"); ProjectModel rootProject = new ProjectModel(rootName, true); Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>(); // cache the root project under its alias and an empty path configs.put("", rootProject); configs.put(rootProject.name.toLowerCase(), rootProject); for (String name : projectConfigs.getSubsections("project")) { ProjectModel project; if (name.equalsIgnoreCase(rootName)) { project = rootProject; } else { project = new ProjectModel(name); } project.title = projectConfigs.getString("project", name, "title"); project.description = projectConfigs.getString("project", name, "description"); // TODO add more interesting metadata // project manager? // commit message regex? // RW+ // RW // R configs.put(name.toLowerCase(), project); } projectCache.clear(); projectCache.putAll(configs); } return projectCache; } /** * Returns a list of project models for the user. * * @param user * @return list of projects that are accessible to the user */ public List<ProjectModel> getProjectModels(UserModel user) { Map<String, ProjectModel> configs = getProjectConfigs(); // per-user project lists, this accounts for security and visibility Map<String, ProjectModel> map = new TreeMap<String, ProjectModel>(); // root project map.put("", configs.get("")); for (RepositoryModel model : getRepositoryModels(user)) { String rootPath = StringUtils.getRootPath(model.name).toLowerCase(); if (!map.containsKey(rootPath)) { ProjectModel project; if (configs.containsKey(rootPath)) { // clone the project model because it's repository list will // be tailored for the requesting user project = DeepCopier.copy(configs.get(rootPath)); } else { project = new ProjectModel(rootPath); } map.put(rootPath, project); } map.get(rootPath).addRepository(model); } // sort projects, root project first List<ProjectModel> projects = new ArrayList<ProjectModel>(map.values()); Collections.sort(projects); projects.remove(map.get("")); projects.add(0, map.get("")); return projects; } /** * Returns the project model for the specified user. * * @param name * @param user * @return a project model, or null if it does not exist */ public ProjectModel getProjectModel(String name, UserModel user) { for (ProjectModel project : getProjectModels(user)) { if (project.name.equalsIgnoreCase(name)) { return project; } } return null; } /** * Returns a project model for the Gitblit/system user. * * @param name a project name * @return a project model or null if the project does not exist */ public ProjectModel getProjectModel(String name) { Map<String, ProjectModel> configs = getProjectConfigs(); ProjectModel project = configs.get(name.toLowerCase()); if (project == null) { return null; } // clone the object project = DeepCopier.copy(project); String folder = name.toLowerCase() + "/"; for (String repository : getRepositoryList()) { if (repository.toLowerCase().startsWith(folder)) { project.addRepository(repository); } } return project; } /** @@ -2180,6 +2310,11 @@ loginService = new GitblitUserService(); } setUserService(loginService); // load and cache the project metadata projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect()); getProjectConfigs(); mailExecutor = new MailExecutor(settings); if (mailExecutor.isReady()) { logger.info("Mail executor is scheduled to process the message queue every 2 minutes."); src/com/gitblit/SyndicationFilter.java
@@ -15,19 +15,30 @@ */ package com.gitblit; import java.io.IOException; import java.text.MessageFormat; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; /** * The SyndicationFilter is an AccessRestrictionFilter which ensures that feed * requests for view-restricted repositories have proper authentication * The SyndicationFilter is an AuthenticationFilter which ensures that feed * requests for projects or view-restricted repositories have proper authentication * credentials and are authorized for the requested feed. * * @author James Moger * */ public class SyndicationFilter extends AccessRestrictionFilter { public class SyndicationFilter extends AuthenticationFilter { /** * Extract the repository name from the url. @@ -35,8 +46,7 @@ * @param url * @return repository name */ @Override protected String extractRepositoryName(String url) { protected String extractRequestedName(String url) { if (url.indexOf('?') > -1) { return url.substring(0, url.indexOf('?')); } @@ -44,52 +54,91 @@ } /** * Analyze the url and returns the action of the request. * doFilter does the actual work of preprocessing the request to ensure that * the user may proceed. * * @param url * @return action of the request * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, * javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override protected String getUrlRequestAction(String url) { return "VIEW"; public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String fullUrl = getFullUrl(httpRequest); String name = extractRequestedName(fullUrl); ProjectModel project = GitBlit.self().getProjectModel(name); RepositoryModel model = null; if (project == null) { // try loading a repository model model = GitBlit.self().getRepositoryModel(name); if (model == null) { // repository not found. send 404. logger.info(MessageFormat.format("ARF: {0} ({1})", fullUrl, HttpServletResponse.SC_NOT_FOUND)); httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND); return; } } /** * Determine if the action may be executed on the repository. * * @param repository * @param action * @return true if the action may be performed */ @Override protected boolean isActionAllowed(RepositoryModel repository, String action) { return true; // Wrap the HttpServletRequest with the AccessRestrictionRequest which // overrides the servlet container user principal methods. // JGit requires either: // // 1. servlet container authenticated user // 2. http.receivepack = true in each repository's config // // Gitblit must conditionally authenticate users per-repository so just // enabling http.receivepack is insufficient. AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest); UserModel user = getUser(httpRequest); if (user != null) { authenticatedRequest.setUser(user); } /** * Determine if the repository requires authentication. * * @param repository * @param action * @return true if authentication required */ @Override protected boolean requiresAuthentication(RepositoryModel repository, String action) { return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW); // BASIC authentication challenge and response processing if (model != null) { if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { if (user == null) { // challenge client to provide credentials. send 401. if (GitBlit.isDebugMode()) { logger.info(MessageFormat.format("ARF: CHALLENGE {0}", fullUrl)); } httpResponse.setHeader("WWW-Authenticate", CHALLENGE); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } else { // check user access for request if (user.canAdmin || user.canAccessRepository(model)) { // authenticated request permitted. // pass processing to the restricted servlet. newSession(authenticatedRequest, httpResponse); logger.info(MessageFormat.format("ARF: {0} ({1}) authenticated", fullUrl, HttpServletResponse.SC_CONTINUE)); chain.doFilter(authenticatedRequest, httpResponse); return; } // valid user, but not for requested access. send 403. if (GitBlit.isDebugMode()) { logger.info(MessageFormat.format("ARF: {0} forbidden to access {1}", user.username, fullUrl)); } httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } } /** * Determine if the user can access the repository and perform the specified * action. * * @param repository * @param user * @param action * @return true if user may execute the action on the repository */ @Override protected boolean canAccess(RepositoryModel repository, UserModel user, String action) { return user.canAccessRepository(repository); if (GitBlit.isDebugMode()) { logger.info(MessageFormat.format("ARF: {0} ({1}) unauthenticated", fullUrl, HttpServletResponse.SC_CONTINUE)); } // unauthenticated request permitted. // pass processing to the restricted servlet. chain.doFilter(authenticatedRequest, httpResponse); } } src/com/gitblit/SyndicationServlet.java
@@ -17,6 +17,8 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -28,9 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.AuthenticationFilter.AuthenticatedRequest; import com.gitblit.models.FeedEntryModel; import com.gitblit.models.ProjectModel; import com.gitblit.models.RefModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.StringUtils; @@ -157,19 +162,36 @@ } response.setContentType("application/rss+xml; charset=UTF-8"); Repository repository = GitBlit.self().getRepository(repositoryName); RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName); List<RevCommit> commits; if (StringUtils.isEmpty(searchString)) { // standard log/history lookup commits = JGitUtils.getRevLog(repository, objectId, offset, length); } else { // repository search commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType, offset, length); boolean isProjectFeed = false; String feedName = null; String feedTitle = null; String feedDescription = null; List<String> repositories = null; if (repositoryName.indexOf('/') == -1 && !repositoryName.toLowerCase().endsWith(".git")) { // try to find a project UserModel user = null; if (request instanceof AuthenticatedRequest) { user = ((AuthenticatedRequest) request).getUser(); } Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository); List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>(); ProjectModel project = GitBlit.self().getProjectModel(repositoryName, user); if (project != null) { isProjectFeed = true; repositories = new ArrayList<String>(project.repositories); // project feed feedName = project.name; feedTitle = project.title; feedDescription = project.description; } } if (repositories == null) { // could not find project, assume this is a repository repositories = Arrays.asList(repositoryName); } boolean mountParameters = GitBlit.getBoolean(Keys.web.mountParameters, true); String urlPattern; @@ -182,6 +204,31 @@ } String gitblitUrl = HttpUtils.getGitblitURL(request); char fsc = GitBlit.getChar(Keys.web.forwardSlashCharacter, '/'); List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>(); for (String name : repositories) { Repository repository = GitBlit.self().getRepository(name); RepositoryModel model = GitBlit.self().getRepositoryModel(name); if (!isProjectFeed) { // single-repository feed feedName = model.name; feedTitle = model.name; feedDescription = model.description; } List<RevCommit> commits; if (StringUtils.isEmpty(searchString)) { // standard log/history lookup commits = JGitUtils.getRevLog(repository, objectId, offset, length); } else { // repository search commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType, offset, length); } Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository); // convert RevCommit to SyndicatedEntryModel for (RevCommit commit : commits) { FeedEntryModel entry = new FeedEntryModel(); @@ -213,20 +260,43 @@ } entries.add(entry); } } // sort & truncate the feed Collections.sort(entries); if (entries.size() > length) { // clip the list entries = entries.subList(0, length); } String feedLink; if (isProjectFeed) { // project feed if (mountParameters) { // mounted url feedLink = MessageFormat.format("{0}/project/{1}", gitblitUrl, StringUtils.encodeURL(feedName)); } else { // parameterized url feedLink = MessageFormat.format("{0}/project/?p={1}", gitblitUrl, StringUtils.encodeURL(feedName)); } } else { // repository feed if (mountParameters) { // mounted url feedLink = MessageFormat.format("{0}/summary/{1}", gitblitUrl, StringUtils.encodeURL(model.name)); StringUtils.encodeURL(feedName)); } else { // parameterized url feedLink = MessageFormat.format("{0}/summary/?r={1}", gitblitUrl, StringUtils.encodeURL(model.name)); StringUtils.encodeURL(feedName)); } } try { SyndicationUtils.toRSS(gitblitUrl, feedLink, getTitle(model.name, objectId), model.description, model.name, entries, response.getOutputStream()); SyndicationUtils.toRSS(gitblitUrl, feedLink, getTitle(feedTitle, objectId), feedDescription, entries, response.getOutputStream()); } catch (Exception e) { logger.error("An error occurred during feed generation", e); } src/com/gitblit/models/ProjectModel.java
New file @@ -0,0 +1,95 @@ /* * Copyright 2012 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.models; import java.io.Serializable; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Set; import com.gitblit.utils.StringUtils; /** * ProjectModel is a serializable model class. * * @author James Moger * */ public class ProjectModel implements Serializable, Comparable<ProjectModel> { private static final long serialVersionUID = 1L; // field names are reflectively mapped in EditProject page public final String name; public String title; public String description; public final Set<String> repositories = new HashSet<String>(); public Date lastChange; public final boolean isRoot; public ProjectModel(String name) { this(name, false); } public ProjectModel(String name, boolean isRoot) { this.name = name; this.isRoot = isRoot; this.lastChange = new Date(0); this.title = ""; this.description = ""; } public boolean hasRepository(String name) { return repositories.contains(name.toLowerCase()); } public void addRepository(String name) { repositories.add(name.toLowerCase()); } public void addRepository(RepositoryModel model) { repositories.add(model.name.toLowerCase()); if (lastChange.before(model.lastChange)) { lastChange = model.lastChange; } } public void addRepositories(Collection<String> names) { for (String name:names) { repositories.add(name.toLowerCase()); } } public void removeRepository(String name) { repositories.remove(name.toLowerCase()); } public String getDisplayName() { return StringUtils.isEmpty(title) ? name : title; } @Override public String toString() { return name; } @Override public int compareTo(ProjectModel o) { return name.compareTo(o.name); } } src/com/gitblit/utils/SyndicationUtils.java
@@ -56,14 +56,13 @@ * @param feedLink * @param title * @param description * @param repository * @param entryModels * @param os * @throws IOException * @throws FeedException */ public static void toRSS(String hostUrl, String feedLink, String title, String description, String repository, List<FeedEntryModel> entryModels, OutputStream os) List<FeedEntryModel> entryModels, OutputStream os) throws IOException, FeedException { SyndFeed feed = new SyndFeedImpl(); src/com/gitblit/wicket/GitBlitWebApp.java
@@ -42,6 +42,8 @@ import com.gitblit.wicket.pages.MarkdownPage; import com.gitblit.wicket.pages.MetricsPage; import com.gitblit.wicket.pages.PatchPage; import com.gitblit.wicket.pages.ProjectPage; import com.gitblit.wicket.pages.ProjectsPage; import com.gitblit.wicket.pages.RawPage; import com.gitblit.wicket.pages.RepositoriesPage; import com.gitblit.wicket.pages.ReviewProposalPage; @@ -112,6 +114,8 @@ mount("/activity", ActivityPage.class, "r", "h"); mount("/gravatar", GravatarProfilePage.class, "h"); mount("/lucene", LuceneSearchPage.class); mount("/project", ProjectPage.class, "p"); mount("/projects", ProjectsPage.class); } private void mount(String location, Class<? extends WebPage> clazz, String... parameters) { src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -315,3 +315,7 @@ gb.allowNamedDescription = grant restricted access to named users or teams gb.markdownFailure = Failed to parse Markdown content! gb.clearCache = clear cache gb.projects = projects gb.project = project gb.allProjects = all projects gb.copyToClipboard = copy to clipboard src/com/gitblit/wicket/WicketUtils.java
@@ -276,6 +276,10 @@ return new PageParameters("team=" + teamname); } public static PageParameters newProjectParameter(String projectName) { return new PageParameters("p=" + projectName); } public static PageParameters newRepositoryParameter(String repositoryName) { return new PageParameters("r=" + repositoryName); } @@ -353,6 +357,10 @@ + ",st=" + type.name() + ",pg=" + pageNumber); } public static String getProjectName(PageParameters params) { return params.getString("p", ""); } public static String getRepositoryName(PageParameters params) { return params.getString("r", ""); } src/com/gitblit/wicket/pages/BasePage.java
@@ -15,10 +15,19 @@ */ package com.gitblit.wicket.pages; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.TimeZone; import java.util.regex.Pattern; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -39,7 +48,6 @@ import org.apache.wicket.protocol.http.WebRequest; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; import org.apache.wicket.request.RequestParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,10 +56,14 @@ import com.gitblit.Constants.FederationStrategy; 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.PageRegistration.DropDownMenuItem; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.LinkPanel; @@ -238,6 +250,89 @@ return sb.toString(); } protected List<ProjectModel> getProjectModels() { final UserModel user = GitBlitWebSession.get().getUser(); List<ProjectModel> projects = GitBlit.self().getProjectModels(user); return projects; } protected List<ProjectModel> getProjects(PageParameters params) { if (params == null) { return getProjectModels(); } boolean hasParameter = false; String regex = WicketUtils.getRegEx(params); String team = WicketUtils.getTeam(params); int daysBack = params.getInt("db", 0); List<ProjectModel> availableModels = getProjectModels(); Set<ProjectModel> models = new HashSet<ProjectModel>(); if (!StringUtils.isEmpty(regex)) { // filter the projects by the regex hasParameter = true; Pattern pattern = Pattern.compile(regex); for (ProjectModel model : availableModels) { if (pattern.matcher(model.name).find()) { models.add(model); } } } if (!StringUtils.isEmpty(team)) { // filter the projects by the specified teams hasParameter = true; List<String> teams = StringUtils.getStringsFromValue(team, ","); // need TeamModels first List<TeamModel> teamModels = new ArrayList<TeamModel>(); for (String name : teams) { TeamModel teamModel = GitBlit.self().getTeamModel(name); if (teamModel != null) { teamModels.add(teamModel); } } // brute-force our way through finding the matching models for (ProjectModel projectModel : availableModels) { for (String repositoryName : projectModel.repositories) { for (TeamModel teamModel : teamModels) { if (teamModel.hasRepository(repositoryName)) { models.add(projectModel); } } } } } if (!hasParameter) { models.addAll(availableModels); } // time-filter the list if (daysBack > 0) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); cal.add(Calendar.DATE, -1 * daysBack); Date threshold = cal.getTime(); Set<ProjectModel> timeFiltered = new HashSet<ProjectModel>(); for (ProjectModel model : models) { if (model.lastChange.after(threshold)) { timeFiltered.add(model); } } models = timeFiltered; } List<ProjectModel> list = new ArrayList<ProjectModel>(models); Collections.sort(list); return list; } public void warn(String message, Throwable t) { logger.warn(message, t); } @@ -246,7 +341,6 @@ logger.error(message + " for " + GitBlitWebSession.get().getUsername()); if (redirect) { GitBlitWebSession.get().cacheErrorMessage(message); RequestParameters params = getRequest().getRequestParameters(); String relativeUrl = urlFor(RepositoriesPage.class, null).toString(); String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl); throw new RedirectToUrlException(absoluteUrl); src/com/gitblit/wicket/pages/ProjectPage.html
New file @@ -0,0 +1,132 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" xml:lang="en" lang="en"> <body> <wicket:extend> <wicket:fragment wicket:id="repositoryAdminLinks"> <span class="link"> <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a> </span> </wicket:fragment> <wicket:fragment wicket:id="repositoryOwnerLinks"> <span class="link"> <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a> </span> </wicket:fragment> <wicket:fragment wicket:id="repositoryUserLinks"> <span class="link"> <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> </span> </wicket:fragment> <div class="row"> <div class="span12"> <h2><span wicket:id="projectTitle"></span> <small><span wicket:id="projectDescription"></span></small> <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed"> <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img> </a> </h2> <div class="markdown" wicket:id="projectMessage">[project message]</div> </div> </div> <div class="tabbable"> <!-- tab titles --> <ul class="nav nav-tabs"> <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li> <li ><a href="#activity" data-toggle="tab"><wicket:message key="gb.activity"></wicket:message></a></li> </ul> <!-- tab content --> <div class="tab-content"> <!-- repositories tab --> <div class="tab-pane active" id="repositories"> <!-- markdown --> <div class="row"> <div class="span12"> <div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div> </div> </div> <!-- repositories --> <div class="row"> <div class="span6" style="border-top:1px solid #eee;" wicket:id="repository"> <div style="padding-top:15px;padding-bottom:15px;margin-right:15px;"> <div class="pull-right" style="text-align:right;padding-right:15px;"> <span wicket:id="repositoryLinks"></span> <div> <img class="inlineIcon" wicket:id="frozenIcon" /> <img class="inlineIcon" wicket:id="federatedIcon" /> <a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets"> <img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img> </a> <a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs"> <img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img> </a> <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed"> <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img> </a> </div> <span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span> </div> <h3><span class="repositorySwatch" wicket:id="repositorySwatch"></span> <span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span> <img class="inlineIcon" wicket:id="accessRestrictionIcon" /> </h3> <div style="padding-left:20px;"> <div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div> <div style="color: #999;"> <wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>, <span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span> </div> <div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div> </div> </div> </div> </div> </div> <!-- activity tab --> <div class="tab-pane" id="activity"> <div class="pageTitle"> <h2><wicket:message key="gb.recentActivity"></wicket:message><small> <span class="hidden-phone">/ <span wicket:id="subheader">[days back]</span></span></small></h2> </div> <div class="hidden-phone" style="height: 155px;text-align: center;"> <table> <tr> <td><span class="hidden-tablet" id="chartDaily"></span></td> <td><span id="chartRepositories"></span></td> <td><span id="chartAuthors"></span></td> </tr> </table> </div> <div wicket:id="activityPanel">[activity panel]</div> </div> </div> </div> </wicket:extend> </body> </html> src/com/gitblit/wicket/pages/ProjectPage.java
New file @@ -0,0 +1,502 @@ /* * Copyright 2012 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.pages; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.wicket.Component; import org.apache.wicket.PageParameters; import org.apache.wicket.RedirectException; import org.apache.wicket.behavior.HeaderContributor; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.markup.repeater.data.ListDataProvider; import org.eclipse.jgit.lib.Constants; import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.SyndicationServlet; import com.gitblit.models.Activity; import com.gitblit.models.Metric; import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ActivityUtils; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.MarkdownUtils; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebApp; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.PageRegistration; import com.gitblit.wicket.PageRegistration.DropDownMenuItem; import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.charting.GoogleChart; import com.gitblit.wicket.charting.GoogleCharts; import com.gitblit.wicket.charting.GoogleLineChart; import com.gitblit.wicket.charting.GooglePieChart; import com.gitblit.wicket.panels.ActivityPanel; import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation; import com.gitblit.wicket.panels.LinkPanel; import com.gitblit.wicket.panels.RepositoryUrlPanel; public class ProjectPage extends RootPage { List<ProjectModel> projectModels = new ArrayList<ProjectModel>(); public ProjectPage() { super(); throw new RedirectException(GitBlitWebApp.get().getHomePage()); } public ProjectPage(PageParameters params) { super(params); setup(params); } @Override protected boolean reusePageParameters() { return true; } private void setup(PageParameters params) { setupPage("", ""); // check to see if we should display a login message boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true); if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) { authenticationError("Please login"); return; } String projectName = WicketUtils.getProjectName(params); if (StringUtils.isEmpty(projectName)) { throw new RedirectException(GitBlitWebApp.get().getHomePage()); } ProjectModel project = getProjectModel(projectName); if (project == null) { throw new RedirectException(GitBlitWebApp.get().getHomePage()); } add(new Label("projectTitle", project.getDisplayName())); add(new Label("projectDescription", project.description)); String feedLink = SyndicationServlet.asLink(getRequest().getRelativePathPrefixToContextRoot(), projectName, null, 0); add(new ExternalLink("syndication", feedLink)); add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(project.getDisplayName(), null), feedLink)); String groupName = projectName; if (project.isRoot) { groupName = ""; } else { groupName += "/"; } // project markdown message File pmkd = new File(GitBlit.getRepositoriesFolder(), groupName + "project.mkd"); String pmessage = readMarkdown(projectName, pmkd); Component projectMessage = new Label("projectMessage", pmessage) .setEscapeModelStrings(false).setVisible(pmessage.length() > 0); add(projectMessage); // markdown message above repositories list File rmkd = new File(GitBlit.getRepositoriesFolder(), groupName + "repositories.mkd"); String rmessage = readMarkdown(projectName, rmkd); Component repositoriesMessage = new Label("repositoriesMessage", rmessage) .setEscapeModelStrings(false).setVisible(rmessage.length() > 0); add(repositoriesMessage); List<RepositoryModel> repositories = getRepositories(params); Collections.sort(repositories, new Comparator<RepositoryModel>() { @Override public int compare(RepositoryModel o1, RepositoryModel o2) { // reverse-chronological sort return o2.lastChange.compareTo(o1.lastChange); } }); final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true); final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true); final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true); final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories); DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) { private static final long serialVersionUID = 1L; public void populateItem(final Item<RepositoryModel> item) { final RepositoryModel entry = item.getModelObject(); // repository swatch Component swatch; if (entry.isBare){ swatch = new Label("repositorySwatch", " ").setEscapeModelStrings(false); } else { swatch = new Label("repositorySwatch", "!"); WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning")); } WicketUtils.setCssBackground(swatch, entry.toString()); item.add(swatch); swatch.setVisible(showSwatch); PageParameters pp = WicketUtils.newRepositoryParameter(entry.name); item.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class, pp)); item.add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils.isEmpty(entry.description))); item.add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets)); item.add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs)); if (entry.isFrozen) { item.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen"))); } else { item.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false)); } if (entry.isFederated) { item.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", getString("gb.isFederated"))); } else { item.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false)); } switch (entry.accessRestriction) { case NONE: item.add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false)); break; case PUSH: item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", getAccessRestrictions().get(entry.accessRestriction))); break; case CLONE: item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", getAccessRestrictions().get(entry.accessRestriction))); break; case VIEW: item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", getAccessRestrictions().get(entry.accessRestriction))); break; default: item.add(WicketUtils.newBlankImage("accessRestrictionIcon")); } item.add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " (" + getString("gb.owner") + ")"))); UserModel user = GitBlitWebSession.get().getUser(); Fragment repositoryLinks; boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner); if (showAdmin || showOwner) { repositoryLinks = new Fragment("repositoryLinks", showAdmin ? "repositoryAdminLinks" : "repositoryOwnerLinks", this); repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class, WicketUtils .newRepositoryParameter(entry.name))); if (showAdmin) { Link<Void> deleteLink = new Link<Void>("deleteRepository") { private static final long serialVersionUID = 1L; @Override public void onClick() { if (GitBlit.self().deleteRepositoryModel(entry)) { info(MessageFormat.format(getString("gb.repositoryDeleted"), entry)); // TODO dp.remove(entry); } else { error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry)); } } }; deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format( getString("gb.deleteRepository"), entry))); repositoryLinks.add(deleteLink); } } else { repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this); } repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits)); repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits)); item.add(repositoryLinks); String lastChange; if (entry.lastChange.getTime() == 0) { lastChange = "--"; } else { lastChange = getTimeUtils().timeAgo(entry.lastChange); } Label lastChangeLabel = new Label("repositoryLastChange", lastChange); item.add(lastChangeLabel); WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange)); if (entry.hasCommits) { // Existing repository item.add(new Label("repositorySize", entry.size).setVisible(showSize)); } else { // New repository item.add(new Label("repositorySize", getString("gb.empty")) .setEscapeModelStrings(false)); } item.add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0))); List<String> repositoryUrls = new ArrayList<String>(); if (gitServlet) { // add the Gitblit repository url repositoryUrls.add(getRepositoryUrl(entry)); } repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name)); String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0); item.add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl)); } }; add(dataView); // project activity // parameters int daysBack = WicketUtils.getDaysBack(params); if (daysBack < 1) { daysBack = 14; } String objectId = WicketUtils.getObject(params); List<Activity> recentActivity = ActivityUtils.getRecentActivity(repositories, daysBack, objectId, getTimeZone()); if (recentActivity.size() == 0) { // no activity, skip graphs and activity panel add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityNone"), daysBack))); add(new Label("activityPanel")); } else { // calculate total commits and total authors int totalCommits = 0; Set<String> uniqueAuthors = new HashSet<String>(); for (Activity activity : recentActivity) { totalCommits += activity.getCommitCount(); uniqueAuthors.addAll(activity.getAuthorMetrics().keySet()); } int totalAuthors = uniqueAuthors.size(); // add the subheader with stat numbers add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityStats"), daysBack, totalCommits, totalAuthors))); // create the activity charts GoogleCharts charts = createCharts(recentActivity); add(new HeaderContributor(charts)); // add activity panel add(new ActivityPanel("activityPanel", recentActivity)); } } /** * Creates the daily activity line chart, the active repositories pie chart, * and the active authors pie chart * * @param recentActivity * @return */ private GoogleCharts createCharts(List<Activity> recentActivity) { // activity metrics Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>(); Map<String, Metric> authorMetrics = new HashMap<String, Metric>(); // aggregate repository and author metrics for (Activity activity : recentActivity) { // aggregate author metrics for (Map.Entry<String, Metric> entry : activity.getAuthorMetrics().entrySet()) { String author = entry.getKey(); if (!authorMetrics.containsKey(author)) { authorMetrics.put(author, new Metric(author)); } authorMetrics.get(author).count += entry.getValue().count; } // aggregate repository metrics for (Map.Entry<String, Metric> entry : activity.getRepositoryMetrics().entrySet()) { String repository = StringUtils.stripDotGit(entry.getKey()); if (!repositoryMetrics.containsKey(repository)) { repositoryMetrics.put(repository, new Metric(repository)); } repositoryMetrics.get(repository).count += entry.getValue().count; } } // build google charts int w = 310; int h = 150; GoogleCharts charts = new GoogleCharts(); // sort in reverse-chronological order and then reverse that Collections.sort(recentActivity); Collections.reverse(recentActivity); // daily line chart GoogleChart chart = new GoogleLineChart("chartDaily", getString("gb.dailyActivity"), "day", getString("gb.commits")); SimpleDateFormat df = new SimpleDateFormat("MMM dd"); df.setTimeZone(getTimeZone()); for (Activity metric : recentActivity) { chart.addValue(df.format(metric.startDate), metric.getCommitCount()); } chart.setWidth(w); chart.setHeight(h); charts.addChart(chart); // active repositories pie chart chart = new GooglePieChart("chartRepositories", getString("gb.activeRepositories"), getString("gb.repository"), getString("gb.commits")); for (Metric metric : repositoryMetrics.values()) { chart.addValue(metric.name, metric.count); } chart.setWidth(w); chart.setHeight(h); charts.addChart(chart); // active authors pie chart chart = new GooglePieChart("chartAuthors", getString("gb.activeAuthors"), getString("gb.author"), getString("gb.commits")); for (Metric metric : authorMetrics.values()) { chart.addValue(metric.name, metric.count); } chart.setWidth(w); chart.setHeight(h); charts.addChart(chart); return charts; } @Override protected void addDropDownMenus(List<PageRegistration> pages) { PageParameters params = getPageParameters(); DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects", ProjectPage.class); projects.menuItems.addAll(getProjectsMenu()); pages.add(0, projects); DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters", ProjectPage.class); // preserve time filter option on repository choices menu.menuItems.addAll(getRepositoryFilterItems(params)); // preserve repository filter option on time choices menu.menuItems.addAll(getTimeFilterItems(params)); if (menu.menuItems.size() > 0) { // Reset Filter menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null)); } pages.add(menu); } @Override protected List<ProjectModel> getProjectModels() { if (projectModels.isEmpty()) { final UserModel user = GitBlitWebSession.get().getUser(); List<ProjectModel> projects = GitBlit.self().getProjectModels(user); projectModels.addAll(projects); } return projectModels; } private ProjectModel getProjectModel(String name) { for (ProjectModel project : getProjectModels()) { if (name.equalsIgnoreCase(project.name)) { return project; } } return null; } protected List<DropDownMenuItem> getProjectsMenu() { List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>(); List<ProjectModel> projects = getProjectModels(); int maxProjects = 15; boolean showAllProjects = projects.size() > maxProjects; if (showAllProjects) { // sort by last changed Collections.sort(projects, new Comparator<ProjectModel>() { @Override public int compare(ProjectModel o1, ProjectModel o2) { return o2.lastChange.compareTo(o1.lastChange); } }); // take most recent subset projects = projects.subList(0, maxProjects); // sort those by name Collections.sort(projects); } for (ProjectModel project : projects) { menu.add(new DropDownMenuItem(project.getDisplayName(), "p", project.name)); } if (showAllProjects) { menu.add(new DropDownMenuItem()); menu.add(new DropDownMenuItem("all projects", null, null)); } return menu; } private String readMarkdown(String projectName, File projectMessage) { String message = ""; if (projectMessage.exists()) { // Read user-supplied message try { FileInputStream fis = new FileInputStream(projectMessage); InputStreamReader reader = new InputStreamReader(fis, Constants.CHARACTER_ENCODING); message = MarkdownUtils.transformMarkdown(reader); reader.close(); } catch (Throwable t) { message = getString("gb.failedToRead") + " " + projectMessage; warn(message, t); } } return message; } } src/com/gitblit/wicket/pages/ProjectsPage.html
New file @@ -0,0 +1,37 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" xml:lang="en" lang="en"> <body> <wicket:extend> <div class="markdown" style="padding-bottom:5px;" wicket:id="projectsMessage">[projects message]</div> <table class="repositories"> <thead> <tr> <th class="left"> <i class="icon-folder-close" ></i> <wicket:message key="gb.project">Project</wicket:message> </th> <th class="hidden-phone" ><span><wicket:message key="gb.description">Description</wicket:message></span></th> <th class="hidden-phone"><wicket:message key="gb.repositories">Repositories</wicket:message></th> <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th> <th class="right"></th> </tr> </thead> <tbody> <tr wicket:id="project"> <td class="left" style="padding-left:3px;" ><span style="padding-left:3px;" wicket:id="projectTitle">[project title]</span></td> <td class="hidden-phone"><span class="list" wicket:id="projectDescription">[project description]</span></td> <td class="hidden-phone" style="padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositoryCount">[repository count]</span></td> <td><span wicket:id="projectLastChange">[last change]</span></td> <td class="rightAlign"></td> </tr> </tbody> </table> </wicket:extend> </body> </html> src/com/gitblit/wicket/pages/ProjectsPage.java
New file @@ -0,0 +1,224 @@ /* * Copyright 2012 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.pages; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.PageParameters; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.markup.repeater.data.ListDataProvider; import org.apache.wicket.resource.ContextRelativeResource; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.eclipse.jgit.lib.Constants; import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.models.ProjectModel; import com.gitblit.utils.MarkdownUtils; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.PageRegistration; import com.gitblit.wicket.PageRegistration.DropDownMenuItem; import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.LinkPanel; public class ProjectsPage extends RootPage { List<ProjectModel> projectModels = new ArrayList<ProjectModel>(); public ProjectsPage() { super(); setup(null); } public ProjectsPage(PageParameters params) { super(params); setup(params); } @Override protected boolean reusePageParameters() { return true; } private void setup(PageParameters params) { setupPage("", ""); // check to see if we should display a login message boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true); if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) { String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit"); String message = readMarkdown(messageSource, "login.mkd"); Component repositoriesMessage = new Label("projectsMessage", message); add(repositoriesMessage.setEscapeModelStrings(false)); add(new Label("projectsPanel")); return; } // Load the markdown welcome message String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit"); String message = readMarkdown(messageSource, "welcome.mkd"); Component projectsMessage = new Label("projectsMessage", message).setEscapeModelStrings( false).setVisible(message.length() > 0); add(projectsMessage); List<ProjectModel> projects = getProjects(params); ListDataProvider<ProjectModel> dp = new ListDataProvider<ProjectModel>(projects); DataView<ProjectModel> dataView = new DataView<ProjectModel>("project", dp) { private static final long serialVersionUID = 1L; int counter; @Override protected void onBeforeRender() { super.onBeforeRender(); counter = 0; } public void populateItem(final Item<ProjectModel> item) { final ProjectModel entry = item.getModelObject(); PageParameters pp = WicketUtils.newProjectParameter(entry.name); item.add(new LinkPanel("projectTitle", "list", entry.getDisplayName(), ProjectPage.class, pp)); item.add(new LinkPanel("projectDescription", "list", entry.description, ProjectPage.class, pp)); item.add(new Label("repositoryCount", entry.repositories.size() + " " + (entry.repositories.size() == 1 ? getString("gb.repository") : getString("gb.repositories")))); String lastChange; if (entry.lastChange.getTime() == 0) { lastChange = "--"; } else { lastChange = getTimeUtils().timeAgo(entry.lastChange); } Label lastChangeLabel = new Label("projectLastChange", lastChange); item.add(lastChangeLabel); WicketUtils.setCssClass(lastChangeLabel, getTimeUtils() .timeAgoCss(entry.lastChange)); WicketUtils.setAlternatingBackground(item, counter); counter++; } }; add(dataView); // push the panel down if we are hiding the admin controls and the // welcome message if (!showAdmin && !projectsMessage.isVisible()) { WicketUtils.setCssStyle(dataView, "padding-top:5px;"); } } @Override protected void addDropDownMenus(List<PageRegistration> pages) { PageParameters params = getPageParameters(); pages.add(0, new PageRegistration("gb.projects", ProjectsPage.class, params)); DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters", ProjectsPage.class); // preserve time filter option on repository choices menu.menuItems.addAll(getRepositoryFilterItems(params)); // preserve repository filter option on time choices menu.menuItems.addAll(getTimeFilterItems(params)); if (menu.menuItems.size() > 0) { // Reset Filter menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null)); } pages.add(menu); } private String readMarkdown(String messageSource, String resource) { String message = ""; if (messageSource.equalsIgnoreCase("gitblit")) { // Read default message message = readDefaultMarkdown(resource); } else { // Read user-supplied message if (!StringUtils.isEmpty(messageSource)) { File file = new File(messageSource); if (file.exists()) { try { FileInputStream fis = new FileInputStream(file); InputStreamReader reader = new InputStreamReader(fis, Constants.CHARACTER_ENCODING); message = MarkdownUtils.transformMarkdown(reader); reader.close(); } catch (Throwable t) { message = getString("gb.failedToRead") + " " + file; warn(message, t); } } else { message = messageSource + " " + getString("gb.isNotValidFile"); } } } return message; } private String readDefaultMarkdown(String file) { String content = readDefaultMarkdown(file, getLanguageCode()); if (StringUtils.isEmpty(content)) { content = readDefaultMarkdown(file, null); } return content; } private String readDefaultMarkdown(String file, String lc) { if (!StringUtils.isEmpty(lc)) { // convert to file_lc.mkd file = file.substring(0, file.lastIndexOf('.')) + "_" + lc + file.substring(file.lastIndexOf('.')); } String message; try { ContextRelativeResource res = WicketUtils.getResource(file); InputStream is = res.getResourceStream().getInputStream(); InputStreamReader reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING); message = MarkdownUtils.transformMarkdown(reader); reader.close(); } catch (ResourceStreamNotFoundException t) { if (lc == null) { // could not find default language resource message = MessageFormat.format(getString("gb.failedToReadMessage"), file); error(message, t, false); } else { // ignore so we can try default language resource message = null; } } catch (Throwable t) { message = MessageFormat.format(getString("gb.failedToReadMessage"), file); error(message, t, false); } return message; } } src/com/gitblit/wicket/pages/RepositoryPage.html
@@ -21,7 +21,7 @@ <div class="nav-collapse" wicket:id="navPanel"></div> <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication"> <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed"> <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img> </a> src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -64,6 +64,7 @@ public abstract class RepositoryPage extends BasePage { protected final String projectName; protected final String repositoryName; protected final String objectId; @@ -78,6 +79,11 @@ public RepositoryPage(PageParameters params) { super(params); repositoryName = WicketUtils.getRepositoryName(params); if (repositoryName.indexOf('/') > -1) { projectName = repositoryName.substring(0, repositoryName.indexOf('/')); } else { projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main"); } objectId = WicketUtils.getObject(params); if (StringUtils.isEmpty(repositoryName)) { @@ -117,6 +123,7 @@ // standard links pages.put("repositories", new PageRegistration("gb.repositories", RepositoriesPage.class)); pages.put("project", new PageRegistration("gb.project", ProjectPage.class, WicketUtils.newProjectParameter(projectName))); pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params)); pages.put("log", new PageRegistration("gb.log", LogPage.class, params)); pages.put("branches", new PageRegistration("gb.branches", BranchesPage.class, params)); src/com/gitblit/wicket/pages/RootPage.java
@@ -178,6 +178,9 @@ PageParameters pp = getPageParameters(); if (pp != null) { PageParameters params = new PageParameters(pp); // remove named project parameter params.remove("p"); // remove named repository parameter params.remove("r"); @@ -230,6 +233,7 @@ final UserModel user = GitBlitWebSession.get().getUser(); List<RepositoryModel> repositories = GitBlit.self().getRepositoryModels(user); repositoryModels.addAll(repositories); Collections.sort(repositoryModels); } return repositoryModels; } @@ -322,6 +326,7 @@ } boolean hasParameter = false; String projectName = WicketUtils.getProjectName(params); String repositoryName = WicketUtils.getRepositoryName(params); String set = WicketUtils.getSet(params); String regex = WicketUtils.getRegEx(params); @@ -338,6 +343,27 @@ if (model.name.equalsIgnoreCase(repositoryName)) { models.add(model); break; } } } if (!StringUtils.isEmpty(projectName)) { // try named project hasParameter = true; if (projectName.equalsIgnoreCase(GitBlit.getString(Keys.web.repositoryRootGroupName, "main"))) { // root project/group for (RepositoryModel model : availableModels) { if (model.name.indexOf('/') == -1) { models.add(model); } } } else { // named project/group String group = projectName.toLowerCase() + "/"; for (RepositoryModel model : availableModels) { if (model.name.toLowerCase().startsWith(group)) { models.add(model); } } } } @@ -411,6 +437,9 @@ } models = timeFiltered; } return new ArrayList<RepositoryModel>(models); List<RepositoryModel> list = new ArrayList<RepositoryModel>(models); Collections.sort(list); return list; } } src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -71,7 +71,8 @@ </wicket:fragment> <wicket:fragment wicket:id="groupRepositoryRow"> <td colspan="7"><span wicket:id="groupName">[group name]</span></td> <td colspan="1"><span wicket:id="groupName">[group name]</span></td> <td colspan="6"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td> </wicket:fragment> <wicket:fragment wicket:id="repositoryRow"> @@ -84,7 +85,7 @@ <td class="rightAlign"> <span class="hidden-phone"> <span wicket:id="repositoryLinks"></span> <a style="text-decoration: none;" wicket:id="syndication"> <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed"> <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img> </a> </span> src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -46,6 +46,7 @@ import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.SyndicationServlet; import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.utils.StringUtils; @@ -54,6 +55,7 @@ import com.gitblit.wicket.pages.BasePage; import com.gitblit.wicket.pages.EditRepositoryPage; import com.gitblit.wicket.pages.EmptyRepositoryPage; import com.gitblit.wicket.pages.ProjectPage; import com.gitblit.wicket.pages.RepositoriesPage; import com.gitblit.wicket.pages.SummaryPage; @@ -112,10 +114,20 @@ roots.add(0, rootPath); groups.put(rootPath, rootRepositories); } Map<String, ProjectModel> projects = new HashMap<String, ProjectModel>(); for (ProjectModel project : GitBlit.self().getProjectModels(user)) { projects.put(project.name, project); } List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>(); for (String root : roots) { List<RepositoryModel> subModels = groups.get(root); groupedModels.add(new GroupRepositoryModel(root, subModels.size())); GroupRepositoryModel group = new GroupRepositoryModel(root, subModels.size()); if (projects.containsKey(root)) { group.title = projects.get(root).title; group.description = projects.get(root).description; } groupedModels.add(group); Collections.sort(subModels); groupedModels.addAll(subModels); } @@ -144,7 +156,8 @@ currGroupName = entry.name; Fragment row = new Fragment("rowContent", "groupRepositoryRow", this); item.add(row); row.add(new Label("groupName", entry.toString())); row.add(new LinkPanel("groupName", null, entry.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name))); row.add(new Label("groupDescription", entry.description == null ? "":entry.description)); WicketUtils.setCssClass(item, "group"); // reset counter so that first row is light background counter = 0; @@ -326,6 +339,7 @@ private static final long serialVersionUID = 1L; int count; String title; GroupRepositoryModel(String name, int count) { super(name, "", "", new Date(0)); @@ -334,7 +348,7 @@ @Override public String toString() { return name + " (" + count + ")"; return StringUtils.isEmpty(title) ? name : title + " (" + count + ")"; } } src/com/gitblit/wicket/panels/RepositoryUrlPanel.html
@@ -9,20 +9,21 @@ <!-- Plain JavaScript manual copy & paste --> <wicket:fragment wicket:id="jsPanel"> <span class="btn" style="padding:0px 3px 0px 3px;vertical-align:middle;"> <img wicket:id="copyIcon" style="padding-top:1px;"></img> <span style="vertical-align:baseline;"> <img wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard"></img> </span> </wicket:fragment> <!-- flash-based button-press copy & paste --> <wicket:fragment wicket:id="clippyPanel"> <object style="padding:0px 2px;vertical-align:middle;" <object wicket:message="title:gb.copyToClipboard" style="vertical-align:middle;" wicket:id="clippy" width="110" width="14" height="14" bgcolor="#ffffff" quality="high" wmode="transparent" scale="noscale" allowScriptAccess="always"></object> </wicket:fragment> </wicket:panel> src/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -42,8 +42,7 @@ } else { // javascript: manual copy & paste with modal browser prompt dialog Fragment fragment = new Fragment("copyFunction", "jsPanel", this); ContextImage img = WicketUtils.newImage("copyIcon", "clipboard_13x13.png"); WicketUtils.setHtmlTooltip(img, "Manual Copy to Clipboard"); ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png"); img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url)); fragment.add(img); add(fragment); tests/com/gitblit/tests/SyndicationUtilsTest.java
@@ -54,7 +54,7 @@ entries.add(entry); } ByteArrayOutputStream os = new ByteArrayOutputStream(); SyndicationUtils.toRSS("http://localhost", "", "Title", "Description", "Repository", SyndicationUtils.toRSS("http://localhost", "", "Title", "Description", entries, os); String feed = os.toString(); os.close();