From 13a3f5bc3e2d25fc76850f86070dc34efe60d77a Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 7 Sep 2012 22:06:15 -0400 Subject: [PATCH] Draft project pages, project metadata, and RSS feeds This is an in-progress feature to offer an interface for grouped repositories. This may help installations with large numbers of repositories stay organized. It also will be part of a future, more advanced security model. --- .gitignore | 1 + distrib/gitblit.properties | 5 + docs/01_setup.mkd | 1 + docs/05_roadmap.mkd | 2 - resources/clippy.png | Bin 0 -> 561 bytes resources/gitblit.css | 4 + src/com/gitblit/GitBlit.java | 135 +++++ src/com/gitblit/SyndicationFilter.java | 143 +++-- src/com/gitblit/SyndicationServlet.java | 164 ++++-- src/com/gitblit/models/ProjectModel.java | 95 ++++ src/com/gitblit/utils/SyndicationUtils.java | 3 +- src/com/gitblit/wicket/GitBlitWebApp.java | 4 + .../gitblit/wicket/GitBlitWebApp.properties | 6 +- src/com/gitblit/wicket/WicketUtils.java | 8 + src/com/gitblit/wicket/pages/BasePage.java | 100 +++- src/com/gitblit/wicket/pages/ProjectPage.html | 132 +++++ src/com/gitblit/wicket/pages/ProjectPage.java | 502 ++++++++++++++++++ .../gitblit/wicket/pages/ProjectsPage.html | 37 ++ .../gitblit/wicket/pages/ProjectsPage.java | 224 ++++++++ .../gitblit/wicket/pages/RepositoryPage.html | 2 +- .../gitblit/wicket/pages/RepositoryPage.java | 7 + src/com/gitblit/wicket/pages/RootPage.java | 31 +- .../wicket/panels/RepositoriesPanel.html | 5 +- .../wicket/panels/RepositoriesPanel.java | 20 +- .../wicket/panels/RepositoryUrlPanel.html | 9 +- .../wicket/panels/RepositoryUrlPanel.java | 3 +- .../gitblit/tests/SyndicationUtilsTest.java | 2 +- 27 files changed, 1529 insertions(+), 116 deletions(-) create mode 100644 resources/clippy.png create mode 100644 src/com/gitblit/models/ProjectModel.java create mode 100644 src/com/gitblit/wicket/pages/ProjectPage.html create mode 100644 src/com/gitblit/wicket/pages/ProjectPage.java create mode 100644 src/com/gitblit/wicket/pages/ProjectsPage.html create mode 100644 src/com/gitblit/wicket/pages/ProjectsPage.java diff --git a/.gitignore b/.gitignore index 6158fcd6..e93e18e4 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 80cbb7e1..c7f0ae35 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -290,6 +290,11 @@ web.authenticateAdminPages = true # 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 42f870f8..eaaf3be5 100644 --- a/docs/01_setup.mkd +++ b/docs/01_setup.mkd @@ -9,6 +9,7 @@ Open `web.xml` in your favorite text editor and make sure to review and set: - <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 6b4def42..3238f732 100644 --- a/docs/05_roadmap.mkd +++ b/docs/05_roadmap.mkd @@ -30,7 +30,5 @@ This list is volatile. * 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 0000000000000000000000000000000000000000..7a462e16629b6fc9138e4a694286cba910e3a66e GIT binary patch literal 561 zcmV-10?z%3P)9>OcY(LCzEs5cG115DjO%#LH%9 zXLp@hNic|zf#1wK^S;l#zkxC8^?DU}rgXq+4_+BSr((wD2nIc*Iuuev~8R9j2#{3 z{`1{Rr4p)EheHiMYBrlMl&-G$$xk)&TYv!oZ{Yz_rx7@200000NkvXXu0mjfq=x@b literal 0 HcmV?d00001 diff --git a/resources/gitblit.css b/resources/gitblit.css index b51637d2..7a73a24c 100644 --- a/resources/gitblit.css +++ b/resources/gitblit.css @@ -734,6 +734,10 @@ table.repositories tr.group td { 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 e6effc20..c7586544 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -37,6 +37,7 @@ import java.util.Map; 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.FederationModel; 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 @@ public class GitBlit implements ServletContextListener { private final Map repositoryListCache = new ConcurrentHashMap(); + private final Map projectCache = new ConcurrentHashMap(); + private final AtomicReference repositoryListSettingsChecksum = new AtomicReference(""); private RepositoryResolver repositoryResolver; @@ -153,6 +157,8 @@ public class GitBlit implements ServletContextListener { private LuceneExecutor luceneExecutor; private TimeZone timezone; + + private FileBasedConfig projectConfigs; public GitBlit() { if (gitblit == null) { @@ -1020,6 +1026,130 @@ public class GitBlit implements ServletContextListener { 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 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 configs = new HashMap(); + // 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 getProjectModels(UserModel user) { + Map configs = getProjectConfigs(); + + // per-user project lists, this accounts for security and visibility + Map map = new TreeMap(); + // 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 projects = new ArrayList(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 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; + } + /** * Workaround JGit. I need to access the raw config object directly in order * to see if the config is dirty so that I can reload a repository model. @@ -2180,6 +2310,11 @@ public class GitBlit implements ServletContextListener { 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 08265666..0dff1c87 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 @@ public class SyndicationFilter extends AccessRestrictionFilter { * @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 @@ public class SyndicationFilter extends AccessRestrictionFilter { } /** - * 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 81cfb768..4c542b6d 100644 --- a/src/com/gitblit/SyndicationServlet.java +++ b/src/com/gitblit/SyndicationServlet.java @@ -17,6 +17,8 @@ package com.gitblit; 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.eclipse.jgit.revwalk.RevCommit; 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 @@ public class SyndicationServlet extends HttpServlet { } response.setContentType("application/rss+xml; charset=UTF-8"); - Repository repository = GitBlit.self().getRepository(repositoryName); - RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName); - List 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 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(project.repositories); + + // project feed + feedName = project.name; + feedTitle = project.title; + feedDescription = project.description; + } } - Map> allRefs = JGitUtils.getAllRefs(repository); - List entries = new ArrayList(); + + 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 @@ public class SyndicationServlet extends HttpServlet { } 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(); + + List entries = new ArrayList(); + + 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 refs = allRefs.get(commit.getId()); - if (refs != null && refs.size() > 0) { - for (RefModel ref : refs) { - entry.tags.add("ref:" + ref.getName()); + List 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> 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(); + + // 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 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 00000000..bc359037 --- /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 { + + 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 repositories = new HashSet(); + + 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 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 061d12a4..d01d4691 100644 --- a/src/com/gitblit/utils/SyndicationUtils.java +++ b/src/com/gitblit/utils/SyndicationUtils.java @@ -56,14 +56,13 @@ public class SyndicationUtils { * @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 entryModels, OutputStream os) + List 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 5d092e56..507de15d 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/com/gitblit/wicket/GitBlitWebApp.java @@ -42,6 +42,8 @@ import com.gitblit.wicket.pages.LuceneSearchPage; 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 @@ public class GitBlitWebApp extends WebApplication { 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 clazz, String... parameters) { diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 0630a12e..c427dd34 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -314,4 +314,8 @@ gb.authorizationControl = authorization control 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 90f7ee68..e4eb29fb 100644 --- a/src/com/gitblit/wicket/WicketUtils.java +++ b/src/com/gitblit/wicket/WicketUtils.java @@ -276,6 +276,10 @@ public class WicketUtils { 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 @@ public class WicketUtils { + ",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 234c2a94..f9f90b0f 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.RequestUtils; 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.AccessRestrictionType; 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 @@ public abstract class BasePage extends WebPage { } return sb.toString(); } + + protected List getProjectModels() { + final UserModel user = GitBlitWebSession.get().getUser(); + List projects = GitBlit.self().getProjectModels(user); + return projects; + } + + protected List 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 availableModels = getProjectModels(); + Set models = new HashSet(); + + 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 teams = StringUtils.getStringsFromValue(team, ","); + + // need TeamModels first + List teamModels = new ArrayList(); + 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 timeFiltered = new HashSet(); + for (ProjectModel model : models) { + if (model.lastChange.after(threshold)) { + timeFiltered.add(model); + } + } + models = timeFiltered; + } + + List list = new ArrayList(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 00000000..db10329b --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectPage.html @@ -0,0 +1,132 @@ + + + + + + + + + + | + | [edit] + | [delete] + + + + + + + | + | [edit] + + + + + + + | + + + +
+
+

