diff options
-rw-r--r-- | releases.moxie | 2 | ||||
-rw-r--r-- | src/main/distrib/data/gitblit.properties | 5 | ||||
-rw-r--r-- | src/main/java/WEB-INF/web.xml | 13 | ||||
-rw-r--r-- | src/main/java/com/gitblit/BranchGraphServlet.java | 363 | ||||
-rw-r--r-- | src/main/java/com/gitblit/Constants.java | 2 | ||||
-rw-r--r-- | src/main/java/com/gitblit/wicket/panels/LogPanel.html | 24 | ||||
-rw-r--r-- | src/main/java/com/gitblit/wicket/panels/LogPanel.java | 19 | ||||
-rw-r--r-- | src/main/resources/gitblit.css | 28 | ||||
-rw-r--r-- | src/test/java/com/gitblit/tests/JGitUtilsTest.java | 18 |
9 files changed, 467 insertions, 7 deletions
diff --git a/releases.moxie b/releases.moxie index 53ad8b53..74cf1e33 100644 --- a/releases.moxie +++ b/releases.moxie @@ -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 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 |