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