blob: 0b275e078f2c2d27648f984d4a7848e81c49546d [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
David Pletcherd7bdaf32014-08-27 14:50:32 -070017import static java.nio.charset.StandardCharsets.UTF_8;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
19
20import com.google.common.annotations.VisibleForTesting;
Dave Borowitz73269892013-11-13 14:23:50 -080021import com.google.common.base.Strings;
Dave Borowitz9de65952012-08-13 16:09:45 -070022import com.google.common.collect.Lists;
23import com.google.common.collect.Maps;
Dave Borowitz9de65952012-08-13 16:09:45 -070024import com.google.gitiles.PathServlet.FileType;
Shawn Pearce73e34532015-02-12 16:27:54 -080025import com.google.gitiles.doc.GitilesMarkdown;
26import com.google.gitiles.doc.ImageLoader;
27import com.google.gitiles.doc.MarkdownToHtml;
28import com.google.template.soy.data.SanitizedContent;
Dave Borowitz9de65952012-08-13 16:09:45 -070029
Shawn Pearce73e34532015-02-12 16:27:54 -080030import org.eclipse.jgit.errors.LargeObjectException;
Dave Borowitz9de65952012-08-13 16:09:45 -070031import org.eclipse.jgit.errors.MissingObjectException;
Shawn Pearce73e34532015-02-12 16:27:54 -080032import org.eclipse.jgit.lib.Config;
33import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070034import org.eclipse.jgit.lib.ObjectId;
Dave Borowitz7c0a8332014-05-01 11:07:04 -070035import org.eclipse.jgit.lib.ObjectReader;
Shawn Pearce73e34532015-02-12 16:27:54 -080036import org.eclipse.jgit.revwalk.RevTree;
Dave Borowitz9de65952012-08-13 16:09:45 -070037import org.eclipse.jgit.treewalk.TreeWalk;
Shawn Pearce73e34532015-02-12 16:27:54 -080038import org.eclipse.jgit.util.RawParseUtils;
39import org.pegdown.ast.RootNode;
40import org.slf4j.Logger;
41import org.slf4j.LoggerFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -070042
43import java.io.IOException;
44import java.util.List;
45import java.util.Map;
Dave Borowitz9de65952012-08-13 16:09:45 -070046
47/** Soy data converter for git trees. */
48public class TreeSoyData {
Shawn Pearce73e34532015-02-12 16:27:54 -080049 private static final Logger log = LoggerFactory.getLogger(TreeSoyData.class);
50
Dave Borowitz9de65952012-08-13 16:09:45 -070051 /**
52 * Number of characters to display for a symlink target. Targets longer than
53 * this are abbreviated for display in a tree listing.
54 */
55 private static final int MAX_SYMLINK_TARGET_LENGTH = 72;
56
57 /**
58 * Maximum number of bytes to load from a blob that claims to be a symlink. If
59 * the blob is larger than this byte limit it will be displayed as a binary
60 * file instead of as a symlink.
61 */
62 static final int MAX_SYMLINK_SIZE = 16 << 10;
63
64 static String resolveTargetUrl(GitilesView view, String target) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -070065 String resolved = Paths.simplifyPathUpToRoot(target, view.getPathPart());
Dave Borowitzbcd753d2013-02-08 11:10:19 -080066 if (resolved == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -070067 return null;
68 }
Dave Borowitz9de65952012-08-13 16:09:45 -070069 return GitilesView.path()
70 .copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -070071 .setPathPart(resolved)
Dave Borowitz9de65952012-08-13 16:09:45 -070072 .toUrl();
73 }
74
75 @VisibleForTesting
76 static String getTargetDisplayName(String target) {
77 if (target.length() <= MAX_SYMLINK_TARGET_LENGTH) {
78 return target;
79 } else {
80 int lastSlash = target.lastIndexOf('/');
81 // TODO(dborowitz): Doesn't abbreviate a long last path component.
82 return lastSlash >= 0 ? "..." + target.substring(lastSlash) : target;
83 }
84 }
85
Dave Borowitz7c0a8332014-05-01 11:07:04 -070086 private final ObjectReader reader;
Dave Borowitz9de65952012-08-13 16:09:45 -070087 private final GitilesView view;
Shawn Pearce73e34532015-02-12 16:27:54 -080088 private final Config cfg;
89 private final RevTree rootTree;
Dave Borowitzc782ebe2013-11-11 11:43:29 -080090 private ArchiveFormat archiveFormat;
Dave Borowitz9de65952012-08-13 16:09:45 -070091
Shawn Pearce73e34532015-02-12 16:27:54 -080092 public TreeSoyData(ObjectReader reader, GitilesView view, Config cfg,
93 RevTree rootTree) {
Dave Borowitz7c0a8332014-05-01 11:07:04 -070094 this.reader = reader;
Dave Borowitz9de65952012-08-13 16:09:45 -070095 this.view = view;
Shawn Pearce73e34532015-02-12 16:27:54 -080096 this.cfg = cfg;
97 this.rootTree = rootTree;
Dave Borowitz9de65952012-08-13 16:09:45 -070098 }
99
Dave Borowitzc782ebe2013-11-11 11:43:29 -0800100 public TreeSoyData setArchiveFormat(ArchiveFormat archiveFormat) {
101 this.archiveFormat = archiveFormat;
102 return this;
103 }
104
Dave Borowitz9de65952012-08-13 16:09:45 -0700105 public Map<String, Object> toSoyData(ObjectId treeId, TreeWalk tw) throws MissingObjectException,
106 IOException {
Shawn Pearce73e34532015-02-12 16:27:54 -0800107 String readmePath = null;
108 ObjectId readmeId = null;
109
Dave Borowitz9de65952012-08-13 16:09:45 -0700110 List<Object> entries = Lists.newArrayList();
111 GitilesView.Builder urlBuilder = GitilesView.path().copyFrom(view);
112 while (tw.next()) {
113 FileType type = FileType.forEntry(tw);
114 String name = tw.getNameString();
115
116 switch (view.getType()) {
117 case PATH:
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700118 urlBuilder.setPathPart(view.getPathPart() + "/" + name);
Dave Borowitz9de65952012-08-13 16:09:45 -0700119 break;
120 case REVISION:
121 // Got here from a tag pointing at a tree.
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700122 urlBuilder.setPathPart(name);
Dave Borowitz9de65952012-08-13 16:09:45 -0700123 break;
124 default:
125 throw new IllegalStateException(String.format(
126 "Cannot render TreeSoyData from %s view", view.getType()));
127 }
128
129 String url = urlBuilder.toUrl();
130 if (type == FileType.TREE) {
131 name += "/";
132 url += "/";
133 }
134 Map<String, String> entry = Maps.newHashMapWithExpectedSize(4);
135 entry.put("type", type.toString());
136 entry.put("name", name);
137 entry.put("url", url);
138 if (type == FileType.SYMLINK) {
139 String target = new String(
Dave Borowitz7c0a8332014-05-01 11:07:04 -0700140 reader.open(tw.getObjectId(0)).getCachedBytes(),
David Pletcherd7bdaf32014-08-27 14:50:32 -0700141 UTF_8);
Dave Borowitz9de65952012-08-13 16:09:45 -0700142 entry.put("targetName", getTargetDisplayName(target));
143 String targetUrl = resolveTargetUrl(view, target);
144 if (targetUrl != null) {
145 entry.put("targetUrl", targetUrl);
146 }
Shawn Pearce73e34532015-02-12 16:27:54 -0800147 } else if (isReadmeFile(name) && type == FileType.REGULAR_FILE) {
148 readmePath = tw.getPathString();
149 readmeId = tw.getObjectId(0);
Dave Borowitz9de65952012-08-13 16:09:45 -0700150 }
151 entries.add(entry);
152 }
153
154 Map<String, Object> data = Maps.newHashMapWithExpectedSize(3);
155 data.put("sha", treeId.name());
156 data.put("entries", entries);
157
158 if (view.getType() == GitilesView.Type.PATH
159 && view.getRevision().getPeeledType() == OBJ_COMMIT) {
160 data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
Dave Borowitzc782ebe2013-11-11 11:43:29 -0800161 data.put("archiveUrl", GitilesView.archive()
162 .copyFrom(view)
Dave Borowitz73269892013-11-13 14:23:50 -0800163 .setPathPart(Strings.emptyToNull(view.getPathPart()))
Dave Borowitzc782ebe2013-11-11 11:43:29 -0800164 .setExtension(archiveFormat.getDefaultSuffix())
165 .toUrl());
166 data.put("archiveType", archiveFormat.getShortName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700167 }
168
Shawn Pearce73e34532015-02-12 16:27:54 -0800169 if (readmeId != null && cfg.getBoolean("markdown", "render", true)) {
170 data.put("readmePath", readmePath);
171 data.put("readmeHtml", render(readmePath, readmeId));
172 }
173
Dave Borowitz9de65952012-08-13 16:09:45 -0700174 return data;
175 }
176
Shawn Pearce73e34532015-02-12 16:27:54 -0800177 /** True if the file is the default markdown file to render in tree view. */
178 private static boolean isReadmeFile(String name) {
179 return name.equalsIgnoreCase("README.md");
180 }
181
Dave Borowitz9de65952012-08-13 16:09:45 -0700182 public Map<String, Object> toSoyData(ObjectId treeId) throws MissingObjectException, IOException {
Dave Borowitz7c0a8332014-05-01 11:07:04 -0700183 TreeWalk tw = new TreeWalk(reader);
Dave Borowitz9de65952012-08-13 16:09:45 -0700184 tw.addTree(treeId);
185 tw.setRecursive(false);
186 return toSoyData(treeId, tw);
187 }
Shawn Pearce73e34532015-02-12 16:27:54 -0800188
189 private SanitizedContent render(String path, ObjectId id) {
190 try {
191 int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
192 byte[] raw = reader.open(id, Constants.OBJ_BLOB).getCachedBytes(inputLimit);
193 String md = RawParseUtils.decode(raw);
194 RootNode root = GitilesMarkdown.parseFile(view, path, md);
195 if (root == null) {
196 return null;
197 }
198
199 int imageLimit = cfg.getInt("markdown", "imageLimit", 256 << 10);
200 ImageLoader img = null;
201 if (imageLimit > 0) {
202 img = new ImageLoader(reader, view, rootTree, path, imageLimit);
203 }
204
205 return new MarkdownToHtml(view, cfg)
206 .setImageLoader(img)
207 .toSoyHtml(root);
208 } catch (LargeObjectException | IOException e) {
209 log.error(String.format("error rendering %s/%s/%s",
210 view.getRepositoryName(), view.getPathPart(), path), e);
211 return null;
212 }
213 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700214}