From 7dd99fe7474604f314c01bcd4123eb7cbacfb583 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 11 Nov 2014 07:52:15 +0100 Subject: Image diffs Ticket 88: https://dev.gitblit.com/tickets/gitblit.git/88 Based on Lea Verou's pure CSS slider: http://lea.verou.me/2014/07/image-comparison-slider-with-pure-css/ * Add a callback interface, pass it through DiffUtils to the GitBlitDiffFormatter. Is needed because the rendering needs access to the repositoryName and other things that are known only at higher levels. * New class ImageDiffHandler responsible for rendering an image diff. Called for all binary diffs, doesn't do anything if it's not an image. HTML is generated via JSoup: no worries about forgetting to close a tag, not about HTML escaping, nor about XSS. * The 3 diff pages set up such an ImageDIffHandler and pass it along. * CSS changes: from Lea Verou, with some minor improvements. I think in the long run there'll be no way around rewriting the HTML diff formatter from scratch, not using the standard JGit DiffFormatter at all. --- src/main/java/com/gitblit/utils/DiffUtils.java | 97 ++++++++++++++- .../com/gitblit/utils/GitBlitDiffFormatter.java | 82 +++++++++--- src/main/java/com/gitblit/utils/HtmlBuilder.java | 96 ++++++++++++++ .../com/gitblit/wicket/pages/BlobDiffPage.java | 14 ++- .../com/gitblit/wicket/pages/CommitDiffPage.java | 8 +- .../java/com/gitblit/wicket/pages/ComparePage.java | 7 +- .../com/gitblit/wicket/pages/ImageDiffHandler.java | 138 +++++++++++++++++++++ src/main/resources/gitblit.css | 60 +++++++++ 8 files changed, 477 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/gitblit/utils/HtmlBuilder.java create mode 100644 src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java diff --git a/src/main/java/com/gitblit/utils/DiffUtils.java b/src/main/java/com/gitblit/utils/DiffUtils.java index f597b946..b30a203d 100644 --- a/src/main/java/com/gitblit/utils/DiffUtils.java +++ b/src/main/java/com/gitblit/utils/DiffUtils.java @@ -51,6 +51,27 @@ public class DiffUtils { private static final Logger LOGGER = LoggerFactory.getLogger(DiffUtils.class); + /** + * Callback interface for binary diffs. All the getDiff methods here take an optional handler; + * if given and the {@link DiffOutputType} is {@link DiffOutputType#HTML HTML}, it is responsible + * for displaying a binary diff. + */ + public interface BinaryDiffHandler { + + /** + * Renders a binary diff. The result must be valid HTML, it will be inserted into an HTML table cell. + * May return {@code null} if the default behavior (which is typically just a textual note "Bnary + * files differ") is desired. + * + * @param diffEntry + * current diff entry + * + * @return the rendered diff as HTML, or {@code null} if the default is desired. + */ + public String renderBinaryDiff(final DiffEntry diffEntry); + + } + /** * Enumeration for the diff output types. */ @@ -180,6 +201,23 @@ public class DiffUtils { return getDiff(repository, null, commit, null, outputType); } + /** + * Returns the complete diff of the specified commit compared to its primary parent. + * + * @param repository + * @param commit + * @param outputType + * @param handler + * to use for rendering binary diffs if {@code outputType} is {@link DiffOutputType#HTML HTML}. + * May be {@code null}, resulting in the default behavior. + * @return the diff + */ + public static DiffOutput getCommitDiff(Repository repository, RevCommit commit, + DiffOutputType outputType, BinaryDiffHandler handler) { + return getDiff(repository, null, commit, null, outputType, handler); + } + + /** * Returns the diff for the specified file or folder from the specified * commit compared to its primary parent. @@ -195,6 +233,24 @@ public class DiffUtils { return getDiff(repository, null, commit, path, outputType); } + /** + * Returns the diff for the specified file or folder from the specified + * commit compared to its primary parent. + * + * @param repository + * @param commit + * @param path + * @param outputType + * @param handler + * to use for rendering binary diffs if {@code outputType} is {@link DiffOutputType#HTML HTML}. + * May be {@code null}, resulting in the default behavior. + * @return the diff + */ + public static DiffOutput getDiff(Repository repository, RevCommit commit, String path, + DiffOutputType outputType, BinaryDiffHandler handler) { + return getDiff(repository, null, commit, path, outputType, handler); + } + /** * Returns the complete diff between the two specified commits. * @@ -209,6 +265,23 @@ public class DiffUtils { return getDiff(repository, baseCommit, commit, null, outputType); } + /** + * Returns the complete diff between the two specified commits. + * + * @param repository + * @param baseCommit + * @param commit + * @param outputType + * @param handler + * to use for rendering binary diffs if {@code outputType} is {@link DiffOutputType#HTML HTML}. + * May be {@code null}, resulting in the default behavior. + * @return the diff + */ + public static DiffOutput getDiff(Repository repository, RevCommit baseCommit, RevCommit commit, + DiffOutputType outputType, BinaryDiffHandler handler) { + return getDiff(repository, baseCommit, commit, null, outputType, handler); + } + /** * Returns the diff between two commits for the specified file. * @@ -225,6 +298,28 @@ public class DiffUtils { */ public static DiffOutput getDiff(Repository repository, RevCommit baseCommit, RevCommit commit, String path, DiffOutputType outputType) { + return getDiff(repository, baseCommit, commit, path, outputType, null); + } + + /** + * Returns the diff between two commits for the specified file. + * + * @param repository + * @param baseCommit + * if base commit is null the diff is to the primary parent of + * the commit. + * @param commit + * @param path + * if the path is specified, the diff is restricted to that file + * or folder. if unspecified, the diff is for the entire commit. + * @param outputType + * @param handler + * to use for rendering binary diffs if {@code outputType} is {@link DiffOutputType#HTML HTML}. + * May be {@code null}, resulting in the default behavior. + * @return the diff + */ + public static DiffOutput getDiff(Repository repository, RevCommit baseCommit, RevCommit commit, String path, DiffOutputType outputType, + final BinaryDiffHandler handler) { DiffStat stat = null; String diff = null; try { @@ -233,7 +328,7 @@ public class DiffUtils { DiffFormatter df; switch (outputType) { case HTML: - df = new GitBlitDiffFormatter(commit.getName(), path); + df = new GitBlitDiffFormatter(commit.getName(), path, handler); break; case PLAIN: default: diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java index 5de9e50a..edaed70f 100644 --- a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java +++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java @@ -19,8 +19,10 @@ 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; @@ -34,6 +36,7 @@ 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; @@ -71,7 +74,7 @@ public class GitBlitDiffFormatter extends DiffFormatter { */ private static final int GLOBAL_DIFF_LIMIT = 20000; - private final ResettableByteArrayOutputStream os; + private final DiffOutputStream os; private final DiffStat diffStat; @@ -122,9 +125,45 @@ public class GitBlitDiffFormatter extends DiffFormatter { /** If {@link #truncated}, contains all entries skipped. */ private final List skipped = new ArrayList(); - public GitBlitDiffFormatter(String commitId, String path) { - super(new ResettableByteArrayOutputStream()); - this.os = (ResettableByteArrayOutputStream) getOutputStream(); + /** + * 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 = ("" + binaryDiff + "").getBytes(StandardCharsets.UTF_8); + super.write(bb, 0, bb.length); + return; + } + } + super.write(b, offset, length); + } + + } + + public GitBlitDiffFormatter(String commitId, String path, BinaryDiffHandler handler) { + super(new DiffOutputStream()); + this.os = (DiffOutputStream) getOutputStream(); + this.os.setFormatter(this, handler); 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. @@ -217,7 +256,7 @@ public class GitBlitDiffFormatter extends DiffFormatter { super.format(ent); if (!truncated) { // Close the table - os.write("
\n".getBytes()); + os.write("\n".getBytes()); } } @@ -235,13 +274,7 @@ public class GitBlitDiffFormatter extends DiffFormatter { private void reset() { if (!isOff) { os.resetTo(startCurrent); - try { - os.write("".getBytes()); - os.write(StringUtils.escapeForHtml(getMsg("gb.diffFileDiffTooLarge", "Diff too large"), false).getBytes()); - os.write("\n".getBytes()); - } catch (IOException ex) { - // Cannot happen with a ByteArrayOutputStream - } + writeFullWidthLine(getMsg("gb.diffFileDiffTooLarge", "Diff too large")); totalNofLinesCurrent = totalNofLinesPrevious; isOff = true; } @@ -277,13 +310,7 @@ public class GitBlitDiffFormatter extends DiffFormatter { default: return; } - try { - os.write("".getBytes()); - os.write(StringUtils.escapeForHtml(message, false).getBytes()); - os.write("\n".getBytes()); - } catch (IOException ex) { - // Cannot happen with a ByteArrayOutputStream - } + writeFullWidthLine(message); } /** @@ -355,6 +382,22 @@ public class GitBlitDiffFormatter extends DiffFormatter { } } + /** + * 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("".getBytes()); + os.write(StringUtils.escapeForHtml(text, false).getBytes()); + os.write("\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) { @@ -453,6 +496,7 @@ public class GitBlitDiffFormatter extends DiffFormatter { if (gitLinkDiff) { sb.append(""); } + sb.append('\n'); } } if (truncated) { diff --git a/src/main/java/com/gitblit/utils/HtmlBuilder.java b/src/main/java/com/gitblit/utils/HtmlBuilder.java new file mode 100644 index 00000000..6208ea82 --- /dev/null +++ b/src/main/java/com/gitblit/utils/HtmlBuilder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2014 Tom + * + * 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 org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Tag; + +/** + * Simple helper class to hide some common setup needed to use JSoup as an HTML generator. + * A {@link HtmlBuilder} has a root element that can be manipulated in all the usual JSoup + * ways (but not added to some {@link Document}); to generate HTML for that root element, + * the builder's {@link #toString()} method can be used. (Or one can invoke toString() + * directly on the root element.) By default, a HTML builder does not pretty-print the HTML. + * + * @author Tom + */ +public class HtmlBuilder { + + private final Document shell; + private final Element root; + + /** + * Creates a new HTML builder with a root element with the given {@code tagName}. + * + * @param tagName + * of the {@link Element} to set as the root element. + */ + public HtmlBuilder(String tagName) { + this(new Element(Tag.valueOf(tagName), "")); + } + + /** + * Creates a new HTML builder for the given element. + * + * @param element + * to set as the root element of this HTML builder. + */ + public HtmlBuilder(Element element) { + shell = Document.createShell(""); + shell.outputSettings().prettyPrint(false); + shell.body().appendChild(element); + root = element; + } + + /** @return the root element of this HTML builder */ + public Element getRoot() { + return root; + } + + /** @return the root element of this HTML builder */ + public Element root() { + return root; + } + + /** @return whether this HTML builder will pretty-print the HTML generated by {@link #toString()} */ + public boolean prettyPrint() { + return shell.outputSettings().prettyPrint(); + } + + /** + * Sets whether this HTML builder will produce pretty-printed HTML in its {@link #toString()} method. + * + * @param pretty + * whether to pretty-print + * @return the HTML builder itself + */ + public HtmlBuilder prettyPrint(boolean pretty) { + shell.outputSettings().prettyPrint(pretty); + return this; + } + + /** @return the HTML for the root element. */ + @Override + public String toString() { + return root.toString(); + } + + /** @return the {@link Document} used as generation shell. */ + protected Document getShell() { + return shell; + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java index 9cc3eae1..517e80b8 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java +++ b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java @@ -15,12 +15,15 @@ */ package com.gitblit.wicket.pages; +import java.util.List; + import org.apache.wicket.PageParameters; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import com.gitblit.Keys; import com.gitblit.utils.DiffUtils; import com.gitblit.utils.DiffUtils.DiffOutputType; import com.gitblit.utils.JGitUtils; @@ -43,16 +46,23 @@ public class BlobDiffPage extends RepositoryPage { Repository r = getRepository(); RevCommit commit = getCommit(); + final List imageExtensions = app().settings().getStrings(Keys.web.imageExtensions); + String diff; if (StringUtils.isEmpty(baseObjectId)) { // use first parent - diff = DiffUtils.getDiff(r, commit, blobPath, DiffOutputType.HTML).content; + RevCommit parent = commit.getParentCount() == 0 ? null : commit.getParent(0); + ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + parent.getName(), commit.getName(), imageExtensions); + diff = DiffUtils.getDiff(r, commit, blobPath, DiffOutputType.HTML, handler).content; add(new BookmarkablePageLink("patchLink", PatchPage.class, WicketUtils.newPathParameter(repositoryName, objectId, blobPath))); } else { // base commit specified RevCommit baseCommit = JGitUtils.getCommit(r, baseObjectId); - diff = DiffUtils.getDiff(r, baseCommit, commit, blobPath, DiffOutputType.HTML).content; + ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + baseCommit.getName(), commit.getName(), imageExtensions); + diff = DiffUtils.getDiff(r, baseCommit, commit, blobPath, DiffOutputType.HTML, handler).content; add(new BookmarkablePageLink("patchLink", PatchPage.class, WicketUtils.newBlobDiffParameter(repositoryName, baseObjectId, objectId, blobPath))); diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java index 34ff5a29..77d5ccf3 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java @@ -31,6 +31,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import com.gitblit.Constants; +import com.gitblit.Keys; import com.gitblit.models.GitNote; import com.gitblit.models.PathModel.PathChangeModel; import com.gitblit.models.SubmoduleModel; @@ -59,8 +60,6 @@ public class CommitDiffPage extends RepositoryPage { RevCommit commit = getCommit(); - final DiffOutput diff = DiffUtils.getCommitDiff(r, commit, DiffOutputType.HTML); - List parents = new ArrayList(); if (commit.getParentCount() > 0) { for (RevCommit parent : commit.getParents()) { @@ -82,6 +81,11 @@ public class CommitDiffPage extends RepositoryPage { add(new CommitHeaderPanel("commitHeader", repositoryName, commit)); + final List imageExtensions = app().settings().getStrings(Keys.web.imageExtensions); + final ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + parents.isEmpty() ? null : parents.get(0), commit.getName(), imageExtensions); + final DiffOutput diff = DiffUtils.getCommitDiff(r, commit, DiffOutputType.HTML, handler); + // add commit diffstat int insertions = 0; int deletions = 0; diff --git a/src/main/java/com/gitblit/wicket/pages/ComparePage.java b/src/main/java/com/gitblit/wicket/pages/ComparePage.java index 3b8bb03e..dae8d8ea 100644 --- a/src/main/java/com/gitblit/wicket/pages/ComparePage.java +++ b/src/main/java/com/gitblit/wicket/pages/ComparePage.java @@ -37,6 +37,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import com.gitblit.Keys; import com.gitblit.models.PathModel.PathChangeModel; import com.gitblit.models.RefModel; import com.gitblit.models.RepositoryModel; @@ -111,7 +112,11 @@ public class ComparePage extends RepositoryPage { fromCommitId.setObject(startId); toCommitId.setObject(endId); - final DiffOutput diff = DiffUtils.getDiff(r, fromCommit, toCommit, DiffOutputType.HTML); + final List imageExtensions = app().settings().getStrings(Keys.web.imageExtensions); + final ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + fromCommit.getName(), toCommit.getName(), imageExtensions); + + final DiffOutput diff = DiffUtils.getDiff(r, fromCommit, toCommit, DiffOutputType.HTML, handler); // add compare diffstat int insertions = 0; diff --git a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java new file mode 100644 index 00000000..69d84f4e --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 Tom + * + * 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.wicket.pages; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.wicket.protocol.http.WicketURLEncoder; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.Side; +import org.jsoup.nodes.Element; + +import com.gitblit.servlet.RawServlet; +import com.gitblit.utils.DiffUtils; +import com.gitblit.utils.HtmlBuilder; + +/** + * A {@link DiffUtils.BinaryDiffHandler BinaryDiffHandler} for images. + * + * @author Tom + */ +public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { + + private final String oldCommitId; + private final String newCommitId; + private final String repositoryName; + private final String baseUrl; + private final List imageExtensions; + + public ImageDiffHandler(final String baseUrl, final String repositoryName, final String oldCommitId, + final String newCommitId, final List imageExtensions) { + this.baseUrl = baseUrl; + this.repositoryName = repositoryName; + this.oldCommitId = oldCommitId; + this.newCommitId = newCommitId; + this.imageExtensions = imageExtensions; + } + + /** {@inheritDoc} */ + @Override + public String renderBinaryDiff(DiffEntry diffEntry) { + switch (diffEntry.getChangeType()) { + case MODIFY: + case RENAME: + case COPY: + // TODO: for very small images such as icons, the slider doesn't really help. Two possible + // approaches: either upscale them for display (may show blurry upscaled images), or show + // them side by side (may still be too small to really make out the differences). + String oldUrl = getImageUrl(diffEntry, Side.OLD); + String newUrl = getImageUrl(diffEntry, Side.NEW); + if (oldUrl != null && newUrl != null) { + HtmlBuilder builder = new HtmlBuilder("div"); + Element container = builder.root().appendElement("div").attr("class", "imgdiff"); + Element resizeable = container.appendElement("div").attr("class", "imgdiff-left"); + // style='max-width:640px;' is necessary for ensuring that the browser limits large images + // to some reasonable width, and to override the "img { max-width: 100%; }" from bootstrap.css, + // which would scale the left image to the width of its resizeable container, which isn't what + // we want here. Note that the max-width must be defined directly as inline style on the element, + // otherwise browsers ignore it if the image is larger, and we end up with an image display that + // is too wide. + // XXX: Maybe add a max-height, too, to limit portrait-oriented images to some reasonable height? + // (Like a 300x10000px image...) + resizeable.appendElement("img").attr("class", "imgdiff imgdiff-left").attr("style", "max-width:640px;").attr("src", oldUrl); + container.appendElement("img").attr("class", "imgdiff").attr("style", "max-width:640px;").attr("src", newUrl); + return builder.toString(); + } + break; + case ADD: + String url = getImageUrl(diffEntry, Side.NEW); + if (url != null) { + return new HtmlBuilder("img").root().attr("class", "diff-img").attr("src", url).toString(); + } + break; + default: + break; + } + return null; + } + + /** + * Constructs a URL that will fetch the designated resource in the git repository. The returned string will + * contain the URL fully URL-escaped, but note that it may still contain unescaped ampersands, so the result + * must still be run through HTML escaping if it is to be used in HTML. + * + * @return the URL to the image, if the given {@link DiffEntry} and {@link Side} refers to an image, or {@code null} otherwise. + */ + protected String getImageUrl(DiffEntry entry, Side side) { + String path = entry.getPath(side); + int i = path.lastIndexOf('.'); + if (i > 0) { + String extension = path.substring(i + 1); + for (String ext : imageExtensions) { + if (ext.equalsIgnoreCase(extension)) { + String commitId = Side.NEW.equals(side) ? newCommitId : oldCommitId; + if (commitId != null) { + return RawServlet.asLink(baseUrl, urlencode(repositoryName), commitId, urlencode(path)); + } else { + return null; + } + } + } + } + return null; + } + + /** + * Encode a URL component of a {@link RawServlet} URL in the special way that the servlet expects it. Note that + * the %-encoding used does not encode '&' or '<'. Slashes are not encoded in the result. + * + * @param component + * to encode using %-encoding + * @return the encoded component + */ + protected String urlencode(final String component) { + // RawServlet handles slashes itself. Note that only the PATH_INSTANCE fits the bill here: it encodes + // spaces as %20, and we just have to correct for encoded slashes. Java's standard URLEncoder would + // encode spaces as '+', and I don't know what effects that would have on other parts of GitBlit. It + // would also be wrong for path components (but fine for a query part), so we'd have to correct it, too. + // + // Actually, this should be done in RawServlet.asLink(). As it is now, this may be incorrect if that + // operation ever uses query parameters instead of paths, or if it is fixed to urlencode its path + // components. But I don't want to touch that static method in RawServlet. + return WicketURLEncoder.PATH_INSTANCE.encode(component, StandardCharsets.UTF_8.name()).replaceAll("%2[fF]", "/"); + } +} diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css index 1064231c..f6d6b24d 100644 --- a/src/main/resources/gitblit.css +++ b/src/main/resources/gitblit.css @@ -1438,6 +1438,66 @@ div.diff > table { color: #555; } +/* Image diffs. + Kudos to Lea Verou: http://lea.verou.me/2014/07/image-comparison-slider-with-pure-css/ + Slightly modified by Tom to allow moving the slider fully at the left edge of the images. */ +div.imgdiff { + margin: 5px 2px; + position: relative; + display: inline-block; + line-height: 0; + padding-left: 18px; +} + +/* Note: width defines the initial position of the slider. Would have liked to have it + at 50% initially, but that fails on webkit, which refuses to go below the specified + width. (min-width won't help.) This is known behavior of webkit, see + https://codereview.chromium.org/239983004 and https://bugs.webkit.org/show_bug.cgi?id=72948 + There is a hack (setting width to 1px in :hover) to work around this, but that causes + ugly screen flicker and makes for a dreadful UI. We're better off setting the slider + to the far left initially. */ +div.imgdiff-left { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 18px; + max-width: 100%; + overflow: hidden; + resize: horizontal; + /* Some border that should be visible on most images, combined of a dark color (red) + and white in case the image was all red itself or used other colors that would make + a thin red line hard to make out. */ + border-right: 1px solid red; + box-shadow: 1px 0px 0px 0px white; +} + +div.imgdiff-left:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + width: 13px; + height: 13px; + background: linear-gradient(-45deg, red 50%, transparent 0); + background-clip: content-box; + cursor: ew-resize; +} + +img.imgdiff-left { + margin-left: 18px; /* Compensate for padding on outer div. */ +} + +img.imagediff { + user-select: none; +} + +.diff-img { + margin: 2px 2px; +} + +/* End image diffs */ + td.changeType { width: 15px; } -- cgit v1.2.3