From 87c3d714693b216cee9c31cdc5170715c56f541d Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 06 Apr 2011 09:27:14 -0400
Subject: [PATCH] Mostly working Diff presentation.

---
 src/com/gitblit/utils/HtmlDiffFormatter.java          |  128 ++++++++++++++++++
 src/com/gitblit/wicket/pages/DiffPage.html            |   31 ++++
 src/com/gitblit/wicket/panels/PathLinksPanel.java     |    3 
 src/com/gitblit/wicket/pages/CommitPage.java          |    2 
 src/com/gitblit/wicket/pages/DiffPage.java            |   46 ++++++
 src/com/gitblit/wicket/GitBlitWebApp.java             |    2 
 src/com/gitblit/wicket/panels/ShortLogLinksPanel.java |    4 
 src/com/gitblit/tests/JGitUtilsTest.java              |   42 ++++-
 resources/gitblit.css                                 |   67 +++++++++
 src/com/gitblit/utils/JGitUtils.java                  |   95 +++++++++++--
 10 files changed, 387 insertions(+), 33 deletions(-)

diff --git a/resources/gitblit.css b/resources/gitblit.css
index d126a8a..96815bd 100644
--- a/resources/gitblit.css
+++ b/resources/gitblit.css
@@ -18,8 +18,9 @@
     padding: 0px;
 }
 
-pre.prettyprint, pre.plainprint {
+pre, pre.prettyprint, pre.plainprint {
 	color: black;
+	font-family: monospace;
 	font-size:12px;
 	border:0px;
 }
@@ -201,6 +202,70 @@
 	text-align: center;
 }
 
