Render README.md at bottom of repository index
If README.md exists in the HEAD tree show only the most recent
5 commits and then display the rendered README.md below that.
For Gitiles itself this is helpful as the top of README.md
explains the project and what the repository contains.
Change-Id: Ibc3b199893958fd1ecfebf2a06734eb8aa3f3375
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
new file mode 100644
index 0000000..07e21f9
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
@@ -0,0 +1,120 @@
+// Copyright 2015 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 com.google.gitiles.doc.GitilesMarkdown;
+import com.google.gitiles.doc.ImageLoader;
+import com.google.gitiles.doc.MarkdownToHtml;
+import com.google.template.soy.data.SanitizedContent;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.pegdown.ast.RootNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class ReadmeHelper {
+ private static final Logger log = LoggerFactory.getLogger(ReadmeHelper.class);
+
+ private final ObjectReader reader;
+ private final GitilesView view;
+ private final Config cfg;
+ private final RevTree rootTree;
+ private final boolean render;
+
+ private String readmePath;
+ private ObjectId readmeId;
+
+ ReadmeHelper(ObjectReader reader, GitilesView view, Config cfg,
+ RevTree rootTree) {
+ this.reader = reader;
+ this.view = view;
+ this.cfg = cfg;
+ this.rootTree = rootTree;
+ render = cfg.getBoolean("markdown", "render", true);
+ }
+
+ void scanTree(RevTree tree) throws MissingObjectException,
+ IncorrectObjectTypeException, CorruptObjectException, IOException {
+ if (render) {
+ TreeWalk tw = new TreeWalk(reader);
+ tw.setRecursive(false);
+ tw.addTree(tree);
+ while (tw.next() && !isPresent()) {
+ considerEntry(tw);
+ }
+ }
+ }
+
+ void considerEntry(TreeWalk tw) {
+ if (render
+ && FileMode.REGULAR_FILE.equals(tw.getRawMode(0))
+ && isReadmeFile(tw.getNameString())) {
+ readmePath = tw.getPathString();
+ readmeId = tw.getObjectId(0);
+ }
+ }
+
+ boolean isPresent() {
+ return readmeId != null;
+ }
+
+ String getPath() {
+ return readmePath;
+ }
+
+ SanitizedContent render() {
+ try {
+ int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
+ byte[] raw = reader.open(readmeId, Constants.OBJ_BLOB).getCachedBytes(inputLimit);
+ String md = RawParseUtils.decode(raw);
+ RootNode root = GitilesMarkdown.parseFile(view, readmePath, md);
+ if (root == null) {
+ return null;
+ }
+
+ int imageLimit = cfg.getInt("markdown", "imageLimit", 256 << 10);
+ ImageLoader img = null;
+ if (imageLimit > 0) {
+ img = new ImageLoader(reader, view, rootTree, readmePath, imageLimit);
+ }
+
+ return new MarkdownToHtml(view, cfg)
+ .setImageLoader(img)
+ .toSoyHtml(root);
+ } catch (LargeObjectException | IOException e) {
+ log.error(String.format("error rendering %s/%s",
+ view.getRepositoryName(), readmePath), e);
+ return null;
+ }
+ }
+
+ /** True if the file is the default markdown file to render in tree view. */
+ private static boolean isReadmeFile(String name) {
+ return name.equalsIgnoreCase("README.md");
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
index 7e8e573..21bf1bf 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
@@ -18,16 +18,20 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gitiles.DateFormatter.Format;
import com.google.gson.reflect.TypeToken;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.http.server.ServletUtils;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
@@ -45,6 +49,7 @@
static final int REF_LIMIT = 10;
private static final int LOG_LIMIT = 20;
+ private static final int LOG_WITH_README_LIMIT = 5;
private final TimeCache timeCache;
@@ -69,11 +74,17 @@
ObjectId headId = repo.resolve(Constants.HEAD);
if (headId != null) {
RevObject head = walk.parseAny(headId);
+ int limit = LOG_LIMIT;
+ Map<String, Object> readme = renderReadme(walk, view, access.getConfig(), head);
+ if (readme != null) {
+ data.putAll(readme);
+ limit = LOG_WITH_README_LIMIT;
+ }
// TODO(dborowitz): Handle non-commit or missing HEAD?
if (head.getType() == Constants.OBJ_COMMIT) {
walk.reset();
walk.markStart((RevCommit) head);
- paginator = new Paginator(walk, LOG_LIMIT, null);
+ paginator = new Paginator(walk, limit, null);
}
}
if (!data.containsKey("entries")) {
@@ -121,4 +132,21 @@
private static <T> List<T> trim(List<T> list) {
return list.size() > REF_LIMIT ? list.subList(0, REF_LIMIT) : list;
}
+
+ private Map<String, Object> renderReadme(RevWalk walk, GitilesView view,
+ Config cfg, RevObject head) throws IOException {
+ RevTree rootTree;
+ try {
+ rootTree = walk.parseTree(head);
+ } catch (IncorrectObjectTypeException notTreeish) {
+ return null;
+ }
+
+ ReadmeHelper readme = new ReadmeHelper(walk.getObjectReader(), view, cfg, rootTree);
+ readme.scanTree(rootTree);
+ if (readme.isPresent()) {
+ return ImmutableMap.<String, Object> of("readmeHtml", readme.render());
+ }
+ return null;
+ }
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
index 4bef0f8..6967c65 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
@@ -22,23 +22,13 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gitiles.PathServlet.FileType;
-import com.google.gitiles.doc.GitilesMarkdown;
-import com.google.gitiles.doc.ImageLoader;
-import com.google.gitiles.doc.MarkdownToHtml;
-import com.google.template.soy.data.SanitizedContent;
-import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.RawParseUtils;
-import org.pegdown.ast.RootNode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
@@ -46,8 +36,6 @@
/** Soy data converter for git trees. */
public class TreeSoyData {
- private static final Logger log = LoggerFactory.getLogger(TreeSoyData.class);
-
/**
* Number of characters to display for a symlink target. Targets longer than
* this are abbreviated for display in a tree listing.
@@ -104,9 +92,7 @@
public Map<String, Object> toSoyData(ObjectId treeId, TreeWalk tw) throws MissingObjectException,
IOException {
- String readmePath = null;
- ObjectId readmeId = null;
-
+ ReadmeHelper readme = new ReadmeHelper(reader, view, cfg, rootTree);
List<Object> entries = Lists.newArrayList();
GitilesView.Builder urlBuilder = GitilesView.path().copyFrom(view);
while (tw.next()) {
@@ -144,9 +130,8 @@
if (targetUrl != null) {
entry.put("targetUrl", targetUrl);
}
- } else if (isReadmeFile(name) && type == FileType.REGULAR_FILE) {
- readmePath = tw.getPathString();
- readmeId = tw.getObjectId(0);
+ } else {
+ readme.considerEntry(tw);
}
entries.add(entry);
}
@@ -166,49 +151,18 @@
data.put("archiveType", archiveFormat.getShortName());
}
- if (readmeId != null && cfg.getBoolean("markdown", "render", true)) {
- data.put("readmePath", readmePath);
- data.put("readmeHtml", render(readmePath, readmeId));
+ if (readme.isPresent()) {
+ data.put("readmePath", readme.getPath());
+ data.put("readmeHtml", readme.render());
}
return data;
}
- /** True if the file is the default markdown file to render in tree view. */
- private static boolean isReadmeFile(String name) {
- return name.equalsIgnoreCase("README.md");
- }
-
public Map<String, Object> toSoyData(ObjectId treeId) throws MissingObjectException, IOException {
TreeWalk tw = new TreeWalk(reader);
tw.addTree(treeId);
tw.setRecursive(false);
return toSoyData(treeId, tw);
}
-
- private SanitizedContent render(String path, ObjectId id) {
- try {
- int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
- byte[] raw = reader.open(id, Constants.OBJ_BLOB).getCachedBytes(inputLimit);
- String md = RawParseUtils.decode(raw);
- RootNode root = GitilesMarkdown.parseFile(view, path, md);
- if (root == null) {
- return null;
- }
-
- int imageLimit = cfg.getInt("markdown", "imageLimit", 256 << 10);
- ImageLoader img = null;
- if (imageLimit > 0) {
- img = new ImageLoader(reader, view, rootTree, path, imageLimit);
- }
-
- return new MarkdownToHtml(view, cfg)
- .setImageLoader(img)
- .toSoyHtml(root);
- } catch (LargeObjectException | IOException e) {
- log.error(String.format("error rendering %s/%s/%s",
- view.getRepositoryName(), view.getPathPart(), path), e);
- return null;
- }
- }
}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
index 13c187f..37e3dca 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
@@ -427,6 +427,12 @@
font-size: 9pt;
padding-top: 5px; /* VPADDING */
}
+.repository-index-doc {
+ border-top: #ddd solid 1px; /* BORDER */
+ margin-top: 5px; /* VPADDING */
+ padding-top: 5px; /* VPADDING */
+ margin-left: 200px;
+}
.doc {
border-bottom: #ddd solid 1px; /* BORDER */
}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
index aaba64b..17b0053 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
@@ -28,15 +28,27 @@
* @param tags list of tag objects with url and name keys.
* @param? moreTagsUrl URL to show more branches, if necessary.
* @param hasLog whether a log should be shown for HEAD.
+ * @param? readmeHtml optional rendered README.md contents.
*/
{template .repositoryIndex}
-{call .header}
- {param title: $repositoryName /}
- {param repositoryName: null /}
- {param menuEntries: $menuEntries /}
- {param headerVariant: $headerVariant /}
- {param breadcrumbs: $breadcrumbs /}
-{/call}
+{if $readmeHtml}
+ {call .header data="all"}
+ {param title: $repositoryName /}
+ {param repositoryName: null /}
+ {param menuEntries: $menuEntries /}
+ {param headerVariant: $headerVariant /}
+ {param breadcrumbs: $breadcrumbs /}
+ {param css: [gitiles.DOC_CSS_URL] /}
+ {/call}
+{else}
+ {call .header}
+ {param title: $repositoryName /}
+ {param repositoryName: null /}
+ {param menuEntries: $menuEntries /}
+ {param headerVariant: $headerVariant /}
+ {param breadcrumbs: $breadcrumbs /}
+ {/call}
+{/if}
{if $description or $mirroredFromUrl}
<div class="repository-description">
@@ -62,6 +74,9 @@
<div class="repository-shortlog">
{call .streamingPlaceholder /}
</div>
+ {if $readmeHtml}
+ <div class="doc repository-index-doc">{$readmeHtml}</div>
+ {/if}
</div>
<div class="repository-refs">