From ed552ba47c02779c270ffd62841d6d1048dade70 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sun, 22 Nov 2015 14:37:16 -0500
Subject: [PATCH] Merge branch 'develop'

---
 src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java |  771 +++++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 535 insertions(+), 236 deletions(-)

diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
index 47ff143..86b7ca2 100644
--- a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -1,236 +1,535 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * 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.utils;
-
-import static org.eclipse.jgit.lib.Constants.encode;
-import static org.eclipse.jgit.lib.Constants.encodeASCII;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.text.MessageFormat;
-
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.util.RawParseUtils;
-
-import com.gitblit.models.PathModel.PathChangeModel;
-import com.gitblit.utils.DiffUtils.DiffStat;
-
-/**
- * Generates an html snippet of a diff in Gitblit's style, tracks changed paths,
- * and calculates diff stats.
- *
- * @author James Moger
- *
- */
-public class GitBlitDiffFormatter extends DiffFormatter {
-
-	private final OutputStream os;
-
-	private final DiffStat diffStat;
-
-	private PathChangeModel currentPath;
-
-	private int left, right;
-
-	public GitBlitDiffFormatter(OutputStream os, String commitId) {
-		super(os);
-		this.os = os;
-		this.diffStat = new DiffStat(commitId);
-	}
-
-	@Override
-	public void format(DiffEntry ent) throws IOException {
-		currentPath = diffStat.addPath(ent);
-		super.format(ent);
-	}
-
-	/**
-	 * 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("<tr><th>..</th><th>..</th><td class='hunk_header'>".getBytes());
-		os.write('@');
-		os.write('@');
-		writeRange('-', aStartLine + 1, aEndLine - aStartLine);
-		writeRange('+', bStartLine + 1, bEndLine - bStartLine);
-		os.write(' ');
-		os.write('@');
-		os.write('@');
-		os.write("</td></tr>\n".getBytes());
-		left = aStartLine + 1;
-		right = bStartLine + 1;
-	}
-
-	protected 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 {
-		// update entry diffstat
-		currentPath.update(prefix);
-
-		// output diff
-		os.write("<tr>".getBytes());
-		switch (prefix) {
-		case '+':
-			os.write(("<th></th><th>" + (right++) + "</th>").getBytes());
-			os.write("<td><div class=\"diff add2\">".getBytes());
-			break;
-		case '-':
-			os.write(("<th>" + (left++) + "</th><th></th>").getBytes());
-			os.write("<td><div class=\"diff remove2\">".getBytes());
-			break;
-		default:
-			os.write(("<th>" + (left++) + "</th><th>" + (right++) + "</th>").getBytes());
-			os.write("<td>".getBytes());
-			break;
-		}
-		os.write(prefix);
-		String line = text.getString(cur);
-		line = StringUtils.escapeForHtml(line, false);
-		os.write(encode(line));
-		switch (prefix) {
-		case '+':
-		case '-':
-			os.write("</div>".getBytes());
-			break;
-		default:
-			os.write("</td>".getBytes());
-		}
-		os.write("</tr>\n".getBytes());
-	}
-
-	/**
-	 * Workaround function for complex private methods in DiffFormatter. This
-	 * sets the html for the diff headers.
-	 *
-	 * @return
-	 */
-	public String getHtml() {
-		ByteArrayOutputStream bos = (ByteArrayOutputStream) os;
-		String html = RawParseUtils.decode(bos.toByteArray());
-		String[] lines = html.split("\n");
-		StringBuilder sb = new StringBuilder();
-		boolean inFile = false;
-		String oldnull = "a/dev/null";
-		for (String line : lines) {
-			if (line.startsWith("index")) {
-				// skip index lines
-			} else if (line.startsWith("new file")) {
-				// skip new file lines
-			} else if (line.startsWith("\\ No newline")) {
-				// skip no new line
-			} else if (line.startsWith("---") || line.startsWith("+++")) {
-				// skip --- +++ lines
-			} else if (line.startsWith("diff")) {
-				line = StringUtils.convertOctal(line);
-				if (line.indexOf(oldnull) > -1) {
-					// a is null, use b
-					line = line.substring(("diff --git " + oldnull).length()).trim();
-					// trim b/
-					line = line.substring(2).trim();
-				} else {
-					// use a
-					line = line.substring("diff --git ".length()).trim();
-					line = line.substring(line.startsWith("\"a/") ? 3 : 2);
-					line = line.substring(0, line.indexOf(" b/") > -1 ? line.indexOf(" b/") : line.indexOf("\"b/")).trim();
-				}
-
-				if (line.charAt(0) == '"') {
-					line = line.substring(1);
-				}
-				if (line.charAt(line.length() - 1) == '"') {
-					line = line.substring(0, line.length() - 1);
-				}
-				if (inFile) {
-					sb.append("</tbody></table></div>\n");
-					inFile = false;
-				}
-
-				sb.append(MessageFormat.format("<div class='header'><div class=\"diffHeader\" id=\"{0}\"><i class=\"icon-file\"></i> ", line)).append(line).append("</div></div>");
-				sb.append("<div class=\"diff\">");
-				sb.append("<table><tbody>");
-				inFile = true;
-			} else {
-				boolean gitLinkDiff = line.length() > 0 && line.substring(1).startsWith("Subproject commit");
-				if (gitLinkDiff) {
-					sb.append("<tr><th></th><th></th>");
-					if (line.charAt(0) == '+') {
-						sb.append("<td><div class=\"diff add2\">");
-					} else {
-						sb.append("<td><div class=\"diff remove2\">");
-					}
-				}
-				sb.append(line);
-				if (gitLinkDiff) {
-					sb.append("</div></td></tr>");
-				}
-			}
-		}
-		sb.append("</table></div>");
-		return sb.toString();
-	}
-
-	public DiffStat getDiffStat() {
-		return diffStat;
-	}
-}
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.utils;
+
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Localizer;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.utils.DiffUtils.BinaryDiffHandler;
+import com.gitblit.utils.DiffUtils.DiffStat;
+import com.gitblit.wicket.GitBlitWebApp;
+
+/**
+ * Generates an html snippet of a diff in Gitblit's style, tracks changed paths, and calculates diff stats.
+ *
+ * @author James Moger
+ * @author Tom <tw201207@gmail.com>
+ *
+ */
+public class GitBlitDiffFormatter extends DiffFormatter {
+
+	/** Regex pattern identifying trailing whitespace. */
+	private static final Pattern trailingWhitespace = Pattern.compile("(\\s+?)\r?\n?$");
+
+	/**
+	 * gitblit.properties key for the per-file limit on the number of diff lines.
+	 */
+	private static final String DIFF_LIMIT_PER_FILE_KEY = "web.maxDiffLinesPerFile";
+
+	/**
+	 * gitblit.properties key for the global limit on the number of diff lines in a commitdiff.
+	 */
+	private static final String GLOBAL_DIFF_LIMIT_KEY = "web.maxDiffLines";
+
+	/**
+	 * Diffs with more lines are not shown in commitdiffs. (Similar to what GitHub does.) Can be reduced
+	 * (but not increased) through gitblit.properties key {@link #DIFF_LIMIT_PER_FILE_KEY}.
+	 */
+	private static final int DIFF_LIMIT_PER_FILE = 4000;
+
+	/**
+	 * Global diff limit. Commitdiffs with more lines are truncated. Can be reduced (but not increased)
+	 * through gitblit.properties key {@link #GLOBAL_DIFF_LIMIT_KEY}.
+	 */
+	private static final int GLOBAL_DIFF_LIMIT = 20000;
+
+	private static final boolean CONVERT_TABS = true;
+
+	private final DiffOutputStream os;
+
+	private final DiffStat diffStat;
+
+	private PathChangeModel currentPath;
+
+	private int left, right;
+
+	/**
+	 * If a single file diff in a commitdiff produces more than this number of lines, we don't display
+	 * the diff. First, it's too taxing on the browser: it'll spend an awful lot of time applying the
+	 * CSS rules (despite my having optimized them). And second, no human can read a diff with thousands
+	 * of lines and make sense of it.
+	 * <p>
+	 * Set to {@link #DIFF_LIMIT_PER_FILE} for commitdiffs, and to -1 (switches off the limit) for
+	 * single-file diffs.
+	 * </p>
+	 */
+	private final int maxDiffLinesPerFile;
+
+	/**
+	 * Global limit on the number of diff lines. Set to {@link #GLOBAL_DIFF_LIMIT} for commitdiffs, and
+	 * to -1 (switched off the limit) for single-file diffs.
+	 */
+	private final int globalDiffLimit;
+
+	/** Number of lines for the current file diff. Set to zero when a new DiffEntry is started. */
+	private int nofLinesCurrent;
+	/**
+	 * Position in the stream when we try to write the first line. Used to rewind when we detect that
+	 * the diff is too large.
+	 */
+	private int startCurrent;
+	/** Flag set to true when we rewind. Reset to false when we start a new DiffEntry. */
+	private boolean isOff;
+	/** The current diff entry. */
+	private DiffEntry entry;
+
+	// Global limit stuff.
+
+	/** Total number of lines written before the current diff entry. */
+	private int totalNofLinesPrevious;
+	/** Running total of the number of diff lines written. Updated until we exceed the global limit. */
+	private int totalNofLinesCurrent;
+	/** Stream position to reset to if we decided to truncate the commitdiff. */
+	private int truncateTo;
+	/** Whether we decided to truncate the commitdiff. */
+	private boolean truncated;
+	/** If {@link #truncated}, contains all entries skipped. */
+	private final List<DiffEntry> skipped = new ArrayList<DiffEntry>();
+
+	private int tabLength;
+
+	/**
+	 * A {@link ResettableByteArrayOutputStream} that intercept the "Binary files differ" message produced
+	 * by the super implementation. Unfortunately the super implementation has far too many things private;
+	 * otherwise we'd just have re-implemented {@link GitBlitDiffFormatter#format(DiffEntry) format(DiffEntry)}
+	 * completely without ever calling the super implementation.
+	 */
+	private static class DiffOutputStream extends ResettableByteArrayOutputStream {
+
+		private static final String BINARY_DIFFERENCE = "Binary files differ\n";
+
+		private GitBlitDiffFormatter formatter;
+		private BinaryDiffHandler binaryDiffHandler;
+
+		public void setFormatter(GitBlitDiffFormatter formatter, BinaryDiffHandler handler) {
+			this.formatter = formatter;
+			this.binaryDiffHandler = handler;
+		}
+
+		@Override
+		public void write(byte[] b, int offset, int length) {
+			if (binaryDiffHandler != null
+					&& RawParseUtils.decode(Arrays.copyOfRange(b, offset, offset + length)).contains(BINARY_DIFFERENCE))
+			{
+				String binaryDiff = binaryDiffHandler.renderBinaryDiff(formatter.entry);
+				if (binaryDiff != null) {
+					byte[] bb = ("<tr><td colspan='4' align='center'>" + binaryDiff + "</td></tr>").getBytes(StandardCharsets.UTF_8);
+					super.write(bb, 0, bb.length);
+					return;
+				}
+			}
+			super.write(b, offset, length);
+		}
+
+	}
+
+	public GitBlitDiffFormatter(String commitId, String path, BinaryDiffHandler handler, int tabLength) {
+		super(new DiffOutputStream());
+		this.os = (DiffOutputStream) getOutputStream();
+		this.os.setFormatter(this, handler);
+		this.diffStat = new DiffStat(commitId);
+		this.tabLength = tabLength;
+		// If we have a full commitdiff, install maxima to avoid generating a super-long diff listing that
+		// will only tax the browser too much.
+		maxDiffLinesPerFile = path != null ? -1 : getLimit(DIFF_LIMIT_PER_FILE_KEY, 500, DIFF_LIMIT_PER_FILE);
+		globalDiffLimit = path != null ? -1 : getLimit(GLOBAL_DIFF_LIMIT_KEY, 1000, GLOBAL_DIFF_LIMIT);
+	}
+
+	/**
+	 * Determines a limit to use for HTML diff output.
+	 *
+	 * @param key
+	 *            to use to read the value from the GitBlit settings, if available.
+	 * @param minimum
+	 *            minimum value to enforce
+	 * @param maximum
+	 *            maximum (and default) value to enforce
+	 * @return the limit
+	 */
+	private int getLimit(String key, int minimum, int maximum) {
+		if (Application.exists()) {
+			Application application = Application.get();
+			if (application instanceof GitBlitWebApp) {
+				GitBlitWebApp webApp = (GitBlitWebApp) application;
+				int configValue = webApp.settings().getInteger(key, maximum);
+				if (configValue < minimum) {
+					return minimum;
+				} else if (configValue < maximum) {
+					return configValue;
+				}
+			}
+		}
+		return maximum;
+	}
+
+	/**
+	 * Returns a localized message string, if there is a localization; otherwise the given default value.
+	 *
+	 * @param key
+	 *            message key for the message
+	 * @param defaultValue
+	 *            to use if no localization for the message can be found
+	 * @return the possibly localized message
+	 */
+	private String getMsg(String key, String defaultValue) {
+		if (Application.exists()) {
+			Localizer localizer = Application.get().getResourceSettings().getLocalizer();
+			if (localizer != null) {
+				// Use getStringIgnoreSettings because we don't want exceptions here if the key is missing!
+				return localizer.getStringIgnoreSettings(key, null, null, defaultValue);
+			}
+		}
+		return defaultValue;
+	}
+
+	@Override
+	public void format(DiffEntry ent) throws IOException {
+		currentPath = diffStat.addPath(ent);
+		nofLinesCurrent = 0;
+		isOff = false;
+		entry = ent;
+		if (!truncated) {
+			totalNofLinesPrevious = totalNofLinesCurrent;
+			if (globalDiffLimit > 0 && totalNofLinesPrevious > globalDiffLimit) {
+				truncated = true;
+				isOff = true;
+			}
+			truncateTo = os.size();
+		} else {
+			isOff = true;
+		}
+		if (truncated) {
+			skipped.add(ent);
+		} else {
+			// Produce a header here and now
+			String path;
+			String id;
+			if (ChangeType.DELETE.equals(ent.getChangeType())) {
+				path = ent.getOldPath();
+				id = ent.getOldId().name();
+			} else {
+				path = ent.getNewPath();
+				id = ent.getNewId().name();
+			}
+			StringBuilder sb = new StringBuilder(MessageFormat.format("<div class='header'><div class=\"diffHeader\" id=\"n{0}\"><i class=\"icon-file\"></i> ", id));
+			sb.append(StringUtils.escapeForHtml(path, false)).append("</div></div>");
+			sb.append("<div class=\"diff\"><table cellpadding='0'><tbody>\n");
+			os.write(sb.toString().getBytes());
+		}
+		// Keep formatting, but if off, don't produce anything anymore. We just keep on counting.
+		super.format(ent);
+		if (!truncated) {
+			// Close the table
+			os.write("</tbody></table></div>\n".getBytes());
+		}
+	}
+
+	@Override
+	public void flush() throws IOException {
+		if (truncated) {
+			os.resetTo(truncateTo);
+		}
+		super.flush();
+	}
+
+	/**
+	 * Rewind and issue a message that the diff is too large.
+	 */
+	private void reset() {
+		if (!isOff) {
+			os.resetTo(startCurrent);
+			writeFullWidthLine(getMsg("gb.diffFileDiffTooLarge", "Diff too large"));
+			totalNofLinesCurrent = totalNofLinesPrevious;
+			isOff = true;
+		}
+	}
+
+	/**
+	 * Writes an initial table row containing information about added/removed/renamed/copied files. In case
+	 * of a deletion, we also suppress generating the diff; it's not interesting. (All lines removed.)
+	 */
+	private void handleChange() {
+		// XXX Would be nice if we could generate blob links for the cases handled here. Alas, we lack the repo
+		// name, and cannot reliably determine it here. We could get the .git directory of a Repository, if we
+		// passed in the repo, and then take the name of the parent directory, but that'd fail for repos nested
+		// in GitBlit projects. And we don't know if the repo is inside a project or is a top-level repo.
+		//
+		// That's certainly solvable (just pass along more information), but would require a larger rewrite than
+		// I'm prepared to do now.
+		String message;
+		switch (entry.getChangeType()) {
+		case ADD:
+			message = getMsg("gb.diffNewFile", "New file");
+			break;
+		case DELETE:
+			message = getMsg("gb.diffDeletedFile", "File was deleted");
+			isOff = true;
+			break;
+		case RENAME:
+			message = MessageFormat.format(getMsg("gb.diffRenamedFile", "File was renamed from {0}"), entry.getOldPath());
+			break;
+		case COPY:
+			message = MessageFormat.format(getMsg("gb.diffCopiedFile", "File was copied from {0}"), entry.getOldPath());
+			break;
+		default:
+			return;
+		}
+		writeFullWidthLine(message);
+	}
+
+	/**
+	 * 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 {
+		if (nofLinesCurrent++ == 0) {
+			handleChange();
+			startCurrent = os.size();
+		}
+		if (!isOff) {
+			totalNofLinesCurrent++;
+			if (nofLinesCurrent > maxDiffLinesPerFile && maxDiffLinesPerFile > 0) {
+				reset();
+			} else {
+				os.write("<tr><th class='diff-line' data-lineno='..'></th><th class='diff-line' data-lineno='..'></th><th class='diff-state'></th><td class='hunk_header'>"
+						.getBytes());
+				os.write('@');
+				os.write('@');
+				writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+				writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+				os.write(' ');
+				os.write('@');
+				os.write('@');
+				os.write("</td></tr>\n".getBytes());
+			}
+		}
+		left = aStartLine + 1;
+		right = bStartLine + 1;
+	}
+
+	protected 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;
+		}
+	}
+
+	/**
+	 * Writes a line spanning the full width of the code view, including the gutter.
+	 *
+	 * @param text
+	 *            to put on that line; will be HTML-escaped.
+	 */
+	private void writeFullWidthLine(String text) {
+		try {
+			os.write("<tr><td class='diff-cell' colspan='4'>".getBytes());
+			os.write(StringUtils.escapeForHtml(text, false).getBytes());
+			os.write("</td></tr>\n".getBytes());
+		} catch (IOException ex) {
+			// Cannot happen with a ByteArrayOutputStream
+		}
+	}
+
+	@Override
+	protected void writeLine(final char prefix, final RawText text, final int cur) throws IOException {
+		if (nofLinesCurrent++ == 0) {
+			handleChange();
+			startCurrent = os.size();
+		}
+		// update entry diffstat
+		currentPath.update(prefix);
+		if (isOff) {
+			return;
+		}
+		totalNofLinesCurrent++;
+		if (nofLinesCurrent > maxDiffLinesPerFile && maxDiffLinesPerFile > 0) {
+			reset();
+		} else {
+			// output diff
+			os.write("<tr>".getBytes());
+			switch (prefix) {
+			case '+':
+				os.write(("<th class='diff-line'></th><th class='diff-line' data-lineno='" + (right++) + "'></th>").getBytes());
+				os.write("<th class='diff-state diff-state-add'></th>".getBytes());
+				os.write("<td class='diff-cell add2'>".getBytes());
+				break;
+			case '-':
+				os.write(("<th class='diff-line' data-lineno='" + (left++) + "'></th><th class='diff-line'></th>").getBytes());
+				os.write("<th class='diff-state diff-state-sub'></th>".getBytes());
+				os.write("<td class='diff-cell remove2'>".getBytes());
+				break;
+			default:
+				os.write(("<th class='diff-line' data-lineno='" + (left++) + "'></th><th class='diff-line' data-lineno='" + (right++) + "'></th>").getBytes());
+				os.write("<th class='diff-state'></th>".getBytes());
+				os.write("<td class='diff-cell context2'>".getBytes());
+				break;
+			}
+			os.write(encode(codeLineToHtml(prefix, text.getString(cur))));
+			os.write("</td></tr>\n".getBytes());
+		}
+	}
+
+	/**
+	 * Convert the given code line to HTML.
+	 *
+	 * @param prefix
+	 *            the diff prefix (+/-) indicating whether the line was added or removed.
+	 * @param line
+	 *            the line to format as HTML
+	 * @return the HTML-formatted line, safe for inserting as is into HTML.
+	 */
+	private String codeLineToHtml(final char prefix, final String line) {
+		if ((prefix == '+' || prefix == '-')) {
+			// Highlight trailing whitespace on deleted/added lines.
+			Matcher matcher = trailingWhitespace.matcher(line);
+			if (matcher.find()) {
+				StringBuilder result = new StringBuilder(StringUtils.escapeForHtml(line.substring(0, matcher.start()), CONVERT_TABS, tabLength));
+				result.append("<span class='trailingws-").append(prefix == '+' ? "add" : "sub").append("'>");
+				result.append(StringUtils.escapeForHtml(matcher.group(1), false));
+				result.append("</span>");
+				return result.toString();
+			}
+		}
+		return StringUtils.escapeForHtml(line, CONVERT_TABS, tabLength);
+	}
+
+	/**
+	 * Workaround function for complex private methods in DiffFormatter. This sets the html for the diff headers.
+	 *
+	 * @return
+	 */
+	public String getHtml() {
+		String html = RawParseUtils.decode(os.toByteArray());
+		String[] lines = html.split("\n");
+		StringBuilder sb = new StringBuilder();
+		for (String line : lines) {
+			if (line.startsWith("index") || line.startsWith("similarity")
+					|| line.startsWith("rename from ") || line.startsWith("rename to ")) {
+				// skip index lines
+			} else if (line.startsWith("new file") || line.startsWith("deleted file")) {
+				// skip new file lines
+			} else if (line.startsWith("\\ No newline")) {
+				// skip no new line
+			} else if (line.startsWith("---") || line.startsWith("+++")) {
+				// skip --- +++ lines
+			} else if (line.startsWith("diff")) {
+				// skip diff lines
+			} else {
+				boolean gitLinkDiff = line.length() > 0 && line.substring(1).startsWith("Subproject commit");
+				if (gitLinkDiff) {
+					sb.append("<tr><th class='diff-line'></th><th class='diff-line'></th>");
+					if (line.charAt(0) == '+') {
+						sb.append("<th class='diff-state diff-state-add'></th><td class=\"diff-cell add2\">");
+					} else {
+						sb.append("<th class='diff-state diff-state-sub'></th><td class=\"diff-cell remove2\">");
+					}
+					line = StringUtils.escapeForHtml(line.substring(1), CONVERT_TABS, tabLength);
+				}
+				sb.append(line);
+				if (gitLinkDiff) {
+					sb.append("</td></tr>");
+				}
+				sb.append('\n');
+			}
+		}
+		if (truncated) {
+			sb.append(MessageFormat.format("<div class='header'><div class='diffHeader'>{0}</div></div>",
+					StringUtils.escapeForHtml(getMsg("gb.diffTruncated", "Diff truncated after the above file"), false)));
+			// List all files not shown. We can be sure we do have at least one path in skipped.
+			sb.append("<div class='diff'><table cellpadding='0'><tbody><tr><td class='diff-cell' colspan='4'>");
+			String deletedSuffix = StringUtils.escapeForHtml(getMsg("gb.diffDeletedFileSkipped", "(deleted)"), false);
+			boolean first = true;
+			for (DiffEntry entry : skipped) {
+				if (!first) {
+					sb.append('\n');
+				}
+				if (ChangeType.DELETE.equals(entry.getChangeType())) {
+					sb.append("<span id=\"n" + entry.getOldId().name() + "\">" + StringUtils.escapeForHtml(entry.getOldPath(), false) + ' ' + deletedSuffix + "</span>");
+				} else {
+					sb.append("<span id=\"n" + entry.getNewId().name() + "\">" + StringUtils.escapeForHtml(entry.getNewPath(), false) + "</span>");
+				}
+				first = false;
+			}
+			skipped.clear();
+			sb.append("</td></tr></tbody></table></div>");
+		}
+		return sb.toString();
+	}
+
+	public DiffStat getDiffStat() {
+		return diffStat;
+	}
+}

--
Gitblit v1.9.1