blob: 1467d4265b5ec4990964294f8f3c1e01f0d129e3 [file] [log] [blame]
// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.gitiles;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.util.QuotedString.GIT_PATH;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import org.apache.commons.text.StringEscapeUtils;
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.patch.FileHeader;
import org.eclipse.jgit.patch.FileHeader.PatchType;
import org.eclipse.jgit.util.RawParseUtils;
/** Formats a unified format patch as UTF-8 encoded HTML. */
final class HtmlDiffFormatter extends DiffFormatter {
private static final byte[] DIFF_BEGIN =
"<pre class=\"u-pre u-monospace Diff-unified\">".getBytes(UTF_8);
private static final byte[] DIFF_END = "</pre>".getBytes(UTF_8);
private static final byte[] HUNK_BEGIN = "<span class=\"Diff-hunk\">".getBytes(UTF_8);
private static final byte[] HUNK_END = "</span>".getBytes(UTF_8);
private static final byte[] LINE_INSERT_BEGIN = "<span class=\"Diff-insert\">".getBytes(UTF_8);
private static final byte[] LINE_DELETE_BEGIN = "<span class=\"Diff-delete\">".getBytes(UTF_8);
private static final byte[] LINE_CHANGE_BEGIN = "<span class=\"Diff-change\">".getBytes(UTF_8);
private static final byte[] LINE_END = "</span>\n".getBytes(UTF_8);
private final Renderer renderer;
private final GitilesView view;
private int fileIndex;
private DiffEntry entry;
HtmlDiffFormatter(Renderer renderer, GitilesView view, OutputStream out) {
super(out);
this.renderer = checkNotNull(renderer, "renderer");
this.view = checkNotNull(view, "view");
}
@Override
public void format(List<? extends DiffEntry> entries) throws IOException {
for (fileIndex = 0; fileIndex < entries.size(); fileIndex++) {
entry = entries.get(fileIndex);
format(entry);
}
}
@Override
public void format(FileHeader hdr, RawText a, RawText b) throws IOException {
int start = hdr.getStartOffset();
int end = hdr.getEndOffset();
if (!hdr.getHunks().isEmpty()) {
end = hdr.getHunks().get(0).getStartOffset();
}
renderHeader(RawParseUtils.decode(hdr.getBuffer(), start, end));
if (hdr.getPatchType() == PatchType.UNIFIED) {
getOutputStream().write(DIFF_BEGIN);
format(hdr.toEditList(), a, b);
getOutputStream().write(DIFF_END);
}
}
private void renderHeader(String header) throws IOException {
int lf = header.indexOf('\n');
String rest = 0 <= lf ? header.substring(lf + 1) : "";
// Based on DiffFormatter.formatGitDiffFirstHeaderLine.
List<Map<String, String>> parts = Lists.newArrayListWithCapacity(3);
parts.add(ImmutableMap.of("text", "diff --git"));
if (entry.getChangeType() != ChangeType.ADD) {
parts.add(
ImmutableMap.of(
"text", GIT_PATH.quote(getOldPrefix() + entry.getOldPath()),
"url", revisionUrl(view.getOldRevision(), entry.getOldPath())));
} else {
parts.add(ImmutableMap.of("text", GIT_PATH.quote(getOldPrefix() + entry.getNewPath())));
}
if (entry.getChangeType() != ChangeType.DELETE) {
parts.add(
ImmutableMap.of(
"text", GIT_PATH.quote(getNewPrefix() + entry.getNewPath()),
"url", revisionUrl(view.getRevision(), entry.getNewPath())));
} else {
parts.add(ImmutableMap.of("text", GIT_PATH.quote(getNewPrefix() + entry.getOldPath())));
}
getOutputStream()
.write(
renderer
.newRenderer("gitiles.diffHeader")
.setData(ImmutableMap.of("firstParts", parts, "rest", rest, "fileIndex", fileIndex))
.renderHtml()
.get()
.toString()
.getBytes(UTF_8));
}
private String revisionUrl(Revision rev, String path) {
return GitilesView.path()
.copyFrom(view)
.setOldRevision(Revision.NULL)
.setRevision(Revision.named(rev.getId().name()))
.setPathPart(path)
.toUrl();
}
@Override
protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine)
throws IOException {
getOutputStream().write(HUNK_BEGIN);
// TODO(sop): If hunk header starts including method names, escape it.
super.writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine);
getOutputStream().write(HUNK_END);
}
@Override
protected void writeLine(char prefix, RawText text, int cur) throws IOException {
// Manually render each line, rather than invoke a Soy template. This method
// can be called thousands of times in a single request. Avoid unnecessary
// overheads by formatting as-is.
OutputStream out = getOutputStream();
switch (prefix) {
case '+':
out.write(LINE_INSERT_BEGIN);
break;
case '-':
out.write(LINE_DELETE_BEGIN);
break;
case ' ':
default:
out.write(LINE_CHANGE_BEGIN);
break;
}
out.write(prefix);
out.write(StringEscapeUtils.escapeHtml4(text.getString(cur)).getBytes(UTF_8));
out.write(LINE_END);
}
}