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">