blob: 956222f65f054ff57b59e4c04697e572301cae3e [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
Dave Borowitzec6a9cc2014-04-25 15:51:23 -040017import static com.google.common.base.Preconditions.checkState;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
19
Luca Milanesiobeb12ac2019-11-18 12:14:38 -080020import com.google.common.annotations.VisibleForTesting;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -040021import com.google.common.base.Strings;
Dave Borowitzab6f7d22014-04-29 11:39:20 -070022import com.google.common.collect.ImmutableList;
Dave Borowitz9de65952012-08-13 16:09:45 -070023import com.google.common.collect.Maps;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -040024import com.google.template.soy.data.SoyListData;
25import com.google.template.soy.data.SoyMapData;
Dave Borowitz3b744b12016-08-19 16:11:10 -040026import java.io.IOException;
27import java.util.List;
28import java.util.Map;
Dave Borowitz9de65952012-08-13 16:09:45 -070029import org.eclipse.jgit.diff.RawText;
30import org.eclipse.jgit.errors.LargeObjectException;
31import org.eclipse.jgit.errors.MissingObjectException;
32import org.eclipse.jgit.lib.Constants;
33import org.eclipse.jgit.lib.ObjectId;
34import org.eclipse.jgit.lib.ObjectLoader;
Dave Borowitz7c0a8332014-05-01 11:07:04 -070035import org.eclipse.jgit.lib.ObjectReader;
Dave Borowitz9de65952012-08-13 16:09:45 -070036import org.eclipse.jgit.util.RawParseUtils;
Dave Borowitzab6f7d22014-04-29 11:39:20 -070037import org.slf4j.Logger;
38import org.slf4j.LoggerFactory;
Shawn Pearcec10cc992015-02-09 00:22:33 -080039import prettify.parser.Prettify;
40import syntaxhighlight.ParseResult;
41
Dave Borowitz9de65952012-08-13 16:09:45 -070042/** Soy data converter for git blobs. */
43public class BlobSoyData {
Dave Borowitzab6f7d22014-04-29 11:39:20 -070044 private static final Logger log = LoggerFactory.getLogger(BlobSoyData.class);
45
Dave Borowitz9de65952012-08-13 16:09:45 -070046 /**
Dave Borowitzdc81e5e2016-07-27 18:11:12 -040047 * Maximum number of bytes to load from a supposed text file for display. Files larger than this
48 * will be displayed as binary files, even if the contents was text. For example really big XML
49 * files may be above this limit and will get displayed as binary.
Dave Borowitz9de65952012-08-13 16:09:45 -070050 */
Luca Milanesiobeb12ac2019-11-18 12:14:38 -080051 @VisibleForTesting static final int MAX_FILE_SIZE = 10 << 20;
Dave Borowitz9de65952012-08-13 16:09:45 -070052
Ronald Bhuleskarc9208e52019-07-10 17:44:08 -070053 /**
54 * Maximum number of lines to be displayed. Files larger than this will be displayed as binary
55 * files, even on a text content. For example really big XML files may be above this limit and
56 * will get displayed as binary.
57 */
58 private static final int MAX_LINE_COUNT = 50000;
59
Dave Borowitz9de65952012-08-13 16:09:45 -070060 private final GitilesView view;
Dave Borowitz7c0a8332014-05-01 11:07:04 -070061 private final ObjectReader reader;
Dave Borowitz9de65952012-08-13 16:09:45 -070062
Dave Borowitz7c0a8332014-05-01 11:07:04 -070063 public BlobSoyData(ObjectReader reader, GitilesView view) {
64 this.reader = reader;
Dave Borowitz9de65952012-08-13 16:09:45 -070065 this.view = view;
Dave Borowitz9de65952012-08-13 16:09:45 -070066 }
67
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020068 public Map<String, Object> toSoyData(ObjectId blobId) throws MissingObjectException, IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -070069 return toSoyData(null, blobId);
70 }
71
72 public Map<String, Object> toSoyData(String path, ObjectId blobId)
73 throws MissingObjectException, IOException {
74 Map<String, Object> data = Maps.newHashMapWithExpectedSize(4);
75 data.put("sha", ObjectId.toString(blobId));
76
Dave Borowitz7c0a8332014-05-01 11:07:04 -070077 ObjectLoader loader = reader.open(blobId, Constants.OBJ_BLOB);
Dave Borowitz9de65952012-08-13 16:09:45 -070078 String content;
79 try {
80 byte[] raw = loader.getCachedBytes(MAX_FILE_SIZE);
Luca Milanesiobeb12ac2019-11-18 12:14:38 -080081 content =
82 (raw.length < MAX_FILE_SIZE && !RawText.isBinary(raw)) ? RawParseUtils.decode(raw) : null;
Ronald Bhuleskarc9208e52019-07-10 17:44:08 -070083 if (isContentTooLargeForDisplay(content)) {
84 content = null;
85 }
Dave Borowitz9de65952012-08-13 16:09:45 -070086 } catch (LargeObjectException.OutOfMemory e) {
87 throw e;
88 } catch (LargeObjectException e) {
89 content = null;
90 }
91
Dave Borowitz9de65952012-08-13 16:09:45 -070092 if (content != null) {
Dave Borowitzec6a9cc2014-04-25 15:51:23 -040093 data.put("lines", prettify(path, content));
Shawn Pearcefdd98f62015-02-12 10:04:07 -080094 if (path != null && path.endsWith(".md")) {
95 data.put("docUrl", GitilesView.doc().copyFrom(view).toUrl());
96 }
Dave Borowitz96a6f472014-11-04 16:38:20 -080097 } else {
Dave Borowitzec6a9cc2014-04-25 15:51:23 -040098 data.put("lines", null);
Dave Borowitz9de65952012-08-13 16:09:45 -070099 data.put("size", Long.toString(loader.getSize()));
100 }
101 if (path != null && view.getRevision().getPeeledType() == OBJ_COMMIT) {
Dave Borowitz7745e7f2014-04-27 19:47:47 -0600102 data.put("fileUrl", GitilesView.path().copyFrom(view).toUrl());
Dave Borowitz9de65952012-08-13 16:09:45 -0700103 data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
Dave Borowitz6ec0c872014-01-29 13:59:37 -0800104 data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl());
Dave Borowitz9de65952012-08-13 16:09:45 -0700105 }
106 return data;
107 }
108
Dave Borowitzab6f7d22014-04-29 11:39:20 -0700109 private SoyListData prettify(String path, String content) {
110 List<ParseResult> results = parse(path, content);
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400111 SoyListData lines = new SoyListData();
112 SoyListData line = new SoyListData();
113 lines.add(line);
114
115 int last = 0;
116 for (ParseResult r : results) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200117 checkState(
118 r.getOffset() >= last,
119 "out-of-order ParseResult, expected %s >= %s",
120 r.getOffset(),
121 last);
David Pursehouse390888b2016-06-17 16:55:02 +0900122 writeResult(lines, null, content, last, r.getOffset());
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400123 last = r.getOffset() + r.getLength();
David Pursehouse390888b2016-06-17 16:55:02 +0900124 writeResult(lines, r.getStyleKeysString(), content, r.getOffset(), last);
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400125 }
126 if (last < content.length()) {
127 writeResult(lines, null, content, last, content.length());
128 }
129 return lines;
130 }
131
Dave Borowitzab6f7d22014-04-29 11:39:20 -0700132 private List<ParseResult> parse(String path, String content) {
Shawn Pearcec10cc992015-02-09 00:22:33 -0800133 String lang = extension(path, content);
Dave Borowitzab6f7d22014-04-29 11:39:20 -0700134 try {
Shawn Pearcec10cc992015-02-09 00:22:33 -0800135 return ThreadSafePrettifyParser.INSTANCE.parse(lang, content);
Dave Borowitzab6f7d22014-04-29 11:39:20 -0700136 } catch (StackOverflowError e) {
137 // TODO(dborowitz): Aaagh. Make prettify use RE2. Or replace it something
138 // else. Or something.
David Pursehousefb9a2a92020-02-19 10:45:07 +0900139 log.warn("StackOverflowError prettifying {}", view.toUrl());
Dave Borowitzab6f7d22014-04-29 11:39:20 -0700140 return ImmutableList.of(
141 new ParseResult(0, content.length(), ImmutableList.of(Prettify.PR_PLAIN)));
142 }
143 }
144
Dave Borowitzdc81e5e2016-07-27 18:11:12 -0400145 private static void writeResult(SoyListData lines, String classes, String s, int start, int end) {
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400146 SoyListData line = lines.getListData(lines.length() - 1);
147 while (true) {
148 int nl = nextLineBreak(s, start, end);
149 if (nl < 0) {
150 break;
151 }
152 addSpan(line, classes, s, start, nl);
153
Dave Borowitzdc81e5e2016-07-27 18:11:12 -0400154 start = nl + 1;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400155 if (start == s.length()) {
David Pursehouse390888b2016-06-17 16:55:02 +0900156 return;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400157 }
158 line = new SoyListData();
159 lines.add(line);
160 }
161 addSpan(line, classes, s, start, end);
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400162 }
163
164 private static void addSpan(SoyListData line, String classes, String s, int start, int end) {
165 if (end - start > 0) {
166 if (Strings.isNullOrEmpty(classes)) {
167 classes = Prettify.PR_PLAIN;
168 }
169 line.add(new SoyMapData("classes", classes, "text", s.substring(start, end)));
170 }
171 }
172
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400173 private static int nextLineBreak(String s, int start, int end) {
Dave Borowitzdc81e5e2016-07-27 18:11:12 -0400174 int n = s.indexOf('\n', start);
175 return n < end ? n : -1;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400176 }
177
178 private static String extension(String path, String content) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700179 if (content.startsWith("#!/bin/sh") || content.startsWith("#!/bin/bash")) {
180 return "sh";
181 } else if (content.startsWith("#!/usr/bin/perl")) {
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400182 return "pl";
Dave Borowitz9de65952012-08-13 16:09:45 -0700183 } else if (content.startsWith("#!/usr/bin/python")) {
184 return "py";
185 } else if (path == null) {
186 return null;
187 }
188
189 int slash = path.lastIndexOf('/');
190 int dot = path.lastIndexOf('.');
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400191 String ext = ((0 < dot) && (slash < dot)) ? path.substring(dot + 1) : null;
192 if ("txt".equalsIgnoreCase(ext)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700193 return null;
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400194 } else if ("mk".equalsIgnoreCase(ext)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700195 return "sh";
196 } else if ("Makefile".equalsIgnoreCase(path)
197 || ((0 < slash) && "Makefile".equalsIgnoreCase(path.substring(slash + 1)))) {
198 return "sh";
199 } else {
Dave Borowitzec6a9cc2014-04-25 15:51:23 -0400200 return ext;
Dave Borowitz9de65952012-08-13 16:09:45 -0700201 }
202 }
Ronald Bhuleskarc9208e52019-07-10 17:44:08 -0700203
204 private static boolean isContentTooLargeForDisplay(String content) {
205 if (content == null) {
206 return false;
207 }
208
209 int lines = 0;
210 int nl = -1;
211 while (true) {
212 nl = nextLineBreak(content, nl + 1, content.length());
213 if (nl < 0) {
214 return false;
215 } else if (++lines == MAX_LINE_COUNT) {
216 return true;
217 }
218 }
219 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700220}