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 # 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 src/com/gitblit/models/DailyActivity.java
New file @@ -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); } } src/com/gitblit/models/RepositoryCommit.java
New file @@ -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; } } 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) { src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -181,3 +181,9 @@ gb.subscribe = subscribe gb.branch = branch 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 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", ""); } @@ -404,6 +412,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, "EEEE, MMMM d, yyyy h:mm a z"); src/com/gitblit/wicket/pages/ActivityPage.html
New file @@ -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> src/com/gitblit/wicket/pages/ActivityPage.java
New file @@ -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)); } } 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)); } src/com/gitblit/wicket/panels/ActivityPanel.html
New file @@ -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> src/com/gitblit/wicket/panels/ActivityPanel.java
New file @@ -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); } }