+ + + +

+
[project message]
+
+
+ +
+ + + + +
+ + +
+ +
+
+
[repositories message]
+
+
+ + +
+
+
+
+ + +
+ + + + + + + + + + + + +
+ [owner] +
+ +

+ [repository name] + +

+ +
+ +
[repository description]
+ +
+ [last change] [last change], + [repository size] +
+ +
[repository clone url]
+
+
+
+
+
+ + +
+
+

/ [days back]

+
+ +
+ + + + + + +
+
+ +
[activity panel]
+
+ +
+
+
+ + \ 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 00000000..be3cf389 --- /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 projectModels = new ArrayList(); + + 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 repositories = getRepositories(params); + + Collections.sort(repositories, new Comparator() { + @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 dp = new ListDataProvider(repositories); + DataView dataView = new DataView("repository", dp) { + private static final long serialVersionUID = 1L; + + public void populateItem(final Item 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("tickets", TicketsPage.class, pp).setVisible(entry.useTickets)); + item.add(new BookmarkablePageLink("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("editRepository", + EditRepositoryPage.class, WicketUtils + .newRepositoryParameter(entry.name))); + if (showAdmin) { + Link deleteLink = new Link("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("tree", TreePage.class, + WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits)); + + repositoryLinks.add(new BookmarkablePageLink("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 repositoryUrls = new ArrayList(); + 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 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 uniqueAuthors = new HashSet(); + 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 recentActivity) { + // activity metrics + Map repositoryMetrics = new HashMap(); + Map authorMetrics = new HashMap(); + + // aggregate repository and author metrics + for (Activity activity : recentActivity) { + + // aggregate author metrics + for (Map.Entry 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 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 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 getProjectModels() { + if (projectModels.isEmpty()) { + final UserModel user = GitBlitWebSession.get().getUser(); + List 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 getProjectsMenu() { + List menu = new ArrayList(); + List projects = getProjectModels(); + int maxProjects = 15; + boolean showAllProjects = projects.size() > maxProjects; + if (showAllProjects) { + + // sort by last changed + Collections.sort(projects, new Comparator() { + @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 00000000..528ed48f --- /dev/null +++ b/src/com/gitblit/wicket/pages/ProjectsPage.html @@ -0,0 +1,37 @@ + + + + + +
[projects message]
+ + + + + + + + + + + + + + + + + + + + +
+ + Project + DescriptionRepositoriesLast Change
[project title][project description][repository count][last change]
+ +
+ + \ 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 00000000..f3c4416e --- /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 projectModels = new ArrayList(); + + 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 projects = getProjects(params); + + ListDataProvider dp = new ListDataProvider(projects); + + DataView dataView = new DataView("project", dp) { + private static final long serialVersionUID = 1L; + int counter; + + @Override + protected void onBeforeRender() { + super.onBeforeRender(); + counter = 0; + } + + public void populateItem(final Item 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 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 3195a937..de64fce0 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.html +++ b/src/com/gitblit/wicket/pages/RepositoryPage.html @@ -21,7 +21,7 @@ - + diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java index 19a5de2c..7e21911b 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/com/gitblit/wicket/pages/RepositoryPage.java @@ -64,6 +64,7 @@ import com.gitblit.wicket.panels.RefsPanel; public abstract class RepositoryPage extends BasePage { + protected final String projectName; protected final String repositoryName; protected final String objectId; @@ -78,6 +79,11 @@ public abstract class RepositoryPage extends BasePage { 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 @@ public abstract class RepositoryPage extends BasePage { // 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 40f7aec4..48583685 100644 --- a/src/com/gitblit/wicket/pages/RootPage.java +++ b/src/com/gitblit/wicket/pages/RootPage.java @@ -178,6 +178,9 @@ public abstract class RootPage extends BasePage { 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 @@ public abstract class RootPage extends BasePage { final UserModel user = GitBlitWebSession.get().getUser(); List repositories = GitBlit.self().getRepositoryModels(user); repositoryModels.addAll(repositories); + Collections.sort(repositoryModels); } return repositoryModels; } @@ -322,6 +326,7 @@ public abstract class RootPage extends BasePage { } boolean hasParameter = false; + String projectName = WicketUtils.getProjectName(params); String repositoryName = WicketUtils.getRepositoryName(params); String set = WicketUtils.getSet(params); String regex = WicketUtils.getRegEx(params); @@ -342,6 +347,27 @@ public abstract class RootPage extends BasePage { } } + 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); + } + } + } + } + if (!StringUtils.isEmpty(regex)) { // filter the repositories by the regex hasParameter = true; @@ -411,6 +437,9 @@ public abstract class RootPage extends BasePage { } models = timeFiltered; } - return new ArrayList(models); + + List list = new ArrayList(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 99bedc63..5da43e00 100644 --- a/src/com/gitblit/wicket/panels/RepositoriesPanel.html +++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.html @@ -71,7 +71,8 @@ - [group name] + [group name] + [description] @@ -84,7 +85,7 @@ - + diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/com/gitblit/wicket/panels/RepositoriesPanel.java index 8c8e1e5e..a113e006 100644 --- a/src/com/gitblit/wicket/panels/RepositoriesPanel.java +++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.java @@ -46,6 +46,7 @@ import com.gitblit.Constants.AccessRestrictionType; 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.WicketUtils; 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 @@ public class RepositoriesPanel extends BasePanel { roots.add(0, rootPath); groups.put(rootPath, rootRepositories); } + + Map projects = new HashMap(); + for (ProjectModel project : GitBlit.self().getProjectModels(user)) { + projects.put(project.name, project); + } List groupedModels = new ArrayList(); for (String root : roots) { List 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 @@ public class RepositoriesPanel extends BasePanel { 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 @@ public class RepositoriesPanel extends BasePanel { private static final long serialVersionUID = 1L; int count; + String title; GroupRepositoryModel(String name, int count) { super(name, "", "", new Date(0)); @@ -334,7 +348,7 @@ public class RepositoriesPanel extends BasePanel { @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 32f79de4..d7c76f13 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html +++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html @@ -9,20 +9,21 @@ - - + + - diff --git a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java index a98e40ab..58df028b 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java +++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java @@ -42,8 +42,7 @@ public class RepositoryUrlPanel extends BasePanel { } 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 4542adbd..75fbd7ca 100644 --- a/tests/com/gitblit/tests/SyndicationUtilsTest.java +++ b/tests/com/gitblit/tests/SyndicationUtilsTest.java @@ -54,7 +54,7 @@ public class SyndicationUtilsTest { 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(); -- 2.39.5