blob: 956222f65f054ff57b59e4c04697e572301cae3e [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.checkState;
import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.template.soy.data.SoyListData;
import com.google.template.soy.data.SoyMapData;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import prettify.parser.Prettify;
import syntaxhighlight.ParseResult;
/** Soy data converter for git blobs. */
public class BlobSoyData {
private static final Logger log = LoggerFactory.getLogger(BlobSoyData.class);
/**
* Maximum number of bytes to load from a supposed text file for display. Files larger than this
* will be displayed as binary files, even if the contents was text. For example really big XML
* files may be above this limit and will get displayed as binary.
*/
@VisibleForTesting static final int MAX_FILE_SIZE = 10 << 20;
/**
* Maximum number of lines to be displayed. Files larger than this will be displayed as binary
* files, even on a text content. For example really big XML files may be above this limit and
* will get displayed as binary.
*/
private static final int MAX_LINE_COUNT = 50000;
private final GitilesView view;
private final ObjectReader reader;
public BlobSoyData(ObjectReader reader, GitilesView view) {
this.reader = reader;
this.view = view;
}
public Map<String, Object> toSoyData(ObjectId blobId) throws MissingObjectException, IOException {
return toSoyData(null, blobId);
}
public Map<String, Object> toSoyData(String path, ObjectId blobId)
throws MissingObjectException, IOException {
Map<String, Object> data = Maps.newHashMapWithExpectedSize(4);
data.put("sha", ObjectId.toString(blobId));
ObjectLoader loader = reader.open(blobId, Constants.OBJ_BLOB);
String content;
try {
byte[] raw = loader.getCachedBytes(MAX_FILE_SIZE);
content =
(raw.length < MAX_FILE_SIZE && !RawText.isBinary(raw)) ? RawParseUtils.decode(raw) : null;
if (isContentTooLargeForDisplay(content)) {
content = null;
}
} catch (LargeObjectException.OutOfMemory e) {
throw e;
} catch (LargeObjectException e) {
content = null;
}
if (content != null) {
data.put("lines", prettify(path, content));
if (path != null && path.endsWith(".md")) {
data.put("docUrl", GitilesView.doc().copyFrom(view).toUrl());
}
} else {
data.put("lines", null);
data.put("size", Long.toString(loader.getSize()));
}
if (path != null && view.getRevision().getPeeledType() == OBJ_COMMIT) {
data.put("fileUrl", GitilesView.path().copyFrom(view).toUrl());
data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl());
}
return data;
}
private SoyListData prettify(String path, String content) {
List<ParseResult> results = parse(path, content);
SoyListData lines = new SoyListData();
SoyListData line = new SoyListData();
lines.add(line);
int last = 0;
for (ParseResult r : results) {
checkState(
r.getOffset() >= last,
"out-of-order ParseResult, expected %s >= %s",
r.getOffset(),
last);
writeResult(lines, null, content, last, r.getOffset());
last = r.getOffset() + r.getLength();
writeResult(lines, r.getStyleKeysString(), content, r.getOffset(), last);
}
if (last < content.length()) {
writeResult(lines, null, content, last, content.length());
}
return lines;
}
private List<ParseResult> parse(String path, String content) {
String lang = extension(path, content);
try {
return ThreadSafePrettifyParser.INSTANCE.parse(lang, content);
} catch (StackOverflowError e) {
// TODO(dborowitz): Aaagh. Make prettify use RE2. Or replace it something
// else. Or something.
log.warn("StackOverflowError prettifying {}", view.toUrl());
return ImmutableList.of(
new ParseResult(0, content.length(), ImmutableList.of(Prettify.PR_PLAIN)));
}
}
private static void writeResult(SoyListData lines, String classes, String s, int start, int end) {
SoyListData line = lines.getListData(lines.length() - 1);
while (true) {
int nl = nextLineBreak(s, start, end);
if (nl < 0) {
break;
}
addSpan(line, classes, s, start, nl);
start = nl + 1;
if (start == s.length()) {
return;
}
line = new SoyListData();
lines.add(line);
}
addSpan(line, classes, s, start, end);
}
private static void addSpan(SoyListData line, String classes, String s, int start, int end) {
if (end - start > 0) {
if (Strings.isNullOrEmpty(classes)) {
classes = Prettify.PR_PLAIN;
}
line.add(new SoyMapData("classes", classes, "text", s.substring(start, end)));
}
}
private static int nextLineBreak(String s, int start, int end) {
int n = s.indexOf('\n', start);
return n < end ? n : -1;
}
private static String extension(String path, String content) {
if (content.startsWith("#!/bin/sh") || content.startsWith("#!/bin/bash")) {
return "sh";
} else if (content.startsWith("#!/usr/bin/perl")) {
return "pl";
} else if (content.startsWith("#!/usr/bin/python")) {
return "py";
} else if (path == null) {
return null;
}
int slash = path.lastIndexOf('/');
int dot = path.lastIndexOf('.');
String ext = ((0 < dot) && (slash < dot)) ? path.substring(dot + 1) : null;
if ("txt".equalsIgnoreCase(ext)) {
return null;
} else if ("mk".equalsIgnoreCase(ext)) {
return "sh";
} else if ("Makefile".equalsIgnoreCase(path)
|| ((0 < slash) && "Makefile".equalsIgnoreCase(path.substring(slash + 1)))) {
return "sh";
} else {
return ext;
}
}
private static boolean isContentTooLargeForDisplay(String content) {
if (content == null) {
return false;
}
int lines = 0;
int nl = -1;
while (true) {
nl = nextLineBreak(content, nl + 1, content.length());
if (nl < 0) {
return false;
} else if (++lines == MAX_LINE_COUNT) {
return true;
}
}
}
}