Browse Source

Implemented a graph servlet based on EGit/JGit's PlotWalk (issue-194)

The graph is generated server-side and therefore requires that the
commit table row height be fixed and match the row height of the
servlet.  There will be layout misalignment if remotes refs are
displayed.  Perhaps this can be improved in the future.

Change-Id: I39d0ffc7b1c3679976ce8c198c772ff86238f1a5
tags/v1.4.0
James Moger 10 years ago
parent
commit
f084f46875

+ 2
- 0
releases.moxie View File

@@ -15,10 +15,12 @@ r20: {
- Personal repository prefix (~) is now configurable (issue-265)
- Updated default binary and Lucene ignore extensions
additions:
- Added branch graph image servlet based on EGit's branch graph renderer (issue-194)
- Added setting to control creating a repository as --shared on Unix servers (issue-263)
dependencyChanges: ~
settings:
- { name: 'git.createRepositoriesShared', defaultValue: 'false' }
- { name: 'web.showBranchGraph', defaultValue: 'true' }
contributors:
- James Moger
- Robin Rosenberg

+ 5
- 0
src/main/distrib/data/gitblit.properties View File

@@ -900,6 +900,11 @@ web.showSearchTypeSelection = false
# SINCE 0.5.0
web.generateActivityGraph = true
# Displays the commits branch graph in the summary page and commits/log page.
#
# SINCE 1.4.0
web.showBranchGraph = true
# The default number of days to show on the activity page.
# Value must exceed 0 else default of 7 is used
#

+ 12
- 1
src/main/java/WEB-INF/web.xml View File

@@ -154,6 +154,17 @@
<url-pattern>/logo.png</url-pattern>
</servlet-mapping>
<!-- Branch Graph Servlet
<url-pattern> MUST match:
* Wicket Filter ignorePaths parameter -->
<servlet>
<servlet-name>BranchGraphServlet</servlet-name>
<servlet-class>com.gitblit.BranchGraphServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>BranchGraphServlet</servlet-name>
<url-pattern>/graph/*</url-pattern>
</servlet-mapping>
<!-- Robots.txt Servlet
<url-pattern> MUST match:
@@ -282,7 +293,7 @@
* PagesFilter <url-pattern>
* PagesServlet <url-pattern>
* com.gitblit.Constants.PAGES_PATH -->
<param-value>git/,feed/,zip/,federation/,rpc/,pages/,robots.txt,logo.png,sparkleshare/</param-value>
<param-value>git/,feed/,zip/,federation/,rpc/,pages/,robots.txt,logo.png,graph/,sparkleshare/</param-value>
</init-param>
</filter>
<filter-mapping>

+ 363
- 0
src/main/java/com/gitblit/BranchGraphServlet.java View File

@@ -0,0 +1,363 @@
/*
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright 2013 gitblit.com.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* 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;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.AbstractPlotRenderer;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
/**
* Handles requests for branch graphs
*
* @author James Moger
*
*/
public class BranchGraphServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final int LANE_WIDTH = 14;
// must match tr.commit css height
private static final int ROW_HEIGHT = 24;
private static final int RIGHT_PAD = 2;
private final Stroke[] strokeCache;
public BranchGraphServlet() {
super();
strokeCache = new Stroke[4];
for (int i = 1; i < strokeCache.length; i++)
strokeCache[i] = new BasicStroke(i);
}
/**
* Returns an url to this servlet for the specified parameters.
*
* @param baseURL
* @param repository
* @param objectId
* @param numberCommits
* @return an url
*/
public static String asLink(String baseURL, String repository, String objectId, int numberCommits) {
if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
baseURL = baseURL.substring(0, baseURL.length() - 1);
}
return baseURL + Constants.BRANCH_GRAPH_PATH + "?r=" + repository
+ (objectId == null ? "" : ("&h=" + objectId))
+ (numberCommits > 0 ? ("&l=" + numberCommits) : "");
}
@Override
protected long getLastModified(HttpServletRequest req) {
String repository = req.getParameter("r");
String objectId = req.getParameter("h");
Repository r = null;
try {
r = GitBlit.self().getRepository(repository);
if (StringUtils.isEmpty(objectId)) {
objectId = JGitUtils.getHEADRef(r);
}
RevCommit commit = JGitUtils.getCommit(r, objectId);
return JGitUtils.getCommitDate(commit).getTime();
} finally {
if (r != null) {
r.close();
}
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
InputStream is = null;
Repository r = null;
PlotWalk rw = null;
try {
String repository = request.getParameter("r");
String objectId = request.getParameter("h");
String length = request.getParameter("l");
r = GitBlit.self().getRepository(repository);
rw = new PlotWalk(r);
if (StringUtils.isEmpty(objectId)) {
objectId = JGitUtils.getHEADRef(r);
}
rw.markStart(rw.lookupCommit(r.resolve(objectId)));
// default to the items-per-page setting, unless specified
int maxCommits = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
if (!StringUtils.isEmpty(length)) {
int l = Integer.parseInt(length);
if (l > 0) {
maxCommits = l;
}
}
// fetch the requested commits plus some extra so that the last
// commit displayed *likely* has correct lane assignments
CommitList commitList = new CommitList();
commitList.source(rw);
commitList.fillTo(2*maxCommits);
// determine the appropriate width for the image
int numLanes = 0;
int numCommits = Math.min(maxCommits, commitList.size());
for (int i = 0; i < numCommits; i++) {
PlotCommit<Lane> commit = commitList.get(i);
int pos = commit.getLane().getPosition();
numLanes = Math.max(numLanes, pos + 1);
}
int graphWidth = numLanes * LANE_WIDTH + RIGHT_PAD;
int rowHeight = ROW_HEIGHT;
// create an image buffer and render the lanes
BufferedImage image = new BufferedImage(graphWidth, rowHeight*numCommits, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = null;
try {
g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
LanesRenderer renderer = new LanesRenderer();
for (int i = 0; i < numCommits; i++) {
PlotCommit<Lane> commit = commitList.get(i);
Graphics row = g.create(0, i*rowHeight, graphWidth, rowHeight);
try {
renderer.paint(row, commit, rowHeight, graphWidth);
} finally {
row.dispose();
row = null;
}
}
} finally {
if (g != null) {
g.dispose();
g = null;
}
}
// write the image buffer to the client
response.setContentType("image/png");
if (numCommits > 0) {
response.setHeader("Cache-Control", "public, max-age=60, must-revalidate");
response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commitList.get(0)).getTime());
}
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
os.flush();
image.flush();
image = null;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
is.close();
is = null;
}
if (rw != null) {
rw.dispose();
rw = null;
}
if (r != null) {
r.close();
r = null;
}
}
}
private Stroke stroke(final int width) {
if (width < strokeCache.length)
return strokeCache[width];
return new BasicStroke(width);
}
static class CommitList extends PlotCommitList<Lane> {
final List<Color> laneColors;
final LinkedList<Color> colors;
CommitList() {
laneColors = new ArrayList<Color>();
laneColors.add(new Color(133, 166, 214));
laneColors.add(new Color(221, 205, 93));
laneColors.add(new Color(199, 134, 57));
laneColors.add(new Color(131, 150, 98));
laneColors.add(new Color(197, 123, 127));
laneColors.add(new Color(139, 136, 140));
laneColors.add(new Color(48, 135, 144));
laneColors.add(new Color(190, 93, 66));
laneColors.add(new Color(143, 163, 54));
laneColors.add(new Color(180, 148, 74));
laneColors.add(new Color(101, 101, 217));
laneColors.add(new Color(72, 153, 119));
laneColors.add(new Color(23, 101, 160));
laneColors.add(new Color(132, 164, 118));
laneColors.add(new Color(255, 230, 59));
laneColors.add(new Color(136, 176, 70));
laneColors.add(new Color(255, 138, 1));
laneColors.add(new Color(123, 187, 95));
laneColors.add(new Color(233, 88, 98));
laneColors.add(new Color(93, 158, 254));
laneColors.add(new Color(175, 215, 0));
laneColors.add(new Color(140, 134, 142));
laneColors.add(new Color(232, 168, 21));
laneColors.add(new Color(0, 172, 191));
laneColors.add(new Color(251, 58, 4));
laneColors.add(new Color(63, 64, 255));
laneColors.add(new Color(27, 194, 130));
laneColors.add(new Color(0, 104, 183));
colors = new LinkedList<Color>();
repackColors();
}
private void repackColors() {
colors.addAll(laneColors);
}
@Override
protected Lane createLane() {
final Lane lane = new Lane();
if (colors.isEmpty())
repackColors();
lane.color = colors.removeFirst();
return lane;
}
@Override
protected void recycleLane(final Lane lane) {
colors.add(lane.color);
}
}
static class Lane extends PlotLane {
private static final long serialVersionUID = 1L;
Color color;
@Override
public boolean equals(Object o) {
return super.equals(o) && color.equals(((Lane)o).color);
}
@Override
public int hashCode() {
return super.hashCode() ^ color.hashCode();
}
}
class LanesRenderer extends AbstractPlotRenderer<Lane, Color> implements Serializable {
private static final long serialVersionUID = 1L;
final Color commitDotFill = new Color(220, 220, 220);
final Color commitDotOutline = new Color(110, 110, 110);
transient Graphics2D g;
void paint(Graphics in, PlotCommit<Lane> commit, int h, int w) {
g = (Graphics2D) in.create();
try {
if (commit != null)
paintCommit(commit, h);
} finally {
g.dispose();
g = null;
}
}
@Override
protected void drawLine(Color color, int x1, int y1, int x2, int y2, int width) {
if (y1 == y2) {
x1 -= width / 2;
x2 -= width / 2;
} else if (x1 == x2) {
y1 -= width / 2;
y2 -= width / 2;
}
g.setColor(color);
g.setStroke(stroke(width));
g.drawLine(x1, y1, x2, y2);
}
@Override
protected void drawCommitDot(int x, int y, int w, int h) {
g.setColor(commitDotFill);
g.setStroke(strokeCache[2]);
g.fillOval(x + 2, y + 1, w - 2, h - 2);
g.setColor(commitDotOutline);
g.drawOval(x + 2, y + 1, w - 2, h - 2);
}
@Override
protected void drawBoundaryDot(int x, int y, int w, int h) {
drawCommitDot(x, y, w, h);
}
@Override
protected void drawText(String msg, int x, int y) {
}
@Override
protected Color laneColor(Lane myLane) {
return myLane != null ? myLane.color : Color.black;
}
@Override
protected int drawLabel(int x, int y, Ref ref) {
return 0;
}
}
}

