From 6e6f9fe4d0c42a40ba7981606fb526391be23f6d Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 16 Nov 2011 22:43:37 -0500
Subject: [PATCH] Working draft of the aggregate activity page

---
 src/com/gitblit/models/DailyActivity.java        |   47 +++
 src/com/gitblit/wicket/GitBlitWebApp.properties  |    8 
 src/com/gitblit/wicket/WicketUtils.java          |   59 ++++
 src/com/gitblit/wicket/panels/ActivityPanel.java |  142 +++++++++++
 docs/04_releases.mkd                             |    5 
 src/com/gitblit/wicket/pages/ActivityPage.html   |   19 +
 src/com/gitblit/wicket/pages/RootPage.java       |    1 
 src/com/gitblit/models/RepositoryCommit.java     |   86 +++++++
 distrib/gitblit.properties                       |   17 +
 src/com/gitblit/wicket/GitBlitWebApp.java        |    3 
 src/com/gitblit/wicket/panels/ActivityPanel.html |   38 +++
 src/com/gitblit/wicket/pages/ActivityPage.java   |  260 +++++++++++++++++++++
 12 files changed, 684 insertions(+), 1 deletions(-)

diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 083a658..5c50a2c 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -161,11 +161,22 @@
 # RESTART REQUIRED
 web.useClientTimezone = false
 
+# Time format
+# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
+#
+# SINCE 0.8.0
+web.timeFormat = HH:mm
+
 # Short date format
 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
 #
 # SINCE 0.5.0
 web.datestampShortFormat = yyyy-MM-dd
+
+# Long date format
+#
+# SINCE 0.8.0
+web.datestampLongFormat = EEEE, MMMM d, yyyy
 
 # Long timestamp format
 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
@@ -239,6 +250,12 @@
 # SINCE 0.5.0 
 web.generateActivityGraph = true
 
+# The number of days to show on the activity page.
+# Value must exceed 0 else default of 14 is used
+#
+# SINCE 0.8.0
+web.activityDuration = 14
+
 # The number of commits to display on the summary page
 # Value must exceed 0 else default of 20 is used
 #
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 5719b19..22ecce7 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -5,6 +5,11 @@
 
 - added: optional Gravatar integration  
     **New:** *web.allowGravatar = true*   
+- added: multi-repository activity page.  this is a timeline of commit activity over the last N days for one or more repositories.
+   **New:** *web.activityDuration = 14*  
+   **New:** *web.timeFormat = HH:mm*  
+   **New:** *web.datestampLongFormat = EEEE, MMMM d, yyyy*  
+
 
 ### Older Releases
 
diff --git a/src/com/gitblit/models/DailyActivity.java b/src/com/gitblit/models/DailyActivity.java
new file mode 100644
index 0000000..f2e816c
--- /dev/null
+++ b/src/com/gitblit/models/DailyActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2011 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.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Model class to represent the commit activity across many repositories. This
+ * class is used by the Activity page.
+ * 
+ * @author James Moger
+ */
+public class DailyActivity implements Serializable, Comparable<DailyActivity> {
+
+	private static final long serialVersionUID = 1L;
+
+	public final Date date;
+
+	public final List<RepositoryCommit> commits;
+
+	public DailyActivity(Date date) {
+		this.date = date;
+		commits = new ArrayList<RepositoryCommit>();
+	}
+
+	@Override
+	public int compareTo(DailyActivity o) {
+		// reverse chronological order
+		return o.date.compareTo(date);
+	}
+}
diff --git a/src/com/gitblit/models/RepositoryCommit.java b/src/com/gitblit/models/RepositoryCommit.java
new file mode 100644
index 0000000..8f5b2bd
--- /dev/null
+++ b/src/com/gitblit/models/RepositoryCommit.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 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.List;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Model class to represent a RevCommit, it's source repository, and the branch.
+ * This class is used by the activity page.
+ * 
+ * @author James Moger
+ */
+public class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
+
+	private static final long serialVersionUID = 1L;
+
+	public final String repository;
+
+	public final String branch;
+
+	private final RevCommit commit;
+
+	private List<RefModel> refs;
+
+	public RepositoryCommit(String repository, String branch, RevCommit commit) {
+		this.repository = repository;
+		this.branch = branch;
+		this.commit = commit;
+	}
+
+	public void setRefs(List<RefModel> refs) {
+		this.refs = refs;
+	}
+
+	public List<RefModel> getRefs() {
+		return refs;
+	}
+
+	public String getName() {
+		return commit.getName();
+	}
+
+	public String getShortName() {
+		return commit.getName().substring(0, 8);
+	}
+
+	public String getShortMessage() {
+		return commit.getShortMessage();
+	}
+
+	public int getParentCount() {
+		return commit.getParentCount();
+	}
+
+	public PersonIdent getAuthorIdent() {
+		return commit.getAuthorIdent();
+	}
+
+	@Override
+	public int compareTo(RepositoryCommit o) {
+		// reverse-chronological order
+		if (commit.getCommitTime() > o.commit.getCommitTime()) {
+			return -1;
+		} else if (commit.getCommitTime() < o.commit.getCommitTime()) {
+			return 1;
+		}
+		return 0;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.java b/src/com/gitblit/wicket/GitBlitWebApp.java
index 301adb3..79083ef 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/com/gitblit/wicket/GitBlitWebApp.java
@@ -25,6 +25,7 @@
 
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
+import com.gitblit.wicket.pages.ActivityPage;
 import com.gitblit.wicket.pages.BlamePage;
 import com.gitblit.wicket.pages.BlobDiffPage;
 import com.gitblit.wicket.pages.BlobPage;
@@ -103,6 +104,8 @@
 		// federation urls
 		mount("/proposal", ReviewProposalPage.class, "t");
 		mount("/registration", FederationRegistrationPage.class, "u", "n");
+		
+		mount("/activity", ActivityPage.class, "r", "h");
 	}
 
 	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 5193843..f157a7e 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -180,4 +180,10 @@
 gb.activity = activity
 gb.subscribe = subscribe
 gb.branch = branch
