]> source.dussan.org Git - gitblit.git/commitdiff
Draft project pages, project metadata, and RSS feeds
authorJames Moger <james.moger@gitblit.com>
Sat, 8 Sep 2012 02:06:15 +0000 (22:06 -0400)
committerJames Moger <james.moger@gitblit.com>
Sat, 8 Sep 2012 02:06:15 +0000 (22:06 -0400)
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.

27 files changed:
.gitignore
distrib/gitblit.properties
docs/01_setup.mkd
docs/05_roadmap.mkd
resources/clippy.png [new file with mode: 0644]
resources/gitblit.css
src/com/gitblit/GitBlit.java
src/com/gitblit/SyndicationFilter.java
src/com/gitblit/SyndicationServlet.java
src/com/gitblit/models/ProjectModel.java [new file with mode: 0644]
src/com/gitblit/utils/SyndicationUtils.java
src/com/gitblit/wicket/GitBlitWebApp.java
src/com/gitblit/wicket/GitBlitWebApp.properties
src/com/gitblit/wicket/WicketUtils.java
src/com/gitblit/wicket/pages/BasePage.java
src/com/gitblit/wicket/pages/ProjectPage.html [new file with mode: 0644]
src/com/gitblit/wicket/pages/ProjectPage.java [new file with mode: 0644]
src/com/gitblit/wicket/pages/ProjectsPage.html [new file with mode: 0644]
src/com/gitblit/wicket/pages/ProjectsPage.java [new file with mode: 0644]
src/com/gitblit/wicket/pages/RepositoryPage.html
src/com/gitblit/wicket/pages/RepositoryPage.java
src/com/gitblit/wicket/pages/RootPage.java
src/com/gitblit/wicket/panels/RepositoriesPanel.html
src/com/gitblit/wicket/panels/RepositoriesPanel.java
src/com/gitblit/wicket/panels/RepositoryUrlPanel.html
src/com/gitblit/wicket/panels/RepositoryUrlPanel.java
tests/com/gitblit/tests/SyndicationUtilsTest.java

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