/users.conf\r
*.directory\r
/.gradle
+/projects.conf
# 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
- <context-parameter> *git.repositoryFolder* (set the full path to your repositories folder)\r
- <context-parameter> *groovy.scriptsFolder* (set the full path to your Groovy hook scripts folder)\r
- <context-parameter> *groovy.grapeFolder* (set the full path to your Groovy Grape artifact cache)\r
+ - <context-parameter> *web.projectsFile* (set the full path to your projects metadata file)\r
- <context-parameter> *realm.userService* (set the full path to `users.conf`)\r
- <context-parameter> *git.packedGitLimit* (set larger than the size of your largest repository)\r
- <context-parameter> *git.streamFileThreshold* (set larger than the size of your largest committed file)\r
* 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
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
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
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
\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
private LuceneExecutor luceneExecutor;\r
\r
private TimeZone timezone;\r
+ \r
+ private FileBasedConfig projectConfigs;\r
\r
public GitBlit() {\r
if (gitblit == null) {\r
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
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
*/\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
* @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
}\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
\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
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
}\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
}\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
--- /dev/null
+/*\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
* @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
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
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
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
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
+ ",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
*/\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
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
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
}\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
--- /dev/null
+<!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
--- /dev/null
+/*\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", " ").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
--- /dev/null
+<!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
--- /dev/null
+/*\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
\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
\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
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
\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
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
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
}\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
}\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
}\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
</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
<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
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
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
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
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
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
\r
@Override\r
public String toString() {\r
- return name + " (" + count + ")";\r
+ return StringUtils.isEmpty(title) ? name : title + " (" + count + ")";\r
}\r
}\r
\r
\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
} 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
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