summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/distrib/data/defaults.properties26
-rw-r--r--src/main/java/com/gitblit/utils/DiffUtils.java7
-rw-r--r--src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java721
-rw-r--r--src/main/java/com/gitblit/utils/ResettableByteArrayOutputStream.java42
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties9
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties7
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties7
-rw-r--r--src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ComparePage.java4
-rw-r--r--src/main/resources/gitblit.css96
10 files changed, 635 insertions, 288 deletions
diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties
index 5258a3c2..dae2e619 100644
--- a/src/main/distrib/data/defaults.properties
+++ b/src/main/distrib/data/defaults.properties
@@ -1354,6 +1354,32 @@ web.debugMode = false
# SINCE 1.3.0
web.forceDefaultLocale =
+# The following two settings serve to avoid browser overload when trying to
+# render very large diffs. Both limits apply to commitdiffs, not to single-file
+# diffs.
+
+# Maximum number of diff lines to display for a single file diff in a commitdiff.
+# Defaults to 4000; can be adjusted in the range [500 .. 4000]. Smaller values
+# set the limit to 500, larger values to 4000. The count includes context lines
+# in the diff.
+#
+# If a file diff in a commitdiff produces more lines, the diff for that file is
+# not shown in the commitdiff.
+#
+# SINCE 1.7.0
+web.maxDiffLinesPerFile = 4000
+
+# Total maximum number of diff lines to show in a commitdiff. Defaults to 20000;
+# can be adjusted in the range [1000 .. 20000]. Smaller values set the limit to
+# 1000, larger values to 20000. The count includes context lines in diffs.
+#
+# If a commitdiff produces more lines, it is truncated after the first file
+# that exceeds the limit. Diffs for subsequent files in the commit are not shown
+# at all in the commitdiff. Omitted files are listed, though.
+#
+# SINCE 1.7.0
+web.maxDiffLines = 20000
+
# Enable/disable global regex substitutions (i.e. shared across repositories)
#
# SINCE 0.5.0
diff --git a/src/main/java/com/gitblit/utils/DiffUtils.java b/src/main/java/com/gitblit/utils/DiffUtils.java
index dd2a7807..f597b946 100644
--- a/src/main/java/com/gitblit/utils/DiffUtils.java
+++ b/src/main/java/com/gitblit/utils/DiffUtils.java
@@ -228,15 +228,16 @@ public class DiffUtils {
DiffStat stat = null;
String diff = null;
try {
- final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ByteArrayOutputStream os = null;
RawTextComparator cmp = RawTextComparator.DEFAULT;
DiffFormatter df;
switch (outputType) {
case HTML:
- df = new GitBlitDiffFormatter(os, commit.getName());
+ df = new GitBlitDiffFormatter(commit.getName(), path);
break;
case PLAIN:
default:
+ os = new ByteArrayOutputStream();
df = new DiffFormatter(os);
break;
}
@@ -271,6 +272,7 @@ public class DiffUtils {
} else {
df.format(diffEntries);
}
+ df.flush();
if (df instanceof GitBlitDiffFormatter) {
// workaround for complex private methods in DiffFormatter
diff = ((GitBlitDiffFormatter) df).getHtml();
@@ -278,7 +280,6 @@ public class DiffUtils {
} else {
diff = os.toString();
}
- df.flush();
} catch (Throwable t) {
LOGGER.error("failed to generate commit diff!", t);
}
diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
index 47ff143a..5de9e50a 100644
--- a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -1,236 +1,485 @@
-/*
- * 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.text.MessageFormat;
+import java.util.ArrayList;
+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.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 final ResettableByteArrayOutputStream 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>();
+
+ public GitBlitDiffFormatter(String commitId, String path) {
+ super(new ResettableByteArrayOutputStream());
+ this.os = (ResettableByteArrayOutputStream) getOutputStream();
+ this.diffStat = new DiffStat(commitId);
+ // 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><br />\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);
+ try {
+ os.write("<tr><td class='diff-cell' colspan='4'>".getBytes());
+ os.write(StringUtils.escapeForHtml(getMsg("gb.diffFileDiffTooLarge", "Diff too large"), false).getBytes());
+ os.write("</td></tr>\n".getBytes());
+ } catch (IOException ex) {
+ // Cannot happen with a ByteArrayOutputStream
+ }
+ 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;
+ }
+ try {
+ os.write("<tr><td class='diff-cell' colspan='4'>".getBytes());
+ os.write(StringUtils.escapeForHtml(message, false).getBytes());
+ os.write("</td></tr>\n".getBytes());
+ } catch (IOException ex) {
+ // Cannot happen with a ByteArrayOutputStream
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ @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()), false));
+ 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, false);
+ }
+
+ /**
+ * 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")) {
+ // 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), false);
+ }
+ sb.append(line);
+ if (gitLinkDiff) {
+ sb.append("</td></tr>");
+ }
+ }
+ }
+ 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;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ResettableByteArrayOutputStream.java b/src/main/java/com/gitblit/utils/ResettableByteArrayOutputStream.java
new file mode 100644
index 00000000..7df0693f
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ResettableByteArrayOutputStream.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2014 Tom <tw201207@gmail.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 java.io.ByteArrayOutputStream;
+
+/**
+ * A {@link ByteArrayOutputStream} that can be reset to a specified position.
+ *
+ * @author Tom <tw201207@gmail.com>
+ */
+public class ResettableByteArrayOutputStream extends ByteArrayOutputStream {
+
+ /**
+ * Reset the stream to the given position. If {@code mark} is <= 0, see {@link #reset()}.
+ * A no-op if the stream contains less than {@code mark} bytes. Otherwise, resets the
+ * current writing position to {@code mark}. Previously allocated buffer space will be
+ * reused in subsequent writes.
+ *
+ * @param mark
+ * to set the current writing position to.
+ */
+ public synchronized void resetTo(int mark) {
+ if (mark <= 0) {
+ reset();
+ } else if (mark < count) {
+ count = mark;
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index ce7b5229..c1b5a30e 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -749,4 +749,11 @@ gb.sortHighestPriority = highest priority
gb.sortLowestPriority = lowest priority
gb.sortHighestSeverity = highest severity
gb.sortLowestSeverity = lowest severity
-gb.missingIntegrationBranchMore = The target integration branch does not exist in the repository! \ No newline at end of file
+gb.missingIntegrationBranchMore = The target integration branch does not exist in the repository!
+gb.diffDeletedFileSkipped = (deleted)
+gb.diffFileDiffTooLarge = Diff too large
+gb.diffNewFile = New file
+gb.diffDeletedFile = File was deleted
+gb.diffRenamedFile = File was renamed from {0}
+gb.diffCopiedFile = File was copied from {0}
+gb.diffTruncated = Diff truncated after the above file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
index 3ec330b7..be36ecd1 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
@@ -743,3 +743,10 @@ gb.permission = Berechtigung
gb.sshKeyPermissionDescription = Geben Sie die Zugriffberechtigung f\u00fcr den SSH Key an
gb.transportPreference = \u00dcbertragungseinstellungen
gb.transportPreferenceDescription = Geben Sie die \u00dcbertragungsart an, die Sie f\u00fcr das Klonen bevorzugen
+gb.diffDeletedFileSkipped = (gel\u00f6scht)
+gb.diffFileDiffTooLarge = Zu viele \u00c4nderungen; Diff wird nicht angezeigt
+gb.diffNewFile = Neue Datei
+gb.diffDeletedFile = Datei wurde gel\u00f6scht
+gb.diffRenamedFile = Datei umbenannt von {0}
+gb.diffCopiedFile = Datei kopiert von {0}
+gb.diffTruncated = Diff nach obiger Datei abgeschnitten
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
index b888beee..1318b1d9 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
@@ -672,3 +672,10 @@ gb.ticketIsClosed = Ce ticket est clos.
gb.mergeToDescription = branche d'int\u00e9gration par d\u00e9faut pour fusionner les correctifs li\u00e9s aux tickets
gb.myTickets = mes tickets
gb.yourAssignedTickets = dont vous \u00eates responsable
+gb.diffDeletedFileSkipped = (effac\u00e9)
+gb.diffFileDiffTooLarge = Trop de diff\u00e9rences, affichage supprim\u00e9e
+gb.diffNewFile = Nouveau fichier
+gb.diffDeletedFile = Fichier a \u00e9t\u00e9 effac\u00e9
+gb.diffRenamedFile = Fichier renomm\u00e9 de {0}
+gb.diffCopiedFile = Fichier copi\u00e9 de {0}
+gb.diffTruncated = Affichage de diff\u00e9rences supprim\u00e9e apr\u00e8s le fichier ci-dessus
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
index d827c449..34ff5a29 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -145,10 +145,10 @@ public class CommitDiffPage extends RepositoryPage {
hasSubmodule = submodule.hasSubmodule;
// add relative link
- item.add(new LinkPanel("pathName", "list", entry.path + " @ " + getShortObjectId(submoduleId), "#" + entry.path));
+ item.add(new LinkPanel("pathName", "list", entry.path + " @ " + getShortObjectId(submoduleId), "#n" + entry.objectId));
} else {
// add relative link
- item.add(new LinkPanel("pathName", "list", entry.path, "#" + entry.path));
+ item.add(new LinkPanel("pathName", "list", entry.path, "#n" + entry.objectId));
}
// quick links
diff --git a/src/main/java/com/gitblit/wicket/pages/ComparePage.java b/src/main/java/com/gitblit/wicket/pages/ComparePage.java
index 1ec66133..3b8bb03e 100644
--- a/src/main/java/com/gitblit/wicket/pages/ComparePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ComparePage.java
@@ -160,10 +160,10 @@ public class ComparePage extends RepositoryPage {
hasSubmodule = submodule.hasSubmodule;
// add relative link
- item.add(new LinkPanel("pathName", "list", entry.path + " @ " + getShortObjectId(submoduleId), "#" + entry.path));
+ item.add(new LinkPanel("pathName", "list", entry.path + " @ " + getShortObjectId(submoduleId), "#n" + entry.objectId));
} else {
// add relative link
- item.add(new LinkPanel("pathName", "list", entry.path, "#" + entry.path));
+ item.add(new LinkPanel("pathName", "list", entry.path, "#n" + entry.objectId));
}
// quick links
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 2484b0cb..1064231c 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -1350,19 +1350,6 @@ span.diff.unchanged {
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;
@@ -1374,62 +1361,83 @@ span.diff.hunk_section {
font-family: inherit;
}
-div.diff.add2 {
- background-color: #DDFFDD;
- font-family: inherit;
+.diff-cell {
+ margin: 0px;
+ padding: 0 2px;
+ border: 0;
+ border-left: 1px solid #bbb;
}
-div.diff.remove2 {
+.add2 {
+ background-color: #DDFFDD;
+}
+
+.remove2 {
background-color: #FFDDDD;
- font-family: inherit;
}
-div.diff table {
+.context2 {
+ background-color: #FEFEFE;
+}
+
+.trailingws-add {
+ background-color: #99FF99;
+}
+
+.trailingws-sub {
+ background-color: #FF9999;
+}
+
+div.diff > table {
border-radius: 0;
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
width: 100%;
}
-div.diff table th, div.diff table td {
- margin: 0px;
- padding: 0px;
- font-family: monospace;
- border: 0;
+.diff-line {
+ background-color: #fbfbfb;
+ text-align: center;
+ color: #999;
+ padding-left: 2px;
+ padding-right: 2px;
+ width: 3em; /* Font-size relative! */
+ min-width: 3em;
}
-div.diff table th {
- background-color: #f0f0f0;
+.diff-line:before {
+ content: attr(data-lineno);
+}
+
+.diff-state {
+ background-color: #fbfbfb;
text-align: center;
color: #999;
- padding-left: 5px;
- padding-right: 5px;
- width: 30px;
+ padding-left: 2px;
+ padding-right: 2px;
+ width: 0.5em; /* Font-size relative! */
}
-div.diff table th.header {
- background-color: #D2C3AF;
- border-right: 0px;
- border-bottom: 1px solid #808080;
- font-family: inherit;
- font-size:0.9em;
- color: black;
- padding: 2px;
- text-align: left;
+.diff-state-add:before {
+ color: green;
+ font-weight: bold;
+ content: '+';
+}
+
+.diff-state-sub:before {
+ color: red;
+ font-weight: bold;
+ content: '-';
}
-div.diff table td.hunk_header {
+.hunk_header {
background-color: #dAe2e5 !important;
+ border-left: 1px solid #bbb;
border-top: 1px solid #bac2c5;
border-bottom: 1px solid #bac2c5;
color: #555;
}
-div.diff table td {
- border-left: 1px solid #bbb;
- background-color: #fbfbfb;
-}
-
td.changeType {
width: 15px;
}