summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2013-08-14 08:56:27 -0400
committerJames Moger <james.moger@gitblit.com>2013-09-17 17:34:00 -0400
commitf084f468756bde745d8e8e27c729f6e57bea5749 (patch)
tree5000ff4a8de48ed3259e2ad5b295e03bd829e859 /src
parent578319a659fba918ba720d12ca4d4fc105918595 (diff)
downloadgitblit-f084f468756bde745d8e8e27c729f6e57bea5749.tar.gz
gitblit-f084f468756bde745d8e8e27c729f6e57bea5749.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/main/distrib/data/gitblit.properties5
-rw-r--r--src/main/java/WEB-INF/web.xml13
-rw-r--r--src/main/java/com/gitblit/BranchGraphServlet.java363
-rw-r--r--src/main/java/com/gitblit/Constants.java2
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LogPanel.html24
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LogPanel.java19
-rw-r--r--src/main/resources/gitblit.css28
-rw-r--r--src/test/java/com/gitblit/tests/JGitUtilsTest.java18
8 files changed, 465 insertions, 7 deletions
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 1fe1561f..3c0f1d15 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -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
#
diff --git a/src/main/java/WEB-INF/web.xml b/src/main/java/WEB-INF/web.xml
index cf714651..d4acb049 100644
--- a/src/main/java/WEB-INF/web.xml
+++ b/src/main/java/WEB-INF/web.xml
@@ -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>
diff --git a/src/main/java/com/gitblit/BranchGraphServlet.java b/src/main/java/com/gitblit/BranchGraphServlet.java
new file mode 100644
index 00000000..8fca4556
--- /dev/null
+++ b/src/main/java/com/gitblit/BranchGraphServlet.java
@@ -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;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index a3a3c70e..88a10223 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -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 = "***********************************************************";
diff --git a/src/main/java/com/gitblit/wicket/panels/LogPanel.html b/src/main/java/com/gitblit/wicket/panels/LogPanel.html
index 1abda874..fde9a3e7 100644
--- a/src/main/java/com/gitblit/wicket/panels/LogPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/LogPanel.html
@@ -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>
diff --git a/src/main/java/com/gitblit/wicket/panels/LogPanel.java b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
index 6c523be6..a8f3d556 100644
--- a/src/main/java/com/gitblit/wicket/panels/LogPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
@@ -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) {
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 4db15486..d8745bf4 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -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;
diff --git a/src/test/java/com/gitblit/tests/JGitUtilsTest.java b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
index 463c0a84..06fd674a 100644
--- a/src/test/java/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
@@ -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();
+ }
} \ No newline at end of file