From 13a3f5bc3e2d25fc76850f86070dc34efe60d77a Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 07 Sep 2012 22:06:15 -0400 Subject: [PATCH] Draft project pages, project metadata, and RSS feeds --- .gitignore | 1 src/com/gitblit/wicket/pages/ProjectsPage.java | 224 ++++++++ src/com/gitblit/wicket/panels/RepositoriesPanel.java | 20 src/com/gitblit/wicket/pages/RepositoryPage.java | 7 tests/com/gitblit/tests/SyndicationUtilsTest.java | 2 src/com/gitblit/wicket/pages/ProjectPage.html | 132 ++++ src/com/gitblit/GitBlit.java | 135 ++++ src/com/gitblit/wicket/GitBlitWebApp.properties | 6 src/com/gitblit/wicket/WicketUtils.java | 8 src/com/gitblit/wicket/pages/RootPage.java | 31 + src/com/gitblit/wicket/panels/RepositoryUrlPanel.html | 9 src/com/gitblit/wicket/pages/RepositoryPage.html | 2 src/com/gitblit/wicket/panels/RepositoriesPanel.html | 5 src/com/gitblit/wicket/panels/RepositoryUrlPanel.java | 3 docs/01_setup.mkd | 1 src/com/gitblit/SyndicationFilter.java | 143 +++- src/com/gitblit/wicket/pages/ProjectPage.java | 502 +++++++++++++++++ distrib/gitblit.properties | 5 src/com/gitblit/SyndicationServlet.java | 164 ++++- resources/clippy.png | 0 src/com/gitblit/utils/SyndicationUtils.java | 3 src/com/gitblit/wicket/pages/BasePage.java | 100 +++ src/com/gitblit/models/ProjectModel.java | 95 +++ src/com/gitblit/wicket/pages/ProjectsPage.html | 37 + src/com/gitblit/wicket/GitBlitWebApp.java | 4 docs/05_roadmap.mkd | 2 resources/gitblit.css | 4 27 files changed, 1,529 insertions(+), 116 deletions(-) diff --git a/.gitignore b/.gitignore index 6158fcd..e93e18e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ /users.conf *.directory /.gradle +/projects.conf diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 80cbb7e..c7f0ae3 100644 --- a/distrib/gitblit.properties +++ b/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. diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd index 42f870f..eaaf3be 100644 --- a/docs/01_setup.mkd +++ b/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) diff --git a/docs/05_roadmap.mkd b/docs/05_roadmap.mkd index 6b4def4..3238f73 100644 --- a/docs/05_roadmap.mkd +++ b/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) diff --git a/resources/clippy.png b/resources/clippy.png new file mode 100644 index 0000000..7a462e1 --- /dev/null +++ b/resources/clippy.png Binary files differ diff --git a/resources/gitblit.css b/resources/gitblit.css index b51637d..7a73a24 100644 --- a/resources/gitblit.css +++ b/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; diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index e6effc2..c758654 100644 --- a/src/com/gitblit/GitBlit.java +++ b/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."); diff --git a/src/com/gitblit/SyndicationFilter.java b/src/com/gitblit/SyndicationFilter.java index 0826566..0dff1c8 100644 --- a/src/com/gitblit/SyndicationFilter.java +++ b/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 { - /** - * 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; - } - - /** - * 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); - } + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; - /** - * 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); - } + 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; + } + } + + // 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); + } + + // 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; + } + } + } + + 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); + } } diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java index 81cfb76..4c542b6 100644 --- a/src/com/gitblit/SyndicationServlet.java +++ b/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(); + } + 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; + } } - Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository); - List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>(); + + 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,51 +204,99 @@ } String gitblitUrl = HttpUtils.getGitblitURL(request); char fsc = GitBlit.getChar(Keys.web.forwardSlashCharacter, '/'); - // convert RevCommit to SyndicatedEntryModel - for (RevCommit commit : commits) { - FeedEntryModel entry = new FeedEntryModel(); - entry.title = commit.getShortMessage(); - entry.author = commit.getAuthorIdent().getName(); - entry.link = MessageFormat.format(urlPattern, gitblitUrl, - StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName()); - entry.published = commit.getCommitterIdent().getWhen(); - entry.contentType = "text/html"; - String message = GitBlit.self().processCommitMessage(model.name, - commit.getFullMessage()); - entry.content = message; - entry.repository = model.name; - entry.branch = objectId; - entry.tags = new ArrayList<String>(); + + List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>(); + + for (String name : repositories) { + Repository repository = GitBlit.self().getRepository(name); + RepositoryModel model = GitBlit.self().getRepositoryModel(name); - // add commit id and parent commit ids - entry.tags.add("commit:" + commit.getName()); - for (RevCommit parent : commit.getParents()) { - entry.tags.add("parent:" + parent.getName()); + if (!isProjectFeed) { + // single-repository feed + feedName = model.name; + feedTitle = model.name; + feedDescription = model.description; } - // add refs to tabs list - List<RefModel> refs = allRefs.get(commit.getId()); - if (refs != null && refs.size() > 0) { - for (RefModel ref : refs) { - entry.tags.add("ref:" + ref.getName()); + 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(); + entry.title = commit.getShortMessage(); + entry.author = commit.getAuthorIdent().getName(); + entry.link = MessageFormat.format(urlPattern, gitblitUrl, + StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName()); + entry.published = commit.getCommitterIdent().getWhen(); + entry.contentType = "text/html"; + String message = GitBlit.self().processCommitMessage(model.name, + commit.getFullMessage()); + entry.content = message; + entry.repository = model.name; + entry.branch = objectId; + entry.tags = new ArrayList<String>(); + + // add commit id and parent commit ids + entry.tags.add("commit:" + commit.getName()); + for (RevCommit parent : commit.getParents()) { + entry.tags.add("parent:" + parent.getName()); } - } - entries.add(entry); + + // add refs to tabs list + List<RefModel> refs = allRefs.get(commit.getId()); + if (refs != null && refs.size() > 0) { + for (RefModel ref : refs) { + entry.tags.add("ref:" + ref.getName()); + } + } + 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 (mountParameters) { - // mounted url - feedLink = MessageFormat.format("{0}/summary/{1}", gitblitUrl, - StringUtils.encodeURL(model.name)); + 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 { - // parameterized url - feedLink = MessageFormat.format("{0}/summary/?r={1}", gitblitUrl, - StringUtils.encodeURL(model.name)); + // repository feed + if (mountParameters) { + // mounted url + feedLink = MessageFormat.format("{0}/summary/{1}", gitblitUrl, + StringUtils.encodeURL(feedName)); + } else { + // parameterized url + feedLink = MessageFormat.format("{0}/summary/?r={1}", gitblitUrl, + 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); } diff --git a/src/com/gitblit/models/ProjectModel.java b/src/com/gitblit/models/ProjectModel.java new file mode 100644 index 0000000..bc35903 --- /dev/null +++ b/src/com/gitblit/models/ProjectModel.java @@ -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); + } +} diff --git a/src/com/gitblit/utils/SyndicationUtils.java b/src/com/gitblit/utils/SyndicationUtils.java index 061d12a..d01d469 100644 --- a/src/com/gitblit/utils/SyndicationUtils.java +++ b/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(); diff --git a/src/com/gitblit/wicket/GitBlitWebApp.java b/src/com/gitblit/wicket/GitBlitWebApp.java index 5d092e5..507de15 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.java +++ b/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) { diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 0630a12..c427dd3 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -314,4 +314,8 @@ gb.allowAuthenticatedDescription = grant restricted access to all authenticated users gb.allowNamedDescription = grant restricted access to named users or teams gb.markdownFailure = Failed to parse Markdown content! -gb.clearCache = clear cache \ No newline at end of file +gb.clearCache = clear cache +gb.projects = projects +gb.project = project +gb.allProjects = all projects +gb.copyToClipboard = copy to clipboard \ No newline at end of file diff --git a/src/com/gitblit/wicket/WicketUtils.java b/src/com/gitblit/wicket/WicketUtils.java index 90f7ee6..e4eb29f 100644 --- a/src/com/gitblit/wicket/WicketUtils.java +++ b/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", ""); } diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java index 234c2a9..f9f90b0 100644 --- a/src/com/gitblit/wicket/pages/BasePage.java +++ b/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; @@ -237,16 +249,98 @@ } 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); } - + public void error(String message, boolean redirect) { 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); diff --git a/src/com/gitblit/wicket/pages/ProjectPage.html b/src/com/gitblit/wicket/pages/ProjectPage.html new file mode 100644 index 0000000..db10329 --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectPage.html @@ -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> \ No newline at end of file diff --git a/src/com/gitblit/wicket/pages/ProjectPage.java b/src/com/gitblit/wicket/pages/ProjectPage.java new file mode 100644 index 0000000..be3cf38 --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectPage.java @@ -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; + } +} diff --git a/src/com/gitblit/wicket/pages/ProjectsPage.html b/src/com/gitblit/wicket/pages/ProjectsPage.html new file mode 100644 index 0000000..528ed48 --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectsPage.html @@ -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> \ No newline at end of file diff --git a/src/com/gitblit/wicket/pages/ProjectsPage.java b/src/com/gitblit/wicket/pages/ProjectsPage.java new file mode 100644 index 0000000..f3c4416 --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectsPage.java @@ -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; + } +} diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.html b/src/com/gitblit/wicket/pages/RepositoryPage.html index 3195a93..de64fce 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.html +++ b/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> diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java index 19a5de2..7e21911 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.java +++ b/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)); diff --git a/src/com/gitblit/wicket/pages/RootPage.java b/src/com/gitblit/wicket/pages/RootPage.java index 40f7aec..4858368 100644 --- a/src/com/gitblit/wicket/pages/RootPage.java +++ b/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; } } diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/com/gitblit/wicket/panels/RepositoriesPanel.html index 99bedc6..5da43e0 100644 --- a/src/com/gitblit/wicket/panels/RepositoriesPanel.html +++ b/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> diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/com/gitblit/wicket/panels/RepositoriesPanel.java index 8c8e1e5..a113e00 100644 --- a/src/com/gitblit/wicket/panels/RepositoriesPanel.java +++ b/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 + ")"; } } diff --git a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html index 32f79de..d7c76f1 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html +++ b/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> diff --git a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java index a98e40a..58df028 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java +++ b/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); diff --git a/tests/com/gitblit/tests/SyndicationUtilsTest.java b/tests/com/gitblit/tests/SyndicationUtilsTest.java index 4542adb..75fbd7c 100644 --- a/tests/com/gitblit/tests/SyndicationUtilsTest.java +++ b/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(); -- Gitblit v1.9.1