summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorTom <tw201207@gmail.com>2014-10-26 18:10:13 +0100
committerTom <tw201207@gmail.com>2014-11-06 18:05:33 +0100
commitdf0ba7f7ff02ed02c0ba7714ae928a79d932baef (patch)
treedaf72f5876cffec0b185276bd86a7060bd7f9391 /src
parent02c6a8c24505625c5f411c0af1019c7c1f443d07 (diff)
downloadgitblit-df0ba7f7ff02ed02c0ba7714ae928a79d932baef.tar.gz
gitblit-df0ba7f7ff02ed02c0ba7714ae928a79d932baef.zip
Improve the commitdiff.
* Optimize CSS: simplify selectors. That alone cuts rendering time in half! * Adapt HTML generation accordingly. * Change line number generation so that one can select only code lines. Also move the +/- out of the code column; it also gets in the way when selecting. * Omit long diffs altogether. * Omit diff lines for deleted files, they're not particularly interesting. * Introduce a global limit on the maximum number of diff lines to show. * Supply translations for the languages I speak for the new messages. https://code.google.com/p/gitblit/issues/detail?id=450 was about a diff with nearly 300k changed lines (with more then 3000 files deleted). But one doesn't have to have such a monster commit to run into problems. My FF 32 become unresponsive for the 30+ seconds it takes it to render a commitdiff with some 30000 changed lines. (90% of which are in two generated files; the whole commit has just 20 files.) GitHub has no problems showing a commitdiff for this commit, but omits the two large generated files, which makes sense. This change implements a similar thing. Files with too many diff lines get omitted from the output, only the header and a message that the diff is too large remains. Additionally, there's a global limit on the length of a commitdiff; if we exceed that, the whole diff is truncated and the files not shown are listed. The CSS change improves performance by not using descendant selectors for all these table cells. Instead, we assign them precise classes and just use that in the CSS. The line number generation thing using data attributes and a :before selector in the CSS, which enables text selections only in the code column, is not strictly XHTML 1.0. (Data attributes are a feature of HTML 5.) However, reasonably modern browsers also handle this correctly if the page claims to be XHTML 1.0. Besides, the commitdiff page isn't XHTML compliant anyway; I don't think a pre-element may contain divs or even tables. (Note that this technique could be used on other diff pages, too. For instance on the blame page.)
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/gitblit/utils/DiffUtils.java7
-rw-r--r--src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java709
-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/resources/gitblit.css85
7 files changed, 583 insertions, 283 deletions
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..0b16c374 100644
--- a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -1,236 +1,473 @@
-/*
- * 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.HashMap;
+import java.util.Map;
+
+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 {
+
+ /**
+ * 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 files skipped,possibly with a suffix message as value to be displayed. */
+ private final Map<String, String> skipped = new HashMap<String, String>();
+
+ 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 (isOff) {
+ if (ent.getChangeType().equals(ChangeType.DELETE)) {
+ skipped.put(ent.getOldPath(), getMsg("gb.diffDeletedFileSkipped", "(deleted file)"));
+ } else {
+ skipped.put(ent.getNewPath(), null);
+ }
+ }
+ // Keep formatting, but if off, don't produce anything anymore. We just keep on counting.
+ super.format(ent);
+ }
+
+ @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;
+ }
+ String line = text.getString(cur);
+ line = StringUtils.escapeForHtml(line, false);
+ os.write(encode(line));
+ os.write("</td></tr>\n".getBytes());
+ }
+ }
+
+ /**
+ * 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();
+ 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") || 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")) {
+ 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 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\">");
+ }
+ }
+ sb.append(line);
+ if (gitLinkDiff) {
+ sb.append("</td></tr>");
+ }
+ }
+ }
+ sb.append("</tbody></table></div>");
+ 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><tbody><tr><td class='diff-cell' colspan='4'>");
+ boolean first = true;
+ for (Map.Entry<String, String> s : skipped.entrySet()) {
+ if (!first) {
+ sb.append('\n');
+ }
+ String path = StringUtils.escapeForHtml(s.getKey(), false);
+ String comment = s.getValue();
+ if (comment != null) {
+ sb.append("<span id='" + path + "'>" + path + ' ' + StringUtils.escapeForHtml(comment, false) + "</span>");
+ } else {
+ sb.append("<span id='" + path + "'>" + path + "</span>");
+ }
+ first = false;
+ }
+ 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..3a2553f4
--- /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;
+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..cfec39e5 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 file)
+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..da437c7f 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\u00f6schte Datei)
+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..67855a0e 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 = (fichier 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/resources/gitblit.css b/src/main/resources/gitblit.css
index 2484b0cb..43b1be86 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,74 @@ span.diff.hunk_section {
font-family: inherit;
}
-div.diff.add2 {
- background-color: #DDFFDD;
- font-family: inherit;
+.diff-cell {
+ margin: 0px;
+ padding: 0px;
+ border: 0;
+ border-left: 1px solid #bbb;
+}
+
+.add2 {
+ background-color: #DDFFDD;
}
-div.diff.remove2 {
+.remove2 {
background-color: #FFDDDD;
- font-family: inherit;
}
-div.diff table {
+.context2 {
+ background-color: #fbfbfb;
+}
+
+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: #f0f0f0;
+ text-align: center;
+ color: #999;
+ padding-left: 2px;
+ padding-right: 2px;
+ width: 3em; /* Font-size relative! */
+}
+
+.diff-line:before {
+ content: attr(data-lineno);
}
-div.diff table th {
+.diff-state {
background-color: #f0f0f0;
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;
}