+ 2
- 0
src/main/java/com/gitblit/Constants.java View File

@@ -63,6 +63,8 @@ public class Constants {
public static final String PAGES = "/pages/";
public static final String SPARKLESHARE_INVITE_PATH = "/sparkleshare/";
public static final String BRANCH_GRAPH_PATH = "/graph/";
public static final String BORDER = "***********************************************************";

+ 20
- 4
src/main/java/com/gitblit/wicket/panels/LogPanel.html View File

@@ -11,13 +11,29 @@
<div class="header"><i class="icon-refresh"></i> <b><span wicket:id="header">[log header]</span></b></div>
<table class="pretty">
<tbody>
<tr wicket:id="commit">
<tr class="hidden-phone hidden-tablet">
<td wicket:id="graph" class="graph"><img wicket:id="image"></img></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="commit" wicket:id="commit">
<td class="date" style="width:6em;"><span wicket:id="commitDate">[commit date]</span></td>
<td class="hidden-phone author"><span wicket:id="commitAuthor">[commit author]</span></td>
<td class="hidden-phone author ellipsize"><span wicket:id="commitAuthor">[commit author]</span></td>
<td class="hidden-phone icon"><img wicket:id="commitIcon" /></td>
<td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
<td class="message ellipsize">
<table class="nestedTable">
<tr>
<td class="ellipsize"><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td>
<td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td>
</tr>
</table>
</td>
<td class="hidden-phone hidden-tablet rightAlign"><span wicket:id="hashLink">[hash link]</span></td>
<td class="hidden-phone hidden-tablet rightAlign">
<td class="hidden-phone hidden-tablet rightAlign" style="white-space: nowrap;">
<span class="link">
<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>

+ 19
- 0
src/main/java/com/gitblit/wicket/panels/LogPanel.java View File

@@ -19,6 +19,9 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.repeater.Item;
@@ -32,9 +35,11 @@ import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.BranchGraphServlet;
import com.gitblit.models.RefModel;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.ExternalImage;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.CommitDiffPage;
import com.gitblit.wicket.pages.CommitPage;
@@ -70,6 +75,20 @@ public class LogPanel extends BasePanel {
// inaccurate way to determine if there are more commits.
// works unless commits.size() represents the exact end.
hasMore = commits.size() >= itemsPerPage;
final String baseUrl = WicketUtils.getGitblitURL(getRequest());
final boolean showGraph = GitBlit.getBoolean(Keys.web.showBranchGraph, true);
MarkupContainer graph = new WebMarkupContainer("graph");
add(graph);
if (!showGraph || commits.isEmpty()) {
// not showing or nothing to show
graph.setVisible(false);
} else {
// set the rowspan on the graph row and +1 for the graph row itself
graph.add(new SimpleAttributeModifier("rowspan", "" + (commits.size() + 1)));
graph.add(new ExternalImage("image", BranchGraphServlet.asLink(baseUrl, repositoryName, commits.get(0).name(), commits.size())));
}
// header
if (pageResults) {

+ 27
- 1
src/main/resources/gitblit.css View File

@@ -1146,6 +1146,32 @@ table.pretty table.nestedTable {
margin-bottom: 0px !important;
}
table.pretty td.graph {
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
table.pretty tr.commit {
/* must match branch graph servlet row height definition */
height: 24px;
}
@media (min-width: 768px) {
td.ellipsize {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
@media (max-width: 767px) {
td.ellipsize {
text-overflow: inherit;
overflow: visible;
white-space: wrap;
}
}
table.comments td {
padding: 4px;
line-height: 17px;
@@ -1204,7 +1230,7 @@ table.palette td.header {
font-weight: bold;
background-color: #ffffff !important;
padding-top: 0px !important;
margin-bottom: 0 !imporant;
margin-bottom: 0 !important;
border: 0 !important;
border-radius: 0 !important;
line-height: 1em;

+ 17
- 1
src/test/java/com/gitblit/tests/JGitUtilsTest.java View File

@@ -37,6 +37,10 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.util.FS;
@@ -602,5 +606,17 @@ public class JGitUtilsTest {
assertTrue(zipFileB.length() > 0);
zipFileB.delete();
}
@Test
public void testPlots() throws Exception {
Repository repository = GitBlitSuite.getTicgitRepository();
PlotWalk pw = new PlotWalk(repository);
PlotCommitList<PlotLane> commits = new PlotCommitList<PlotLane>();
commits.source(pw);
commits.fillTo(25);
for (PlotCommit<PlotLane> commit : commits) {
System.out.println(commit);
}
repository.close();
}
}

Loading…
Cancel
Save