+div.diff {
+	font-family: monospace;
+}
+
+div.diff.header {
+	-moz-border-bottom-colors: none;
+    -moz-border-image: none;
+    -moz-border-left-colors: none;
+    -moz-border-right-colors: none;
+    -moz-border-top-colors: none;
+    background-color: #EDECE6;
+    border-color: #D9D8D1;
+    border-style: solid;
+    border-width: 1px 0;
+    font-weight: bold;
+    margin-top: 4px;
+    padding: 4px 0 2px;
+}
+
+div.diff.extended_header {
+	background-color: #F6F5EE;
+    padding: 2px 0;
+    font-family: inherit;
+}
+
+div.diff.add {
+	color: #008800;
+	font-family: inherit;
+}
+
+div.diff.remove {
+	color: #cc0000;
+	font-family: inherit;
+}
+
+div.diff.unchanged {
+	color: inherit;
+	font-family: inherit;
+}
+
+div.diff.hunk_header {
+	-moz-border-bottom-colors: none;
+    -moz-border-image: none;
+    -moz-border-left-colors: none;
+    -moz-border-right-colors: none;
+    -moz-border-top-colors: none;
+    border-color: #FFE0FF;
+    border-style: dotted;
+    border-width: 1px 0 0;
+    margin-top: 2px;
+    font-family: inherit;
+}
+
+span.diff.hunk_info {
+	background-color: #FFEEFF;	
+	color: #990099;
+	font-family: inherit;
+}
+
+span.diff.hunk_section {	
+	color: #AA22AA;
+	font-family: inherit;
+}
+
 a.list {
 	text-decoration: none;
 	color: #000000;
diff --git a/src/com/gitblit/tests/JGitUtilsTest.java b/src/com/gitblit/tests/JGitUtilsTest.java
index 6c39840..708f35a 100644
--- a/src/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/com/gitblit/tests/JGitUtilsTest.java
@@ -16,50 +16,50 @@
 
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.TicGitTicket;
+import com.gitblit.wicket.models.PathModel;
 import com.gitblit.wicket.models.RefModel;
 
-
 public class JGitUtilsTest extends TestCase {
-	
+
 	private File repositoriesFolder = new File("c:/projects/git");
 	private boolean exportAll = true;
 	private boolean readNested = true;
-	
+
 	private List<String> getRepositories() {
 		return JGitUtils.getRepositoryList(repositoriesFolder, exportAll, readNested);
 	}
-	
+
 	private Repository getRepository() throws Exception {
 		return new FileRepository(new File(repositoriesFolder, getRepositories().get(0)) + "/" + Constants.DOT_GIT);
 	}
-	
+
 	public void testFindRepositories() {
 		List<String> list = getRepositories();
 		assertTrue("No repositories found in " + repositoriesFolder, list.size() > 0);
 	}
-	
-	public void testOpenRepository() throws Exception {		
+
+	public void testOpenRepository() throws Exception {
 		Repository r = getRepository();
 		r.close();
 		assertTrue("Could not find repository!", r != null);
 	}
-	
-	public void testLastChangeRepository() throws Exception {		
+
+	public void testLastChangeRepository() throws Exception {
 		Repository r = getRepository();
 		Date date = JGitUtils.getLastChange(r);
 		r.close();
 		assertTrue("Could not get last repository change date!", date != null);
 	}
-	
+
 	public void testRetrieveRevObject() throws Exception {
 		Repository r = getRepository();
 		RevCommit commit = JGitUtils.getCommit(r, Constants.HEAD);
 		RevTree tree = commit.getTree();
-		RevObject object = JGitUtils.getRevObject(r, tree, "AUTHORS");		
+		RevObject object = JGitUtils.getRevObject(r, tree, "AUTHORS");
 		r.close();
 		assertTrue("Object is null!", object != null);
 	}
-	
+
 	public void testRetrieveStringContent() throws Exception {
 		Repository r = getRepository();
 		RevCommit commit = JGitUtils.getCommit(r, Constants.HEAD);
@@ -69,7 +69,7 @@
 		r.close();
 		assertTrue("Content is null!", content != null);
 	}
-	
+
 	public void testTicGit() throws Exception {
 		Repository r = new FileRepository(new File(repositoriesFolder, "ticgit") + "/" + Constants.DOT_GIT);
 		RefModel ticgit = JGitUtils.getTicGitBranch(r);
@@ -79,4 +79,20 @@
 		r.close();
 	}
 
+	public void testFilesInCommit() throws Exception {
+		Repository r = getRepository();
+		RevCommit commit = JGitUtils.getCommit(r, Constants.HEAD);
+		List<PathModel> paths = JGitUtils.getFilesInCommit(r, commit);
+		r.close();
+		assertTrue("No changed paths found!", paths.size() > 0);
+	}
+	
+	public void testCommitDiff() throws Exception {
+		Repository r = getRepository();
+		RevCommit commit = JGitUtils.getCommit(r, Constants.HEAD);
+		String diff = JGitUtils.getCommitDiff(r, commit, false);
+		r.close();
+		System.out.println(diff);
+	}
+
 }
diff --git a/src/com/gitblit/utils/HtmlDiffFormatter.java b/src/com/gitblit/utils/HtmlDiffFormatter.java
new file mode 100644
index 0000000..7c37862
--- /dev/null
+++ b/src/com/gitblit/utils/HtmlDiffFormatter.java
@@ -0,0 +1,128 @@
+package com.gitblit.utils;
+
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+
+public class HtmlDiffFormatter extends DiffFormatter {
+	private final OutputStream os;
+
+	public HtmlDiffFormatter(OutputStream os) {
+		super(os);
+		this.os = os;
+	}
+
+	/**
+	 * Output a hunk header
+	 * 
+	 * @param aStartLine
+	 *            within first source
+	 * @param aEndLine
+	 *            within first source
+	 * @param bStartLine
+	 *            within second source
+	 * @param bEndLine
+	 *            within second source
+	 * @throws IOException
+	 */
+	@Override
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine) throws IOException {
+		os.write("<div class=\"diff hunk_header\"><span class=\"diff hunk_info\">".getBytes());
+		os.write('@');
+		os.write('@');
+		writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+		writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+		os.write(' ');
+		os.write('@');
+		os.write('@');
+		// TODO not sure if JGit can determine hunk section
+		//os.write("<span class=\"diff hunk_section\">".getBytes());
+		//os.write("</span>".getBytes());
+		os.write("</span></div>".getBytes());
+	}
+
+	private void writeRange(final char prefix, final int begin, final int cnt) throws IOException {
+		os.write(' ');
+		os.write(prefix);
+		switch (cnt) {
+		case 0:
+			// If the range is empty, its beginning number must
+			// be the
+			// line just before the range, or 0 if the range is
+			// at the
+			// start of the file stream. Here, begin is always 1
+			// based,
+			// so an empty file would produce "0,0".
+			//
+			os.write(encodeASCII(begin - 1));
+			os.write(',');
+			os.write('0');
+			break;
+
+		case 1:
+			// If the range is exactly one line, produce only
+			// the number.
+			//
+			os.write(encodeASCII(begin));
+			break;
+
+		default:
+			os.write(encodeASCII(begin));
+			os.write(',');
+			os.write(encodeASCII(cnt));
+			break;
+		}
+	}
+
+	@Override
+	protected void writeLine(final char prefix, final RawText text, final int cur) throws IOException {
+		switch (prefix) {
+		case '+':
+			os.write("<div class=\"diff add\">".getBytes());
+			break;
+		case '-':
+			os.write("<div class=\"diff remove\">".getBytes());
+			break;
+		}
+		os.write(prefix);
+		text.writeLine(os, cur);
+		switch (prefix) {
+		case '+':
+		case '-':
+			os.write("</div>".getBytes());
+			break;
+		default:
+			os.write('\n');
+		}
+	}
+
+	/**
+	 * Workaround function for complex private methods in DiffFormatter. This
+	 * sets the html for the diff headers.
+	 * 
+	 * @return
+	 */
+	public String getHtml() {
+		String html = os.toString();
+		String[] lines = html.split("\n");
+		StringBuilder sb = new StringBuilder();
+		sb.append("<div class=\"diff\">");
+		for (String line : lines) {
+			if (line.startsWith("diff")) {
+				sb.append("<div class=\"diff header\">").append(line).append("</div>");
+			} else if (line.startsWith("---")) {
+				sb.append("<div class=\"diff remove\">").append(line).append("</div>");
+			} else if (line.startsWith("+++")) {
+				sb.append("<div class=\"diff add\">").append(line).append("</div>");
+			} else {
+				sb.append(line).append('\n');
+			}
+		}
+		sb.append("</div>");
+		return sb.toString();
+	}
+}
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index b4e0b15..1b418b0 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -17,6 +17,9 @@
 import java.util.Set;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -35,6 +38,8 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -243,29 +248,89 @@
 		return list;
 	}
 
-	public static List<PathModel> getCommitChangedPaths(Repository r, String commitId) {
+	public static List<PathModel> getFilesInCommit(Repository r, String commitId) {
 		RevCommit commit = getCommit(r, commitId);
-		return getCommitChangedPaths(r, commit);
+		return getFilesInCommit(r, commit);
 	}
 
-	public static List<PathModel> getCommitChangedPaths(Repository r, RevCommit commit) {
+	public static List<PathModel> getFilesInCommit(Repository r, RevCommit commit) {
 		List<PathModel> list = new ArrayList<PathModel>();
-		final TreeWalk walk = new TreeWalk(r);
-		walk.setRecursive(false);
 		try {
-			walk.addTree(commit.getTree());
-			while (walk.next()) {
-				list.add(getPathModel(walk, null, commit));
-			}
+			final RevWalk rw = new RevWalk(r);
+			RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+			RevTree parentTree = parent.getTree();
+			RevTree commitTree = commit.getTree();
 
-		} catch (IOException e) {
-			LOGGER.error("Failed to get files for commit " + commit.getName(), e);
-		} finally {
-			if (walk != null) {
-				walk.release();
+			final TreeWalk walk = new TreeWalk(r);
+			walk.reset();
+			walk.setRecursive(true);
+			walk.addTree(parentTree);
+			walk.addTree(commitTree);
+			walk.setFilter(TreeFilter.ANY_DIFF);
+
+			RawTextComparator cmp = RawTextComparator.DEFAULT;
+			DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+			df.setRepository(r);
+			df.setDiffComparator(cmp);
+			df.setDetectRenames(true);
+			List<DiffEntry> diffs = df.scan(parentTree, commitTree);
+			for (DiffEntry diff : diffs) {
+				list.add(new PathModel(diff.getNewPath(), diff.getNewPath(), 0, diff.getNewMode().getBits(), commit.getId().getName()));
 			}
+		} catch (Throwable t) {
+			LOGGER.error("failed to determine files in commit!", t);
 		}
 		return list;
+	}
+
+	public static String getCommitDiff(Repository r, RevCommit commit, boolean outputHtml) {
+		return getCommitDiff(r, commit, null, outputHtml);
+	}
+	
+	public static String getCommitDiff(Repository r, RevCommit commit, String path, boolean outputHtml) {
+		try {
+			final RevWalk rw = new RevWalk(r);
+			RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+			RevTree parentTree = parent.getTree();
+			RevTree commitTree = commit.getTree();
+
+			final TreeWalk walk = new TreeWalk(r);
+			walk.reset();
+			walk.setRecursive(true);
+			walk.addTree(parentTree);
+			walk.addTree(commitTree);
+			if (path != null && path.trim().length() > 0) {
+				walk.setFilter(PathFilter.create(path));
+			} else {
+				walk.setFilter(TreeFilter.ANY_DIFF);
+			}
+
+			final ByteArrayOutputStream os = new ByteArrayOutputStream();
+			RawTextComparator cmp = RawTextComparator.DEFAULT;
+			DiffFormatter df;
+			if (outputHtml) {
+				df = new HtmlDiffFormatter(os);
+			} else {
+				df = new DiffFormatter(os);
+			}
+			df.setRepository(r);
+			df.setDiffComparator(cmp);
+			df.setDetectRenames(true);
+			List<DiffEntry> diffs = df.scan(parentTree, commitTree);
+			df.format(diffs);
+			String diff;
+			if (outputHtml) {
+				// workaround for complex private methods in DiffFormatter
+				diff = ((HtmlDiffFormatter) df).getHtml();
+			} else {
+				diff = os.toString();
+			}
+			df.flush();
+			return diff;
+		} catch (Throwable t) {
+			LOGGER.error("failed to generate commit diff!", t);
+		}
+		return null;
 	}
 
 	private static PathModel getPathModel(TreeWalk walk, String basePath, RevCommit commit) {
@@ -519,7 +584,7 @@
 		}
 		return null;
 	}
-	
+
 	private static void readTicketContents(Repository r, RefModel ticgitBranch, TicGitTicket ticket) {
 		List<PathModel> ticketFiles = getFilesInPath(r, ticket.name, ticgitBranch.getCommit());
 		for (PathModel file : ticketFiles) {
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.java b/src/com/gitblit/wicket/GitBlitWebApp.java
index 6e8a6dd..70bb970 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/com/gitblit/wicket/GitBlitWebApp.java
@@ -31,6 +31,7 @@
 import com.gitblit.wicket.pages.BlobPage;
 import com.gitblit.wicket.pages.BranchesPage;
 import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.DiffPage;
 import com.gitblit.wicket.pages.RepositoriesPage;
 import com.gitblit.wicket.pages.ShortLogPage;
 import com.gitblit.wicket.pages.SummaryPage;
@@ -69,6 +70,7 @@
 		mount(new MixedParamUrlCodingStrategy("/tag", TagPage.class, new String[] { "p", "h" }));
 		mount(new MixedParamUrlCodingStrategy("/tree", TreePage.class, new String[] { "p", "h", "f" }));
 		mount(new MixedParamUrlCodingStrategy("/blob", BlobPage.class, new String[] { "p", "h", "f" }));
+		mount(new MixedParamUrlCodingStrategy("/diff", DiffPage.class, new String[] { "p", "h", "f" }));
 		
 		// setup extended urls
 		mount(new MixedParamUrlCodingStrategy("/ticgit", TicGitPage.class, new String[] { "p" }));
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index ad1fce0..0754532 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -76,7 +76,7 @@
 		addFullText("fullMessage", c.getFullMessage(), true);
 
 		// changed paths list
-		List<PathModel> paths  = JGitUtils.getCommitChangedPaths(r, c);
+		List<PathModel> paths  = JGitUtils.getFilesInCommit(r, c);
 		ListDataProvider<PathModel> pathsDp = new ListDataProvider<PathModel>(paths);
 		DataView<PathModel> pathsView = new DataView<PathModel>("changedPath", pathsDp) {
 			private static final long serialVersionUID = 1L;
diff --git a/src/com/gitblit/wicket/pages/DiffPage.html b/src/com/gitblit/wicket/pages/DiffPage.html
new file mode 100644
index 0000000..b7aa03b
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/DiffPage.html
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml" >
+<head>
+   	<link href="prettify/prettify.css" type="text/css" rel="stylesheet" />
+	<script type="text/javascript" src="prettify/prettify.js"></script>
+</head>
+<body onload="prettyPrint()">
+	<!-- page header -->
+	<div wicket:id="pageHeader"></div>
+
+	<!-- page nav links -->	
+	<div wicket:id="pageLinks"></div>
+	
+	<!-- blob nav links -->	
+	<div class="page_nav2">
+		<span wicket:id="historyLink"></span> | <span wicket:id="rawLink"></span> | <span wicket:id="headLink"></span>
+	</div>	
+	
+	<!-- shortlog header -->
+	<div class="header" wicket:id="shortlog"></div>
+
+	<!-- breadcrumbs -->
+	<div wicket:id="breadcrumbs"></div>
+		
+	<!--  diff content -->
+	<pre wicket:id="diffText"></pre>
+	
+	<!-- footer -->
+	<div wicket:id="pageFooter"></div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/DiffPage.java b/src/com/gitblit/wicket/pages/DiffPage.java
new file mode 100644
index 0000000..05500f8
--- /dev/null
+++ b/src/com/gitblit/wicket/pages/DiffPage.java
@@ -0,0 +1,46 @@
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.LinkPanel;
+import com.gitblit.wicket.RepositoryPage;
+import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
+
+public class DiffPage extends RepositoryPage {
+
+	public DiffPage(PageParameters params) {
+		super(params, "diff");
+
+		final String blobPath = params.getString("f", null);
+
+		Repository r = getRepository();
+		RevCommit commit = JGitUtils.getCommit(r, commitId);
+		String diff;
+		if (blobPath != null && blobPath.length() > 0) {
+			// blob diff
+			diff = JGitUtils.getCommitDiff(r, commit, blobPath, true);
+		} else {
+			// commit diff
+			diff = JGitUtils.getCommitDiff(r, commit, true);
+		}
+		r.close();
+
+		// diff page links
+		add(new Label("historyLink", "history"));
+		add(new Label("rawLink", "raw"));
+		add(new Label("headLink", "HEAD"));
+
+		add(new LinkPanel("shortlog", "title", commit.getShortMessage(), CommitPage.class, newCommitParameter()));
+
+		add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, commitId));
+
+		add(new Label("diffText", diff).setEscapeModelStrings(false));
+
+		// footer
+		addFooter();
+	}
+}
diff --git a/src/com/gitblit/wicket/panels/PathLinksPanel.java b/src/com/gitblit/wicket/panels/PathLinksPanel.java
index c29124b..49aeb4a 100644
--- a/src/com/gitblit/wicket/panels/PathLinksPanel.java
+++ b/src/com/gitblit/wicket/panels/PathLinksPanel.java
@@ -7,6 +7,7 @@
 import com.gitblit.wicket.LinkPanel;
 import com.gitblit.wicket.models.PathModel;
 import com.gitblit.wicket.pages.BlobPage;
+import com.gitblit.wicket.pages.DiffPage;
 
 
 public class PathLinksPanel extends Panel {
@@ -15,7 +16,7 @@
 
 	public PathLinksPanel(String id, String repositoryName, PathModel path) {
 		super(id);
-		add(new Label("diff", "diff"));
+		add(new LinkPanel("diff", null, "diff", DiffPage.class, new PageParameters("p=" + repositoryName + ",h=" + path.commitId + ",f=" + path.path)));
 		add(new LinkPanel("blob", null, "view", BlobPage.class, new PageParameters("p=" + repositoryName + ",h=" + path.commitId + ",f=" + path.path)));
 		add(new Label("history", "history"));
 	}
diff --git a/src/com/gitblit/wicket/panels/ShortLogLinksPanel.java b/src/com/gitblit/wicket/panels/ShortLogLinksPanel.java
index 58ec37a..51c894d 100644
--- a/src/com/gitblit/wicket/panels/ShortLogLinksPanel.java
+++ b/src/com/gitblit/wicket/panels/ShortLogLinksPanel.java
@@ -1,11 +1,11 @@
 package com.gitblit.wicket.panels;
 
 import org.apache.wicket.PageParameters;
-import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.panel.Panel;
 
 import com.gitblit.wicket.LinkPanel;
 import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.DiffPage;
 import com.gitblit.wicket.pages.TreePage;
 
 
@@ -17,7 +17,7 @@
 		super(id);
 
 		add(new LinkPanel("commit", null, "commit", CommitPage.class, new PageParameters("p=" + repositoryName + ",h=" + commitId)));
-		add(new Label("commitdiff", "commitdiff"));
+		add(new LinkPanel("commitdiff", null, "commitdiff", DiffPage.class, new PageParameters("p=" + repositoryName + ",h=" + commitId)));
 		add(new LinkPanel("tree", null, "tree", TreePage.class, new PageParameters("p=" + repositoryName + ",h=" + commitId)));
 	}
 }
\ No newline at end of file

--
Gitblit v1.9.1