-gb.maxHits = max hits
\ No newline at end of file
+gb.maxHits = max hits
+gb.recentActivity = recent activity
+gb.recentActivitySubheader = last {0} days / {1} commits by {2} authors
+gb.dailyActivity = daily activity
+gb.activeRepositories = active repositories
+gb.activeAuthors = active authors
+gb.commits = commits
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/WicketUtils.java b/src/com/gitblit/wicket/WicketUtils.java
index 37b4447..6157a44 100644
--- a/src/com/gitblit/wicket/WicketUtils.java
+++ b/src/com/gitblit/wicket/WicketUtils.java
@@ -358,6 +358,14 @@
 		return params.getInt("pg", 1);
 	}
 
+	public static String getSet(PageParameters params) {
+		return params.getString("set", "");
+	}
+
+	public static int getDaysBack(PageParameters params) {
+		return params.getInt("db", 14);
+	}
+
 	public static String getUsername(PageParameters params) {
 		return params.getString("user", "");
 	}
@@ -403,6 +411,57 @@
 		}
 		return label;
 	}
+	
+	public static Label createTimeLabel(String wicketId, Date date, TimeZone timeZone) {
+		String format = GitBlit.getString(Keys.web.timeFormat, "HH:mm");
+		DateFormat df = new SimpleDateFormat(format);
+		if (timeZone != null) {
+			df.setTimeZone(timeZone);
+		}
+		String timeString;
+		if (date.getTime() == 0) {
+			timeString = "--";
+		} else {
+			timeString = df.format(date);
+		}
+		String title = TimeUtils.timeAgo(date);
+		Label label = new Label(wicketId, timeString);
+		WicketUtils.setCssClass(label, TimeUtils.timeAgoCss(date));
+		if (!StringUtils.isEmpty(title)) {
+			WicketUtils.setHtmlTooltip(label, title);
+		}
+		return label;
+	}
+	
+	public static Label createDatestampLabel(String wicketId, Date date, TimeZone timeZone) {
+		String format = GitBlit.getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
+		DateFormat df = new SimpleDateFormat(format);
+		if (timeZone != null) {
+			df.setTimeZone(timeZone);
+		}
+		String dateString;
+		if (date.getTime() == 0) {
+			dateString = "--";
+		} else {
+			dateString = df.format(date);
+		}
+		String title = null;
+		if (date.getTime() <= System.currentTimeMillis()) {
+			// past
+			title = TimeUtils.timeAgo(date);
+		}
+		if ((System.currentTimeMillis() - date.getTime()) < 10 * 24 * 60 * 60 * 1000L) {
+			String tmp = dateString;
+			dateString = title;
+			title = tmp;
+		}
+		Label label = new Label(wicketId, dateString);
+		WicketUtils.setCssClass(label, TimeUtils.timeAgoCss(date));
+		if (!StringUtils.isEmpty(title)) {
+			WicketUtils.setHtmlTooltip(label, title);
+		}
+		return label;
+	}
 
 	public static Label createTimestampLabel(String wicketId, Date date, TimeZone timeZone) {
 		String format = GitBlit.getString(Keys.web.datetimestampLongFormat,
diff --git a/src/com/gitblit/wicket/pages/ActivityPage.html b/src/com/gitblit/wicket/pages/ActivityPage.html
new file mode 100644
index 0000000..c86028f
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/ActivityPage.html
@@ -0,0 +1,19 @@
+<!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="page-header">
+		<h2><wicket:message key="gb.recentActivity"></wicket:message><small> / <span wicket:id="subheader">[days back]</span></small></h2>
+	</div>
+	<div style="text-align: center;">
+		<span id="chartDaily"></span>		
+		<span id="chartRepositories"></span>
+		<span id="chartAuthors"></span>
+	</div>
+	<div wicket:id="activityPanel" style="padding-top:5px;" >[activity panel]</div>
+</wicket:extend>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/ActivityPage.java b/src/com/gitblit/wicket/pages/ActivityPage.java
new file mode 100644
index 0000000..53c7f41
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/ActivityPage.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2011 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.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.DailyActivity;
+import com.gitblit.models.Metric;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+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;
+
+/**
+ * Activity Page shows a list of recent commits across all visible Gitblit
+ * repositories.
+ * 
+ * @author James Moger
+ * 
+ */
+public class ActivityPage extends RootPage {
+
+	public ActivityPage(PageParameters params) {
+		super();
+		setupPage("", "");
+		final UserModel user = GitBlitWebSession.get().getUser();
+
+		// parameters
+		int daysBack = WicketUtils.getDaysBack(params);
+		if (daysBack < 1) {
+			daysBack = 14;
+		}		
+		String set = WicketUtils.getSet(params);
+		String repositoryName = WicketUtils.getRepositoryName(params);
+		String objectId = WicketUtils.getObject(params);
+
+		List<RepositoryModel> models = null;
+		if (!StringUtils.isEmpty(repositoryName)) {
+			// named repository
+			models = new ArrayList<RepositoryModel>();
+			RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+			if (user.canAccessRepository(model)) {
+				models.add(model);
+			}
+		}
+
+		// get all user accessible repositories
+		if (models == null) {
+			models = GitBlit.self().getRepositoryModels(user);
+		}
+
+		// filter the repositories by the specified set
+		if (!StringUtils.isEmpty(set)) {
+			List<String> sets = StringUtils.getStringsFromValue(set, ",");
+			List<RepositoryModel> setModels = new ArrayList<RepositoryModel>();
+			for (RepositoryModel model : models) {
+				for (String curr : sets) {
+					if (model.federationSets.contains(curr)) {
+						setModels.add(model);
+					}
+				}
+			}
+			models = setModels;
+		}
+
+		// Activity panel shows last daysBack of activity across all
+		// repositories.
+		Date thresholdDate = new Date(System.currentTimeMillis() - daysBack * TimeUtils.ONEDAY);
+
+		// Build a map of DailyActivity from the available repositories for the
+		// specified threshold date.
+		DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+		Calendar cal = Calendar.getInstance();
+
+		Map<String, DailyActivity> activity = new HashMap<String, DailyActivity>();
+		for (RepositoryModel model : models) {
+			if (model.hasCommits && model.lastChange.after(thresholdDate)) {
+				Repository repository = GitBlit.self().getRepository(model.name);
+				List<RevCommit> commits = JGitUtils.getRevLog(repository, objectId, thresholdDate);
+				Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
+				repository.close();
+
+				// determine commit branch
+				String branch = objectId;
+				if (StringUtils.isEmpty(branch)) {
+					List<RefModel> headRefs = allRefs.get(commits.get(0).getId());
+					List<String> localBranches = new ArrayList<String>();
+					for (RefModel ref : headRefs) {
+						if (ref.getName().startsWith(Constants.R_HEADS)) {
+							localBranches.add(ref.getName().substring(Constants.R_HEADS.length()));
+						}
+					}
+					// determine branch
+					if (localBranches.size() == 1) {
+						// only one branch, choose it
+						branch = localBranches.get(0);
+					} else if (localBranches.size() > 1) {
+						if (localBranches.contains("master")) {
+							// choose master
+							branch = "master";
+						} else {
+							// choose first branch
+							branch = localBranches.get(0);
+						}
+					}
+				}
+
+				for (RevCommit commit : commits) {
+					Date date = JGitUtils.getCommitDate(commit);
+					String dateStr = df.format(date);
+					if (!activity.containsKey(dateStr)) {
+						// Normalize the date to midnight
+						cal.setTime(date);
+						cal.set(Calendar.HOUR_OF_DAY, 0);
+						cal.set(Calendar.MINUTE, 0);
+						cal.set(Calendar.SECOND, 0);
+						cal.set(Calendar.MILLISECOND, 0);
+						activity.put(dateStr, new DailyActivity(cal.getTime()));
+					}
+					RepositoryCommit commitModel = new RepositoryCommit(model.name, branch, commit);
+					commitModel.setRefs(allRefs.get(commit.getId()));
+					activity.get(dateStr).commits.add(commitModel);
+				}
+			}
+		}
+
+		// activity metrics
+		Map<String, Metric> dayMetrics = new HashMap<String, Metric>();
+		Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
+		Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
+
+		// prepare day metrics
+		cal.setTimeInMillis(System.currentTimeMillis());
+		for (int i = 0; i < daysBack; i++) {
+			cal.add(Calendar.DATE, -1);
+			String key = df.format(cal.getTime());
+			dayMetrics.put(key, new Metric(key));
+		}
+
+		// calculate activity metrics
+		for (Map.Entry<String, DailyActivity> entry : activity.entrySet()) {
+			// day metrics
+			Metric day = dayMetrics.get(entry.getKey());
+			day.count = entry.getValue().commits.size();
+
+			for (RepositoryCommit commit : entry.getValue().commits) {
+				// repository metrics
+				String repository = commit.repository;
+				if (!repositoryMetrics.containsKey(repository)) {
+					repositoryMetrics.put(repository, new Metric(repository));
+				}
+				repositoryMetrics.get(repository).count++;
+
+				// author metrics
+				String author = commit.getAuthorIdent().getEmailAddress().toLowerCase();
+				if (!authorMetrics.containsKey(author)) {
+					authorMetrics.put(author, new Metric(author));
+				}
+				authorMetrics.get(author).count++;
+			}
+		}
+
+		// sort the activity groups and their commit contents
+		int totalCommits = 0;
+		List<DailyActivity> recentActivity = new ArrayList<DailyActivity>(activity.values());
+		for (DailyActivity daily : recentActivity) {
+			Collections.sort(daily.commits);
+			totalCommits += daily.commits.size();
+		}
+
+		// 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"));
+		df = new SimpleDateFormat("MMM dd");
+		for (DailyActivity metric : recentActivity) {
+			chart.addValue(df.format(metric.date), metric.commits.size());
+		}
+		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);
+
+		add(new HeaderContributor(charts));
+
+		add(new Label("subheader", MessageFormat.format(getString("gb.recentActivitySubheader"),
+				daysBack, totalCommits, authorMetrics.size())));
+
+		// add activity panel
+		add(new ActivityPanel("activityPanel", recentActivity));
+	}
+}
diff --git a/src/com/gitblit/wicket/pages/RootPage.java b/src/com/gitblit/wicket/pages/RootPage.java
index f00c041..06ab298 100644
--- a/src/com/gitblit/wicket/pages/RootPage.java
+++ b/src/com/gitblit/wicket/pages/RootPage.java
@@ -82,6 +82,7 @@
 		// navigation links
 		List<PageRegistration> pages = new ArrayList<PageRegistration>();
 		pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class));
