| /* | |
| * 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 java.util.Set; | |
| import java.util.TreeSet; | |
| 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); | |
| int requestedCommits = maxCommits; | |
| if (!StringUtils.isEmpty(length)) { | |
| int l = Integer.parseInt(length); | |
| if (l > 0) { | |
| requestedCommits = 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*Math.max(requestedCommits, maxCommits)); | |
| // determine the appropriate width for the image | |
| int numLanes = 1; | |
| int numCommits = Math.min(requestedCommits, commitList.size()); | |
| if (numCommits > 1) { | |
| // determine graph width | |
| Set<String> parents = new TreeSet<String>(); | |
| for (int i = 0; i < commitList.size(); i++) { | |
| PlotCommit<Lane> commit = commitList.get(i); | |
| boolean checkLane = false; | |
| if (i < numCommits) { | |
| // commit in visible list | |
| checkLane = true; | |
| // remember parents | |
| for (RevCommit p : commit.getParents()) { | |
| parents.add(p.getName()); | |
| } | |
| } else if (parents.contains(commit.getName())) { | |
| // commit outside visible list, but it is a parent of a | |
| // commit in the visible list so we need to know it's lane | |
| // assignment | |
| checkLane = true; | |
| } | |
| if (checkLane) { | |
| 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 < commitList.size(); 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 > 1) { | |
| 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; | |
| } | |
| } | |
| } |