+		pages.add(new PageRegistration("gb.activity", ActivityPage.class));
 		if (showAdmin) {
 			pages.add(new PageRegistration("gb.users", UsersPage.class));
 		}
diff --git a/src/com/gitblit/wicket/panels/ActivityPanel.html b/src/com/gitblit/wicket/panels/ActivityPanel.html
new file mode 100644
index 0000000..703dc00
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/ActivityPanel.html
@@ -0,0 +1,38 @@
+<!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:panel>
+
+	<div wicket:id="activity">
+		<div class="header"><span wicket:id="title">[title]</span></div>
+		<table wicket:id="commits">
+			<tr wicket:id="commit"></tr>		
+		</table>	
+	</div>
+	
+	<wicket:fragment wicket:id="commitFragment">
+		<td class="date" style="width:40px; vertical-align: middle;" ><span wicket:id="time">[time of day]</span></td>
+		<td style="width:30px;vertical-align: middle;"><img wicket:id="avatar" style="vertical-align: middle;"></img></td>
+		<td class="author" style="vertical-align: middle;">
+			<img wicket:id="commitIcon" style="vertical-align: middle;"></img>
+			<span wicket:id="message">[shortlog commit link]</span><br/>
+			<span wicket:id="author" style="padding-left:20px;">[author link]</span> committed <span wicket:id="commitid">[commit id]</span> to <span wicket:id="branch"></span>
+		</td>
+		<td style="text-align:right;vertical-align: middle;">
+			<div wicket:id="commitRefs">[commit refs]</div>
+			<span wicket:id="repository">[repository link]</span>
+		</td>
+		<td class="rightAlign" style="width:7em;vertical-align: middle;">
+        	<span class="link">
+				<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+			</span>
+		</td>		
+	</wicket:fragment>
+	
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/ActivityPanel.java b/src/com/gitblit/wicket/panels/ActivityPanel.java
new file mode 100644
index 0000000..128ef2b
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/ActivityPanel.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2011 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.panels;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+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 com.gitblit.Constants;
+import com.gitblit.models.DailyActivity;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.GravatarImage;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.SearchPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TreePage;
+
+/**
+ * Renders activity in day-blocks in reverse-chronological order.
+ * 
+ * @author James Moger
+ * 
+ */
+public class ActivityPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public ActivityPanel(String wicketId, List<DailyActivity> recentActivity) {
+		super(wicketId);
+
+		Collections.sort(recentActivity);
+
+		DataView<DailyActivity> activityView = new DataView<DailyActivity>("activity",
+				new ListDataProvider<DailyActivity>(recentActivity)) {
+			private static final long serialVersionUID = 1L;
+
+			public void populateItem(final Item<DailyActivity> item) {
+				final DailyActivity entry = item.getModelObject();
+				item.add(WicketUtils.createDatestampLabel("title", entry.date, GitBlitWebSession
+						.get().getTimezone()));
+
+				// display the commits in chronological order
+				DataView<RepositoryCommit> commits = new DataView<RepositoryCommit>("commits",
+						new ListDataProvider<RepositoryCommit>(entry.commits)) {
+					private static final long serialVersionUID = 1L;
+
+					public void populateItem(final Item<RepositoryCommit> item) {
+						final RepositoryCommit commit = item.getModelObject();
+						Fragment fragment = new Fragment("commit", "commitFragment", this);
+
+						// time of day
+						fragment.add(WicketUtils.createTimeLabel("time", commit.getAuthorIdent()
+								.getWhen(), GitBlitWebSession.get().getTimezone()));
+
+						// avatar
+						fragment.add(new GravatarImage("avatar", commit.getAuthorIdent(), 36));
+
+						// merge icon
+						if (commit.getParentCount() > 1) {
+							fragment.add(WicketUtils.newImage("commitIcon",
+									"commit_merge_16x16.png"));
+						} else {
+							fragment.add(WicketUtils.newBlankImage("commitIcon"));
+						}
+
+						// author search link
+						String author = commit.getAuthorIdent().getName();
+						LinkPanel authorLink = new LinkPanel("author", "list", author,
+								SearchPage.class, WicketUtils.newSearchParameter(commit.repository,
+										commit.getName(), author, Constants.SearchType.AUTHOR));
+						setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+						fragment.add(authorLink);
+
+						// repository summary page link
+						LinkPanel repositoryLink = new LinkPanel("repository", "list",
+								commit.repository, SummaryPage.class,
+								WicketUtils.newRepositoryParameter(commit.repository));
+						fragment.add(repositoryLink);
+
+						// repository branch
+						LinkPanel branchLink = new LinkPanel("branch", "list", commit.branch,
+								LogPage.class, WicketUtils.newObjectParameter(commit.repository,
+										commit.branch));
+						WicketUtils.setCssStyle(branchLink, "color: #008000;");
+						fragment.add(branchLink);
+
+						LinkPanel commitid = new LinkPanel("commitid", "list subject",
+								commit.getShortName(), CommitPage.class,
+								WicketUtils.newObjectParameter(commit.repository, commit.getName()));
+						fragment.add(commitid);
+
+						// message/commit link
+						String shortMessage = commit.getShortMessage();
+						LinkPanel shortlog = new LinkPanel("message", "list subject", shortMessage,
+								CommitPage.class, WicketUtils.newObjectParameter(commit.repository,
+										commit.getName()));
+						fragment.add(shortlog);
+
+						// refs
+						fragment.add(new RefsPanel("commitRefs", commit.repository, commit
+								.getRefs()));
+
+						// view, diff, tree links
+						fragment.add(new BookmarkablePageLink<Void>("view", CommitPage.class,
+								WicketUtils.newObjectParameter(commit.repository, commit.getName())));
+						fragment.add(new BookmarkablePageLink<Void>("diff", CommitDiffPage.class,
+								WicketUtils.newObjectParameter(commit.repository, commit.getName()))
+								.setEnabled(commit.getParentCount() > 0));
+						fragment.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
+								WicketUtils.newObjectParameter(commit.repository, commit.getName())));
+
+						item.add(fragment);
+					}
+				};
+				item.add(commits);
+			}
+		};
+		add(activityView);
+	}
+}

--
Gitblit v1.9.1