Gitiles is a simple browser for Git repositories It is based on JGit and uses Google Closure Templates as a templating language. Access to underlying repositories is based on a few simple interfaces; currently, there is only a simple disk-based implementation, but other implementations are possible. Features include viewing repositories by branch, shortlogs, showing individual files and diffs with syntax highlighting, with many more planned. The application itself is a standard Java servlet and is configured primarily via a git config format file. Deploying the WAR in any servlet container should be possible. In addition, a standalone server may be run with jetty-maven-plugin with `mvn package && mvn jetty:run`. Change-Id: I0ab8875b6c50f7df03b9a42b4a60923a4827bde7
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/AbstractHttpFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/AbstractHttpFilter.java new file mode 100644 index 0000000..092e12d --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/AbstractHttpFilter.java
@@ -0,0 +1,49 @@ +// 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 java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +abstract class AbstractHttpFilter implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + doFilter((HttpServletRequest) req, (HttpServletResponse) res, chain); + } + + @Override + @SuppressWarnings("unused") // Allow subclasses to throw ServletException. + public void init(FilterConfig config) throws ServletException { + // Default implementation does nothing. + } + + @Override + public void destroy() { + // Default implementation does nothing. + } + + /** @see #doFilter(ServletRequest, ServletResponse, FilterChain) */ + public abstract void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws IOException, ServletException; +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java new file mode 100644 index 0000000..70dc14e --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -0,0 +1,119 @@ +// 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 javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.net.HttpHeaders; + +import org.joda.time.Instant; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Base servlet class for Gitiles servlets that serve Soy templates. */ +public abstract class BaseServlet extends HttpServlet { + private static final String DATA_ATTRIBUTE = BaseServlet.class.getName() + "/Data"; + + static void setNotCacheable(HttpServletResponse res) { + res.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate"); + res.setHeader(HttpHeaders.PRAGMA, "no-cache"); + res.setHeader(HttpHeaders.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT"); + res.setDateHeader(HttpHeaders.DATE, new Instant().getMillis()); + } + + public static BaseServlet notFoundServlet() { + return new BaseServlet(null) { + @Override + public void service(HttpServletRequest req, HttpServletResponse res) { + res.setStatus(SC_NOT_FOUND); + } + }; + } + + public static Map<String, String> menuEntry(String text, String url) { + if (url != null) { + return ImmutableMap.of("text", text, "url", url); + } else { + return ImmutableMap.of("text", text); + } + } + + protected static Map<String, Object> getData(HttpServletRequest req) { + @SuppressWarnings("unchecked") + Map<String, Object> data = (Map<String, Object>) req.getAttribute(DATA_ATTRIBUTE); + if (data == null) { + data = Maps.newHashMap(); + req.setAttribute(DATA_ATTRIBUTE, data); + } + return data; + } + + protected final Renderer renderer; + + protected BaseServlet(Renderer renderer) { + this.renderer = renderer; + } + + /** + * Put a value into a request's Soy data map. + * <p> + * This method is intended to support a composition pattern whereby a + * {@link BaseServlet} is wrapped in a different {@link HttpServlet} that can + * update its data map. + * + * @param req in-progress request. + * @param key key. + * @param value Soy data value. + */ + public void put(HttpServletRequest req, String key, Object value) { + getData(req).put(key, value); + } + + protected void render(HttpServletRequest req, HttpServletResponse res, String templateName, + Map<String, ?> soyData) throws IOException { + try { + res.setContentType(FormatType.HTML.getMimeType()); + res.setCharacterEncoding(Charsets.UTF_8.name()); + setCacheHeaders(req, res); + + Map<String, Object> allData = getData(req); + allData.putAll(soyData); + GitilesView view = ViewFilter.getView(req); + if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) { + allData.put("repositoryName", view.getRepositoryName()); + } + if (!allData.containsKey("breadcrumbs")) { + allData.put("breadcrumbs", view.getBreadcrumbs()); + } + + res.setStatus(HttpServletResponse.SC_OK); + renderer.render(res, templateName, allData); + } finally { + req.removeAttribute(DATA_ATTRIBUTE); + } + } + + protected void setCacheHeaders(HttpServletRequest req, HttpServletResponse res) { + setNotCacheable(res); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java new file mode 100644 index 0000000..6b57cbc --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java
@@ -0,0 +1,109 @@ +// 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 org.eclipse.jgit.lib.Constants.OBJ_COMMIT; + +import com.google.common.collect.Maps; + +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.revwalk.RevWalk; +import org.eclipse.jgit.util.RawParseUtils; + +import java.io.IOException; +import java.util.Map; + +/** Soy data converter for git blobs. */ +public class BlobSoyData { + /** + * 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. + */ + private static final int MAX_FILE_SIZE = 10 << 20; + + private final GitilesView view; + private final RevWalk walk; + + public BlobSoyData(RevWalk walk, GitilesView view) { + this.view = view; + this.walk = walk; + } + + 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 = walk.getObjectReader().open(blobId, Constants.OBJ_BLOB); + String content; + try { + byte[] raw = loader.getCachedBytes(MAX_FILE_SIZE); + content = !RawText.isBinary(raw) ? RawParseUtils.decode(raw) : null; + } catch (LargeObjectException.OutOfMemory e) { + throw e; + } catch (LargeObjectException e) { + content = null; + } + + data.put("data", content); + if (content != null) { + data.put("lang", guessPrettifyLang(path, content)); + } else if (content == null) { + data.put("size", Long.toString(loader.getSize())); + } + if (path != null && view.getRevision().getPeeledType() == OBJ_COMMIT) { + data.put("logUrl", GitilesView.log().copyFrom(view).toUrl()); + } + return data; + } + + private static String guessPrettifyLang(String path, String content) { + if (content.startsWith("#!/bin/sh") || content.startsWith("#!/bin/bash")) { + return "sh"; + } else if (content.startsWith("#!/usr/bin/perl")) { + return "perl"; + } else if (content.startsWith("#!/usr/bin/python")) { + return "py"; + } else if (path == null) { + return null; + } + + int slash = path.lastIndexOf('/'); + int dot = path.lastIndexOf('.'); + String lang = ((0 < dot) && (slash < dot)) ? path.substring(dot + 1) : null; + if ("txt".equalsIgnoreCase(lang)) { + return null; + } else if ("mk".equalsIgnoreCase(lang)) { + return "sh"; + } else if ("Makefile".equalsIgnoreCase(path) + || ((0 < slash) && "Makefile".equalsIgnoreCase(path.substring(slash + 1)))) { + return "sh"; + } else { + return lang; + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java new file mode 100644 index 0000000..9f8a97d --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java
@@ -0,0 +1,287 @@ +// 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.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.template.soy.data.restricted.NullData; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.util.GitDateFormatter; +import org.eclipse.jgit.util.GitDateFormatter.Format; +import org.eclipse.jgit.util.RelativeDateFormatter; +import org.eclipse.jgit.util.io.NullOutputStream; + +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +/** Soy data converter for git commits. */ +public class CommitSoyData { + /** Valid sets of keys to include in Soy data for commits. */ + public static enum KeySet { + DETAIL("author", "committer", "sha", "tree", "treeUrl", "parents", "message", "logUrl"), + DETAIL_DIFF_TREE(DETAIL, "diffTree"), + SHORTLOG("abbrevSha", "url", "shortMessage", "author", "branches", "tags"), + DEFAULT(DETAIL); + + private final Set<String> keys; + + private KeySet(String... keys) { + this.keys = ImmutableSet.copyOf(keys); + } + + private KeySet(KeySet other, String... keys) { + this.keys = ImmutableSet.<String> builder().addAll(other.keys).add(keys).build(); + } + } + + private final Linkifier linkifier; + private final HttpServletRequest req; + private final Repository repo; + private final RevWalk walk; + private final GitilesView view; + private final Map<AnyObjectId, Set<Ref>> refsById; + private final GitDateFormatter dateFormatter; + + // TODO(dborowitz): This constructor is getting a bit ridiculous. + public CommitSoyData(@Nullable Linkifier linkifier, HttpServletRequest req, Repository repo, + RevWalk walk, GitilesView view) { + this(linkifier, req, repo, walk, view, null); + } + + public CommitSoyData(@Nullable Linkifier linkifier, HttpServletRequest req, Repository repo, + RevWalk walk, GitilesView view, @Nullable Map<AnyObjectId, Set<Ref>> refsById) { + this.linkifier = linkifier; + this.req = req; + this.repo = repo; + this.walk = walk; + this.view = view; + this.refsById = refsById; + this.dateFormatter = new GitDateFormatter(Format.DEFAULT); + } + + public Map<String, Object> toSoyData(RevCommit commit, KeySet keys) throws IOException { + Map<String, Object> data = Maps.newHashMapWithExpectedSize(KeySet.DEFAULT.keys.size()); + if (keys.keys.contains("author")) { + data.put("author", toSoyData(commit.getAuthorIdent(), dateFormatter)); + } + if (keys.keys.contains("committer")) { + data.put("committer", toSoyData(commit.getCommitterIdent(), dateFormatter)); + } + if (keys.keys.contains("sha")) { + data.put("sha", ObjectId.toString(commit)); + } + if (keys.keys.contains("abbrevSha")) { + ObjectReader reader = repo.getObjectDatabase().newReader(); + try { + data.put("abbrevSha", reader.abbreviate(commit).name()); + } finally { + reader.release(); + } + } + if (keys.keys.contains("url")) { + data.put("url", GitilesView.revision() + .copyFrom(view) + .setRevision(commit) + .toUrl()); + } + if (keys.keys.contains("logUrl")) { + Revision rev = view.getRevision(); + GitilesView.Builder logView = GitilesView.log() + .copyFrom(view) + .setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit) + .setOldRevision(Revision.NULL) + .setTreePath(null); + data.put("logUrl", logView.toUrl()); + } + if (keys.keys.contains("tree")) { + data.put("tree", ObjectId.toString(commit.getTree())); + } + if (keys.keys.contains("treeUrl")) { + data.put("treeUrl", GitilesView.path().copyFrom(view).setTreePath("/").toUrl()); + } + if (keys.keys.contains("parents")) { + data.put("parents", toSoyData(view, commit.getParents())); + } + if (keys.keys.contains("shortMessage")) { + data.put("shortMessage", commit.getShortMessage()); + } + if (keys.keys.contains("branches")) { + data.put("branches", getRefsById(commit, Constants.R_HEADS)); + } + if (keys.keys.contains("tags")) { + data.put("tags", getRefsById(commit, Constants.R_TAGS)); + } + if (keys.keys.contains("message")) { + if (linkifier != null) { + data.put("message", linkifier.linkify(req, commit.getFullMessage())); + } else { + data.put("message", commit.getFullMessage()); + } + } + if (keys.keys.contains("diffTree")) { + data.put("diffTree", computeDiffTree(commit)); + } + checkState(keys.keys.size() == data.size(), "bad commit data keys: %s != %s", keys.keys, + data.keySet()); + return ImmutableMap.copyOf(data); + } + + public Map<String, Object> toSoyData(RevCommit commit) throws IOException { + return toSoyData(commit, KeySet.DEFAULT); + } + + // TODO(dborowitz): Extract this. + static Map<String, String> toSoyData(PersonIdent ident, GitDateFormatter dateFormatter) { + return ImmutableMap.of( + "name", ident.getName(), + "email", ident.getEmailAddress(), + "time", dateFormatter.formatDate(ident), + // TODO(dborowitz): Switch from relative to absolute at some threshold. + "relativeTime", RelativeDateFormatter.format(ident.getWhen())); + } + + private List<Map<String, String>> toSoyData(GitilesView view, RevCommit[] parents) { + List<Map<String, String>> result = Lists.newArrayListWithCapacity(parents.length); + int i = 1; + // TODO(dborowitz): Render something slightly different when we're actively + // viewing a diff against one of the parents. + for (RevCommit parent : parents) { + String name = parent.name(); + GitilesView.Builder diff = GitilesView.diff().copyFrom(view).setTreePath(""); + String parentName; + if (parents.length == 1) { + parentName = view.getRevision().getName() + "^"; + } else { + parentName = view.getRevision().getName() + "^" + (i++); + } + result.add(ImmutableMap.of( + "sha", name, + "url", GitilesView.revision() + .copyFrom(view) + .setRevision(parentName, parent) + .toUrl(), + "diffUrl", diff.setOldRevision(parentName, parent).toUrl())); + } + return result; + } + + private AbstractTreeIterator getTreeIterator(RevWalk walk, RevCommit commit) throws IOException { + CanonicalTreeParser p = new CanonicalTreeParser(); + p.reset(walk.getObjectReader(), walk.parseTree(walk.parseCommit(commit).getTree())); + return p; + } + + private Object computeDiffTree(RevCommit commit) throws IOException { + AbstractTreeIterator oldTree; + GitilesView.Builder diffUrl = GitilesView.diff().copyFrom(view) + .setTreePath(""); + switch (commit.getParentCount()) { + case 0: + oldTree = new EmptyTreeIterator(); + diffUrl.setOldRevision(Revision.NULL); + break; + case 1: + oldTree = getTreeIterator(walk, commit.getParent(0)); + diffUrl.setOldRevision(view.getRevision().getName() + "^", commit.getParent(0)); + break; + default: + // TODO(dborowitz): handle merges + return NullData.INSTANCE; + } + AbstractTreeIterator newTree = getTreeIterator(walk, commit); + + DiffFormatter diff = new DiffFormatter(NullOutputStream.INSTANCE); + try { + diff.setRepository(repo); + diff.setDetectRenames(true); + + List<Object> result = Lists.newArrayList(); + for (DiffEntry e : diff.scan(oldTree, newTree)) { + Map<String, Object> entry = Maps.newHashMapWithExpectedSize(5); + entry.put("path", e.getNewPath()); + entry.put("url", GitilesView.path() + .copyFrom(view) + .setTreePath(e.getNewPath()) + .toUrl()); + entry.put("diffUrl", diffUrl.setAnchor("F" + result.size()).toUrl()); + entry.put("changeType", e.getChangeType().toString()); + if (e.getChangeType() == ChangeType.COPY || e.getChangeType() == ChangeType.RENAME) { + entry.put("oldPath", e.getOldPath()); + } + result.add(entry); + } + return result; + } finally { + diff.release(); + } + } + + private static final Comparator<Map<String, String>> NAME_COMPARATOR = + new Comparator<Map<String, String>>() { + @Override + public int compare(Map<String, String> o1, Map<String, String> o2) { + return o1.get("name").compareTo(o2.get("name")); + } + }; + + private List<Map<String, String>> getRefsById(ObjectId id, String prefix) { + checkNotNull(refsById, "must pass in ID to ref map to look up refs by ID"); + Set<Ref> refs = refsById.get(id); + if (refs == null) { + return ImmutableList.of(); + } + List<Map<String, String>> result = Lists.newArrayListWithCapacity(refs.size()); + for (Ref ref : refs) { + if (ref.getName().startsWith(prefix)) { + result.add(ImmutableMap.of( + "name", ref.getName().substring(prefix.length()), + "url", GitilesView.revision() + .copyFrom(view) + .setRevision(Revision.unpeeled(ref.getName(), ref.getObjectId())) + .toUrl())); + } + } + Collections.sort(result, NAME_COMPARATOR); + return result; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java b/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java new file mode 100644 index 0000000..4f8733b --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java
@@ -0,0 +1,141 @@ +// 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 com.google.common.base.Predicates; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import org.eclipse.jgit.lib.Config; +import org.joda.time.Duration; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Utilities for working with {@link Config} objects. */ +public class ConfigUtil { + /** + * Read a duration value from the configuration. + * <p> + * Durations can be written as expressions, for example {@code "1 s"} or + * {@code "5 days"}. If units are not specified, milliseconds are assumed. + * + * @param config JGit config object. + * @param section section to read, e.g. "google" + * @param subsection subsection to read, e.g. "bigtable" + * @param name variable to read, e.g. "deadline". + * @param defaultValue value to use when the value is not assigned. + * @return a standard duration representing the time read, or defaultValue. + */ + public static Duration getDuration(Config config, String section, String subsection, String name, + Duration defaultValue) { + String valStr = config.getString(section, subsection, name); + if (valStr == null) { + return defaultValue; + } + + valStr = valStr.trim(); + if (valStr.length() == 0) { + return defaultValue; + } + + Matcher m = matcher("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$", valStr); + if (!m.matches()) { + String key = section + (subsection != null ? "." + subsection : "") + "." + name; + throw new IllegalStateException("Not time unit: " + key + " = " + valStr); + } + + String digits = m.group(1); + String unitName = m.group(2).trim(); + + TimeUnit unit; + if ("".equals(unitName)) { + unit = TimeUnit.MILLISECONDS; + } else if (anyOf(unitName, "ms", "millis", "millisecond", "milliseconds")) { + unit = TimeUnit.MILLISECONDS; + } else if (anyOf(unitName, "s", "sec", "second", "seconds")) { + unit = TimeUnit.SECONDS; + } else if (anyOf(unitName, "m", "min", "minute", "minutes")) { + unit = TimeUnit.MINUTES; + } else if (anyOf(unitName, "h", "hr", "hour", "hours")) { + unit = TimeUnit.HOURS; + } else if (anyOf(unitName, "d", "day", "days")) { + unit = TimeUnit.DAYS; + } else { + String key = section + (subsection != null ? "." + subsection : "") + "." + name; + throw new IllegalStateException("Not time unit: " + key + " = " + valStr); + } + + try { + if (digits.indexOf('.') == -1) { + long val = Long.parseLong(digits); + return new Duration(val * TimeUnit.MILLISECONDS.convert(1, unit)); + } else { + double val = Double.parseDouble(digits); + return new Duration((long) (val * TimeUnit.MILLISECONDS.convert(1, unit))); + } + } catch (NumberFormatException nfe) { + String key = section + (subsection != null ? "." + subsection : "") + "." + name; + throw new IllegalStateException("Not time unit: " + key + " = " + valStr, nfe); + } + } + + /** + * Get a {@link CacheBuilder} from a config. + * + * @param config JGit config object. + * @param name name of the cache subsection under the "cache" section. + * @return a new cache builder. + */ + public static CacheBuilder<Object, Object> getCacheBuilder(Config config, String name) { + CacheBuilder<Object, Object> b = CacheBuilder.newBuilder(); + try { + if (config.getString("cache", name, "maximumWeight") != null) { + b.maximumWeight(config.getLong("cache", name, "maximumWeight", 20 << 20)); + } + if (config.getString("cache", name, "maximumSize") != null) { + b.maximumSize(config.getLong("cache", name, "maximumSize", 16384)); + } + Duration expireAfterWrite = getDuration(config, "cache", name, "expireAfterWrite", null); + if (expireAfterWrite != null) { + b.expireAfterWrite(expireAfterWrite.getMillis(), TimeUnit.MILLISECONDS); + } + Duration expireAfterAccess = getDuration(config, "cache", name, "expireAfterAccess", null); + if (expireAfterAccess != null) { + b.expireAfterAccess(expireAfterAccess.getMillis(), TimeUnit.MILLISECONDS); + } + // Add other methods as needed. + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error getting CacheBuilder for " + name, e); + } catch (IllegalStateException e) { + throw new IllegalStateException("Error getting CacheBuilder for " + name, e); + } + return b; + } + + private static Matcher matcher(String pattern, String valStr) { + return Pattern.compile(pattern).matcher(valStr); + } + + private static boolean anyOf(String a, String... cases) { + return Iterables.any(ImmutableList.copyOf(cases), + Predicates.equalTo(a.toLowerCase())); + } + + private ConfigUtil() { + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java new file mode 100644 index 0000000..e56518a --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
@@ -0,0 +1,57 @@ +// 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 com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import com.google.template.soy.SoyFileSet; +import com.google.template.soy.tofu.SoyTofu; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; + +/** Renderer that reloads Soy templates from the filesystem on every request. */ +public class DebugRenderer extends Renderer { + public DebugRenderer(String staticPrefix, String customTemplatesFilename, + final String soyTemplatesRoot) { + super( + new Function<String, URL>() { + @Override + public URL apply(String name) { + return toFileURL(soyTemplatesRoot + File.separator + name); + } + }, + ImmutableMap.<String, String> of(), staticPrefix, + toFileURL(customTemplatesFilename)); + } + + @Override + protected SoyTofu getTofu() { + SoyFileSet.Builder builder = new SoyFileSet.Builder() + .setCompileTimeGlobals(globals); + for (URL template : templates) { + try { + checkState(new File(template.toURI()).exists(), "Missing Soy template %s", template); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + builder.add(template); + } + return builder.build().compileToTofu(); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java new file mode 100644 index 0000000..5b6d822 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultAccess.java
@@ -0,0 +1,239 @@ +// 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.checkNotNull; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Queues; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.resolver.FileResolver; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.IO; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * Default implementation of {@link GitilesAccess} with local repositories. + * <p> + * Repositories are scanned on-demand under the given path, configured by + * default from {@code gitiles.basePath}. There is no access control beyond what + * user the JVM is running under. + */ +public class DefaultAccess implements GitilesAccess { + private static final String ANONYMOUS_USER_KEY = "anonymous user"; + + public static class Factory implements GitilesAccess.Factory { + private final File basePath; + private final String canonicalBasePath; + private final String baseGitUrl; + private final FileResolver<HttpServletRequest> resolver; + + Factory(File basePath, String baseGitUrl, FileResolver<HttpServletRequest> resolver) + throws IOException { + this.basePath = checkNotNull(basePath, "basePath"); + this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); + this.resolver = checkNotNull(resolver, "resolver"); + this.canonicalBasePath = basePath.getCanonicalPath(); + } + + @Override + public GitilesAccess forRequest(HttpServletRequest req) { + String path = req.getPathInfo(); + String repositoryPath; + if (path == null || path == "/") { + repositoryPath = null; + } else { + int slashPlus = path.indexOf("/+/"); + if (slashPlus >= 0) { + repositoryPath = path.substring(0, slashPlus); + } else if (path.endsWith("/+")) { + repositoryPath = path.substring(0, path.length() - 2); + } else { + repositoryPath = path; + } + } + return newAccess(basePath, canonicalBasePath, baseGitUrl, resolver, req); + } + + protected DefaultAccess newAccess(File basePath, String canonicalBasePath, String baseGitUrl, + FileResolver<HttpServletRequest> resolver, HttpServletRequest req) { + return new DefaultAccess(basePath, canonicalBasePath, baseGitUrl, resolver, req); + } + } + + protected final File basePath; + protected final String canonicalBasePath; + protected final String baseGitUrl; + protected final FileResolver<HttpServletRequest> resolver; + protected final HttpServletRequest req; + + protected DefaultAccess(File basePath, String canonicalBasePath, String baseGitUrl, + FileResolver<HttpServletRequest> resolver, HttpServletRequest req) { + this.basePath = checkNotNull(basePath, "basePath"); + this.canonicalBasePath = checkNotNull(canonicalBasePath, "canonicalBasePath"); + this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); + this.resolver = checkNotNull(resolver, "resolver"); + this.req = checkNotNull(req, "req"); + } + + @Override + public Map<String, RepositoryDescription> listRepositories(Set<String> branches) + throws IOException { + Map<String, RepositoryDescription> repos = Maps.newTreeMap(); + for (Repository repo : scanRepositories(basePath, req)) { + repos.put(getRepositoryName(repo), buildDescription(repo, branches)); + repo.close(); + } + return repos; + } + + @Override + public Object getUserKey() { + // Always return the same anonymous user key (effectively running with the + // same user permissions as the JVM). Subclasses may override this behavior. + return ANONYMOUS_USER_KEY; + } + + @Override + public String getRepositoryName() { + return getRepositoryName(ServletUtils.getRepository(req)); + } + + @Override + public RepositoryDescription getRepositoryDescription() throws IOException { + return buildDescription(ServletUtils.getRepository(req), Collections.<String> emptySet()); + } + + private String getRepositoryName(Repository repo) { + String path = getRelativePath(repo); + if (repo.isBare() && path.endsWith(".git")) { + path = path.substring(0, path.length() - 4); + } + return path; + } + + private String getRelativePath(Repository repo) { + String path = repo.isBare() ? repo.getDirectory().getPath() : repo.getDirectory().getParent(); + if (repo.isBare()) { + path = repo.getDirectory().getPath(); + if (path.endsWith(".git")) { + path = path.substring(0, path.length() - 4); + } + } else { + path = repo.getDirectory().getParent(); + } + return getRelativePath(path); + } + + private String getRelativePath(String path) { + String base = basePath.getPath(); + if (path.startsWith(base)) { + return path.substring(base.length() + 1); + } + if (path.startsWith(canonicalBasePath)) { + return path.substring(canonicalBasePath.length() + 1); + } + throw new IllegalStateException(String.format( + "Repository path %s is outside base path %s", path, base)); + } + + private String loadDescriptionText(Repository repo) throws IOException { + String desc = null; + StoredConfig config = repo.getConfig(); + IOException configError = null; + try { + config.load(); + desc = config.getString("gitweb", null, "description"); + } catch (ConfigInvalidException e) { + configError = new IOException(e); + } + if (desc == null) { + File descFile = new File(repo.getDirectory(), "description"); + if (descFile.exists()) { + desc = new String(IO.readFully(descFile)); + } else if (configError != null) { + throw configError; + } + } + return desc; + } + + private RepositoryDescription buildDescription(Repository repo, Set<String> branches) + throws IOException { + RepositoryDescription desc = new RepositoryDescription(); + desc.name = getRepositoryName(repo); + desc.cloneUrl = baseGitUrl + getRelativePath(repo); + desc.description = loadDescriptionText(repo); + if (!branches.isEmpty()) { + desc.branches = Maps.newLinkedHashMap(); + for (String name : branches) { + Ref ref = repo.getRef(normalizeRefName(name)); + if ((ref != null) && (ref.getObjectId() != null)) { + desc.branches.put(name, ref.getObjectId().name()); + } + } + } + return desc; + } + + private static String normalizeRefName(String name) { + if (name.startsWith("refs/")) { + return name; + } + return "refs/heads/" + name; + } + + private Collection<Repository> scanRepositories(final File basePath, final HttpServletRequest req) throws IOException { + List<Repository> repos = Lists.newArrayList(); + Queue<File> todo = Queues.newArrayDeque(); + File[] baseFiles = basePath.listFiles(); + if (baseFiles == null) { + throw new IOException("base path is not a directory: " + basePath.getPath()); + } + todo.addAll(Arrays.asList(baseFiles)); + while (!todo.isEmpty()) { + File file = todo.remove(); + try { + repos.add(resolver.open(req, getRelativePath(file.getPath()))); + } catch (RepositoryNotFoundException e) { + File[] children = file.listFiles(); + if (children != null) { + todo.addAll(Arrays.asList(children)); + } + } catch (ServiceNotEnabledException e) { + throw new IOException(e); + } + } + return repos; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java new file mode 100644 index 0000000..f4bd1fb --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
@@ -0,0 +1,59 @@ +// 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 com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import com.google.template.soy.SoyFileSet; +import com.google.template.soy.tofu.SoyTofu; + +import java.net.URL; +import java.util.Map; + +/** Renderer that precompiles Soy and uses static precompiled CSS. */ +public class DefaultRenderer extends Renderer { + private final SoyTofu tofu; + + DefaultRenderer() { + this("", null); + } + + public DefaultRenderer(String staticPrefix, URL customTemplates) { + this(ImmutableMap.<String, String> of(), staticPrefix, customTemplates); + } + + public DefaultRenderer(Map<String, String> globals, String staticPrefix, URL customTemplates) { + super( + new Function<String, URL>() { + @Override + public URL apply(String name) { + return Resources.getResource(Renderer.class, "templates/" + name); + } + }, + globals, staticPrefix, customTemplates); + SoyFileSet.Builder builder = new SoyFileSet.Builder() + .setCompileTimeGlobals(this.globals); + for (URL template : templates) { + builder.add(template); + } + tofu = builder.build().compileToTofu(); + } + + @Override + protected SoyTofu getTofu() { + return tofu; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultUrls.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultUrls.java new file mode 100644 index 0000000..6495b5d --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultUrls.java
@@ -0,0 +1,60 @@ +// 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.checkNotNull; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import javax.servlet.http.HttpServletRequest; + +/** + * Default implementation of {@link GitilesUrls}. + * <p> + * This implementation uses statically-configured defaults, and thus assumes + * that the servlet is running a single virtual host. + */ +class DefaultUrls implements GitilesUrls { + private final String canonicalHostName; + private final String baseGitUrl; + private final String baseGerritUrl; + + DefaultUrls(String canonicalHostName, String baseGitUrl, String baseGerritUrl) + throws UnknownHostException { + if (canonicalHostName != null) { + this.canonicalHostName = canonicalHostName; + } else { + this.canonicalHostName = InetAddress.getLocalHost().getCanonicalHostName(); + } + this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl"); + this.baseGerritUrl = checkNotNull(baseGerritUrl, "baseGerritUrl"); + } + + @Override + public String getHostName(HttpServletRequest req) { + return canonicalHostName; + } + + @Override + public String getBaseGitUrl(HttpServletRequest req) { + return baseGitUrl; + } + + @Override + public String getBaseGerritUrl(HttpServletRequest req) { + return baseGerritUrl; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java new file mode 100644 index 0000000..2408cd7 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
@@ -0,0 +1,162 @@ +// 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.checkNotNull; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import com.google.common.base.Charsets; + +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves an HTML page with all the diffs for a commit. */ +public class DiffServlet extends BaseServlet { + private static final String PLACEHOLDER = "id=\"DIFF_OUTPUT_BLOCK\""; + + private final Linkifier linkifier; + + public DiffServlet(Renderer renderer, Linkifier linkifier) { + super(renderer); + this.linkifier = checkNotNull(linkifier, "linkifier"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + GitilesView view = ViewFilter.getView(req); + Repository repo = ServletUtils.getRepository(req); + + RevWalk walk = new RevWalk(repo); + try { + boolean showCommit; + AbstractTreeIterator oldTree; + AbstractTreeIterator newTree; + try { + // If we are viewing the diff between a commit and one of its parents, + // include the commit detail in the rendered page. + showCommit = isParentOf(walk, view.getOldRevision(), view.getRevision()); + oldTree = getTreeIterator(walk, view.getOldRevision().getId()); + newTree = getTreeIterator(walk, view.getRevision().getId()); + } catch (MissingObjectException e) { + res.setStatus(SC_NOT_FOUND); + return; + } catch (IncorrectObjectTypeException e) { + res.setStatus(SC_NOT_FOUND); + return; + } + + Map<String, Object> data = getData(req); + data.put("title", "Diff - " + view.getRevisionRange()); + if (showCommit) { + data.put("commit", new CommitSoyData(linkifier, req, repo, walk, view) + .toSoyData(walk.parseCommit(view.getRevision().getId()))); + } + if (!data.containsKey("repositoryName") && (view.getRepositoryName() != null)) { + data.put("repositoryName", view.getRepositoryName()); + } + if (!data.containsKey("breadcrumbs")) { + data.put("breadcrumbs", view.getBreadcrumbs()); + } + + String[] html = renderAndSplit(data); + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType(FormatType.HTML.getMimeType()); + res.setCharacterEncoding(Charsets.UTF_8.name()); + setCacheHeaders(req, res); + + OutputStream out = res.getOutputStream(); + try { + out.write(html[0].getBytes(Charsets.UTF_8)); + formatHtmlDiff(out, repo, walk, oldTree, newTree, view.getTreePath()); + out.write(html[1].getBytes(Charsets.UTF_8)); + } finally { + out.close(); + } + } finally { + walk.release(); + } + } + + private static boolean isParentOf(RevWalk walk, Revision oldRevision, Revision newRevision) + throws MissingObjectException, IncorrectObjectTypeException, IOException { + RevCommit newCommit = walk.parseCommit(newRevision.getId()); + if (newCommit.getParentCount() > 0) { + return Arrays.asList(newCommit.getParents()).contains(oldRevision.getId()); + } else { + return oldRevision == Revision.NULL; + } + } + + private String[] renderAndSplit(Map<String, Object> data) { + String html = renderer.newRenderer("gitiles.diffDetail") + .setData(data) + .render(); + int id = html.indexOf(PLACEHOLDER); + if (id < 0) { + throw new IllegalStateException("Template must contain " + PLACEHOLDER); + } + + int lt = html.lastIndexOf('<', id); + int gt = html.indexOf('>', id + PLACEHOLDER.length()); + return new String[] {html.substring(0, lt), html.substring(gt + 1)}; + } + + private void formatHtmlDiff(OutputStream out, + Repository repo, RevWalk walk, + AbstractTreeIterator oldTree, AbstractTreeIterator newTree, + String path) + throws IOException { + DiffFormatter diff = new HtmlDiffFormatter(renderer, out); + try { + if (!path.equals("")) { + diff.setPathFilter(PathFilter.create(path)); + } + diff.setRepository(repo); + diff.setDetectRenames(true); + diff.format(oldTree, newTree); + } finally { + diff.release(); + } + } + + private static AbstractTreeIterator getTreeIterator(RevWalk walk, ObjectId id) + throws IOException { + if (!id.equals(ObjectId.zeroId())) { + CanonicalTreeParser p = new CanonicalTreeParser(); + p.reset(walk.getObjectReader(), walk.parseTree(id)); + return p; + } else { + return new EmptyTreeIterator(); + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/FormatType.java b/gitiles-servlet/src/main/java/com/google/gitiles/FormatType.java new file mode 100644 index 0000000..9417a4c --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/FormatType.java
@@ -0,0 +1,76 @@ +// 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 com.google.common.base.Strings; +import com.google.common.net.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; + +/** Type of formatting to use in the response to the client. */ +public enum FormatType { + HTML("text/html"), + TEXT("text/plain"), + JSON("application/json"), + DEFAULT("*/*"); + + private static final String FORMAT_TYPE_ATTRIBUTE = FormatType.class.getName(); + + public static FormatType getFormatType(HttpServletRequest req) { + FormatType result = (FormatType) req.getAttribute(FORMAT_TYPE_ATTRIBUTE); + if (result != null) { + return result; + } + + String format = req.getParameter("format"); + if (format != null) { + for (FormatType type : FormatType.values()) { + if (format.equalsIgnoreCase(type.name())) { + return set(req, type); + } + } + throw new IllegalArgumentException("Invalid format " + format); + } + + String accept = req.getHeader(HttpHeaders.ACCEPT); + if (Strings.isNullOrEmpty(accept)) { + return set(req, DEFAULT); + } + + for (String p : accept.split("[ ,;][ ,;]*")) { + for (FormatType type : FormatType.values()) { + if (p.equals(type.mimeType)) { + return set(req, type != HTML ? type : DEFAULT); + } + } + } + return set(req, DEFAULT); + } + + private static FormatType set(HttpServletRequest req, FormatType format) { + req.setAttribute(FORMAT_TYPE_ATTRIBUTE, format); + return format; + } + + private final String mimeType; + + private FormatType(String mimeType) { + this.mimeType = mimeType; + } + + public String getMimeType() { + return mimeType; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java new file mode 100644 index 0000000..648d709 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java
@@ -0,0 +1,68 @@ +// 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 org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * Git storage interface for Gitiles. + * <p> + * Each instance is associated with a single end-user request, which implicitly + * includes information about the host and repository. + */ +public interface GitilesAccess { + /** Factory for per-request access. */ + public interface Factory { + public GitilesAccess forRequest(HttpServletRequest req); + } + + /** + * List repositories on the host. + * + * @param branches branches to list along with each repository. + * @return map of repository names to descriptions. + * @throws ServiceNotEnabledException to trigger an HTTP 403 Forbidden + * (matching behavior in {@link org.eclipse.jgit.http.server.RepositoryFilter}). + * @throws ServiceNotAuthorizedException to trigger an HTTP 401 Unauthorized + * (matching behavior in {@link org.eclipse.jgit.http.server.RepositoryFilter}). + * @throws IOException if an error occurred. + */ + public Map<String, RepositoryDescription> listRepositories(Set<String> branches) + throws ServiceNotEnabledException, ServiceNotAuthorizedException, IOException; + + /** + * @return an opaque object that uniquely identifies the end-user making the + * request, and supports {@link #equals(Object)} and {@link #hashCode()}. + * Never null. + */ + public Object getUserKey(); + + /** @return the repository name associated with the request. */ + public String getRepositoryName(); + + /** + * @return the description attached to the repository of this request. + * @throws IOException an error occurred reading the description string from + * the repository. + */ + public RepositoryDescription getRepositoryDescription() throws IOException; +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java new file mode 100644 index 0000000..b2cf9ec --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -0,0 +1,362 @@ +// 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.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gitiles.GitilesServlet.STATIC_PREFIX; +import static com.google.gitiles.ViewFilter.getRegexGroup; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Maps; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.http.server.RepositoryFilter; +import org.eclipse.jgit.http.server.glue.MetaFilter; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.resolver.FileResolver; +import org.eclipse.jgit.transport.resolver.RepositoryResolver; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.FS; + +import java.io.File; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * MetaFilter to serve Gitiles. + * <p> + * Do not use directly; use {@link GitilesServlet}. + */ +class GitilesFilter extends MetaFilter { + private static final String CONFIG_PATH_PARAM = "configPath"; + + // The following regexes have the following capture groups: + // 1. The whole string, which causes RegexPipeline to set REGEX_GROUPS but + // not otherwise modify the original request. + // 2. The repository name part, before /<CMD>. + // 3. The command, <CMD>, with no slashes and beginning with +. Commands have + // names analogous to (but not exactly the same as) git command names, such + // as "+log" and "+show". The bare command "+" maps to one of the other + // commands based on the revision/path, and may change over time. + // 4. The revision/path part, after /<CMD> (called just "path" below). This is + // split into a revision and a path by RevisionParser. + + private static final String CMD = "\\+[a-z0-9-]*"; + + @VisibleForTesting + static final Pattern ROOT_REGEX = Pattern.compile("" + + "^( " // 1. Everything + + " /* " // Excess slashes + + " (/) " // 2. Repo name (just slash) + + " () " // 3. Command + + " () " // 4. Path + + ")$ ", + Pattern.COMMENTS); + + @VisibleForTesting + static final Pattern REPO_REGEX = Pattern.compile("" + + "^( " // 1. Everything + + " /* " // Excess slashes + + " ( " // 2. Repo name + + " / " // Leading slash + + " (?:.(?! " // Anything, as long as it's not followed by... + + " /" + CMD + "/ " // the special "/<CMD>/" separator, + + " |/" + CMD + "$ " // or "/<CMD>" at the end of the string + + " ))*? " + + " ) " + + " /* " // Trailing slashes + + " () " // 3. Command + + " () " // 4. Path + + ")$ ", + Pattern.COMMENTS); + + @VisibleForTesting + static final Pattern REPO_PATH_REGEX = Pattern.compile("" + + "^( " // 1. Everything + + " /* " // Excess slashes + + " ( " // 2. Repo name + + " / " // Leading slash + + " .*? " // Anything, non-greedy + + " ) " + + " /(" + CMD + ")" // 3. Command + + " ( " // 4. Path + + " (?:/.*)? " // Slash path, or nothing. + + " ) " + + ")$ ", + Pattern.COMMENTS); + + private static class DispatchFilter extends AbstractHttpFilter { + private final ListMultimap<GitilesView.Type, Filter> filters; + private final Map<GitilesView.Type, HttpServlet> servlets; + + private DispatchFilter(ListMultimap<GitilesView.Type, Filter> filters, + Map<GitilesView.Type, HttpServlet> servlets) { + this.filters = LinkedListMultimap.create(filters); + this.servlets = ImmutableMap.copyOf(servlets); + for (GitilesView.Type type : GitilesView.Type.values()) { + checkState(servlets.containsKey(type), "Missing handler for view %s", type); + } + } + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws IOException, ServletException { + GitilesView view = checkNotNull(ViewFilter.getView(req)); + final Iterator<Filter> itr = filters.get(view.getType()).iterator(); + final HttpServlet servlet = servlets.get(view.getType()); + new FilterChain() { + @Override + public void doFilter(ServletRequest req, ServletResponse res) + throws IOException, ServletException { + if (itr.hasNext()) { + itr.next().doFilter(req, res, this); + } else { + servlet.service(req, res); + } + } + }.doFilter(req, res); + } + } + + private final ListMultimap<GitilesView.Type, Filter> filters = LinkedListMultimap.create(); + private final Map<GitilesView.Type, HttpServlet> servlets = Maps.newHashMap(); + + private Renderer renderer; + private GitilesUrls urls; + private Linkifier linkifier; + private GitilesAccess.Factory accessFactory; + private RepositoryResolver<HttpServletRequest> resolver; + private VisibilityCache visibilityCache; + private boolean initialized; + + GitilesFilter() { + } + + GitilesFilter( + Renderer renderer, + GitilesUrls urls, + GitilesAccess.Factory accessFactory, + final RepositoryResolver<HttpServletRequest> resolver, + VisibilityCache visibilityCache) { + this.renderer = checkNotNull(renderer, "renderer"); + this.urls = checkNotNull(urls, "urls"); + this.accessFactory = checkNotNull(accessFactory, "accessFactory"); + this.visibilityCache = checkNotNull(visibilityCache, "visibilityCache"); + this.linkifier = new Linkifier(urls); + this.resolver = wrapResolver(resolver); + } + + @Override + public synchronized void init(FilterConfig config) throws ServletException { + super.init(config); + setDefaultFields(config); + + for (GitilesView.Type type : GitilesView.Type.values()) { + if (!servlets.containsKey(type)) { + servlets.put(type, getDefaultHandler(type)); + } + } + + Filter repositoryFilter = new RepositoryFilter(resolver); + Filter viewFilter = new ViewFilter(accessFactory, urls, visibilityCache); + Filter dispatchFilter = new DispatchFilter(filters, servlets); + String browserCssName; + String prettifyCssName; + String prettifyJsName; + + serveRegex(ROOT_REGEX) + .through(viewFilter) + .through(dispatchFilter); + + serveRegex(REPO_REGEX) + .through(repositoryFilter) + .through(viewFilter) + .through(dispatchFilter); + + serveRegex(REPO_PATH_REGEX) + .through(repositoryFilter) + .through(viewFilter) + .through(dispatchFilter); + + initialized = true; + } + + public synchronized BaseServlet getDefaultHandler(GitilesView.Type view) { + checkNotInitialized(); + switch (view) { + case HOST_INDEX: + return new HostIndexServlet(renderer, urls, accessFactory); + case REPOSITORY_INDEX: + return new RepositoryIndexServlet(renderer, accessFactory); + case REVISION: + return new RevisionServlet(renderer, linkifier); + case PATH: + return new PathServlet(renderer); + case DIFF: + return new DiffServlet(renderer, linkifier); + case LOG: + return new LogServlet(renderer, linkifier); + default: + throw new IllegalArgumentException("Invalid view type: " + view); + } + } + + synchronized void addFilter(GitilesView.Type view, Filter filter) { + checkNotInitialized(); + filters.put(checkNotNull(view, "view"), checkNotNull(filter, "filter for %s", view)); + } + + synchronized void setHandler(GitilesView.Type view, HttpServlet handler) { + checkNotInitialized(); + servlets.put(checkNotNull(view, "view"), + checkNotNull(handler, "handler for %s", view)); + } + + private synchronized void checkNotInitialized() { + checkState(!initialized, "Gitiles already initialized"); + } + + private static RepositoryResolver<HttpServletRequest> wrapResolver( + final RepositoryResolver<HttpServletRequest> resolver) { + checkNotNull(resolver, "resolver"); + return new RepositoryResolver<HttpServletRequest>() { + @Override + public Repository open(HttpServletRequest req, String name) + throws RepositoryNotFoundException, ServiceNotAuthorizedException, + ServiceNotEnabledException, ServiceMayNotContinueException { + return resolver.open(req, ViewFilter.trimLeadingSlash(getRegexGroup(req, 1))); + } + }; + } + + private void setDefaultFields(FilterConfig config) throws ServletException { + if (renderer != null && urls != null && accessFactory != null && resolver != null + && visibilityCache != null) { + return; + } + String configPath = config.getInitParameter(CONFIG_PATH_PARAM); + if (configPath == null) { + throw new ServletException("Missing required parameter " + configPath); + } + FileBasedConfig jgitConfig = new FileBasedConfig(new File(configPath), FS.DETECTED); + try { + jgitConfig.load(); + } catch (IOException e) { + throw new ServletException(e); + } catch (ConfigInvalidException e) { + throw new ServletException(e); + } + + if (renderer == null) { + String staticPrefix = config.getServletContext().getContextPath() + STATIC_PREFIX; + String customTemplates = jgitConfig.getString("gitiles", null, "customTemplates"); + // TODO(dborowitz): Automatically set to true when run with mvn jetty:run. + if (jgitConfig.getBoolean("gitiles", null, "reloadTemplates", false)) { + renderer = new DebugRenderer(staticPrefix, customTemplates, + Joiner.on(File.separatorChar).join(System.getProperty("user.dir"), + "gitiles-servlet", "src", "main", "resources", + "com", "google", "gitiles", "templates")); + } else { + renderer = new DefaultRenderer(staticPrefix, Renderer.toFileURL(customTemplates)); + } + } + if (urls == null) { + try { + urls = new DefaultUrls( + jgitConfig.getString("gitiles", null, "canonicalHostName"), + getBaseGitUrl(jgitConfig), + getGerritUrl(jgitConfig)); + } catch (UnknownHostException e) { + throw new ServletException(e); + } + } + linkifier = new Linkifier(urls); + if (accessFactory == null || resolver == null) { + String basePath = jgitConfig.getString("gitiles", null, "basePath"); + if (basePath == null) { + throw new ServletException("gitiles.basePath not set"); + } + boolean exportAll = jgitConfig.getBoolean("gitiles", null, "exportAll", false); + + FileResolver<HttpServletRequest> fileResolver; + if (resolver == null) { + fileResolver = new FileResolver<HttpServletRequest>(new File(basePath), exportAll); + resolver = wrapResolver(fileResolver); + } else if (resolver instanceof FileResolver) { + fileResolver = (FileResolver<HttpServletRequest>) resolver; + } else { + fileResolver = null; + } + if (accessFactory == null) { + checkState(fileResolver != null, "need a FileResolver when GitilesAccess.Factory not set"); + try { + accessFactory = new DefaultAccess.Factory( + new File(basePath), + getBaseGitUrl(jgitConfig), + fileResolver); + } catch (IOException e) { + throw new ServletException(e); + } + } + } + if (visibilityCache == null) { + if (jgitConfig.getSubsections("cache").contains("visibility")) { + visibilityCache = + new VisibilityCache(false, ConfigUtil.getCacheBuilder(jgitConfig, "visibility")); + } else { + visibilityCache = new VisibilityCache(false); + } + } + } + + private static String getBaseGitUrl(Config config) throws ServletException { + String baseGitUrl = config.getString("gitiles", null, "baseGitUrl"); + if (baseGitUrl == null) { + throw new ServletException("gitiles.baseGitUrl not set"); + } + return baseGitUrl; + } + + private static String getGerritUrl(Config config) throws ServletException { + String gerritUrl = config.getString("gitiles", null, "gerritUrl"); + if (gerritUrl == null) { + throw new ServletException("gitiles.gerritUrl not set"); + } + return gerritUrl; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesServlet.java new file mode 100644 index 0000000..2cdbc02 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesServlet.java
@@ -0,0 +1,113 @@ +// 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 org.eclipse.jgit.http.server.glue.MetaServlet; +import org.eclipse.jgit.transport.resolver.RepositoryResolver; + +import java.util.Enumeration; + +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; + +/** + * Servlet to serve Gitiles. + * <p> + * This servlet can either be constructed manually with its dependencies, or + * configured to use default implementations for the Gitiles interfaces. To + * configure the defaults, you must provide a single init parameter + * "configPath", which is the path to a git config file containing additional + * configuration. + */ +public class GitilesServlet extends MetaServlet { + /** The prefix from which static resources are served. */ + public static final String STATIC_PREFIX = "/+static/"; + + public GitilesServlet(Renderer renderer, + GitilesUrls urls, + GitilesAccess.Factory accessFactory, + RepositoryResolver<HttpServletRequest> resolver, + VisibilityCache visibilityCache) { + super(new GitilesFilter(renderer, urls, accessFactory, resolver, visibilityCache)); + } + + public GitilesServlet() { + super(new GitilesFilter()); + } + + @Override + protected GitilesFilter getDelegateFilter() { + return (GitilesFilter) super.getDelegateFilter(); + } + + @Override + public void init(final ServletConfig config) throws ServletException { + getDelegateFilter().init(new FilterConfig() { + @Override + public String getFilterName() { + return getDelegateFilter().getClass().getName(); + } + + @Override + public String getInitParameter(String name) { + return config.getInitParameter(name); + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getInitParameterNames() { + return config.getInitParameterNames(); + } + + @Override + public ServletContext getServletContext() { + return config.getServletContext(); + } + }); + } + + /** + * Add a custom filter for a view. + * <p> + * Must be called before initializing the servlet. + * + * @param view view type. + * @param filter filter. + */ + public void addFilter(GitilesView.Type view, Filter filter) { + getDelegateFilter().addFilter(view, filter); + } + + /** + * Set a custom handler for a view. + * <p> + * Must be called before initializing the servlet. + * + * @param view view type. + * @param handler handler. + */ + public void setHandler(GitilesView.Type view, HttpServlet handler) { + getDelegateFilter().setHandler(view, handler); + } + + public BaseServlet getDefaultHandler(GitilesView.Type view) { + return getDelegateFilter().getDefaultHandler(view); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesUrls.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesUrls.java new file mode 100644 index 0000000..fb7ff3b --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesUrls.java
@@ -0,0 +1,79 @@ +// 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 com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import javax.servlet.http.HttpServletRequest; + +/** Interface for URLs displayed on source browsing pages. */ +public interface GitilesUrls { + /** + * Escapes repository or path names to be safely embedded into a URL. + * <p> + * This escape implementation escapes a repository or path name such as + * "foo/bar</child" to appear as "foo/bar%3C/child". Spaces are escaped as + * "%20". Its purpose is to escape a repository name to be safe for inclusion + * in the path component of the URL, where "/" is a valid character that + * should not be encoded, while almost any other non-alpha, non-numeric + * character will be encoded using URL style encoding. + */ + public static final Function<String, String> NAME_ESCAPER = new Function<String, String>() { + @Override + public String apply(String s) { + try { + return URLEncoder.encode(s, Charsets.UTF_8.name()) + .replace("%2F", "/") + .replace("%2f", "/") + .replace("+", "%20") + .replace("%2B", "+") + .replace("%2b", "+"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + }; + + /** + * Return the name of the host from the request. + * + * Used in various user-visible text, like "MyHost Git Repositories". + * + * @param req request. + * @return host name; may be null. + */ + public String getHostName(HttpServletRequest req); + + /** + * Return the base URL for git repositories on this host. + * + * @param req request. + * @return base URL for git repositories. + */ + public String getBaseGitUrl(HttpServletRequest req); + + /** + * Return the base URL for Gerrit projects on this host. + * + * @param req request. + * @return base URL for Gerrit Code Review, or null if Gerrit is not + * configured. + */ + public String getBaseGerritUrl(HttpServletRequest req); +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java new file mode 100644 index 0000000..ad685c4 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -0,0 +1,563 @@ +// 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.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gitiles.GitilesUrls.NAME_ESCAPER; + +import com.google.common.base.Charsets; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; + +import org.eclipse.jgit.revwalk.RevObject; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +/** + * Information about a view in Gitiles. + * <p> + * Views are uniquely identified by a type, and dispatched to servlet types by + * {@link GitilesServlet}. This class contains the list of all types, as + * well as some methods containing basic information parsed from the URL. + * Construction happens in {@link ViewFilter}. + */ +public class GitilesView { + /** All the possible view types supported in the application. */ + public static enum Type { + HOST_INDEX, + REPOSITORY_INDEX, + REVISION, + PATH, + DIFF, + LOG; + } + + /** Builder for views. */ + public static class Builder { + private final Type type; + private final ListMultimap<String, String> params = LinkedListMultimap.create(); + + private String hostName; + private String servletPath; + private String repositoryName; + private Revision revision = Revision.NULL; + private Revision oldRevision = Revision.NULL; + private String path; + private String anchor; + + private Builder(Type type) { + this.type = type; + } + + public Builder copyFrom(GitilesView other) { + hostName = other.hostName; + servletPath = other.servletPath; + switch (type) { + case LOG: + case DIFF: + oldRevision = other.oldRevision; + // Fallthrough. + case PATH: + path = other.path; + // Fallthrough. + case REVISION: + revision = other.revision; + // Fallthrough. + case REPOSITORY_INDEX: + repositoryName = other.repositoryName; + } + // Don't copy params. + return this; + } + + public Builder copyFrom(HttpServletRequest req) { + return copyFrom(ViewFilter.getView(req)); + } + + public Builder setHostName(String hostName) { + this.hostName = checkNotNull(hostName); + return this; + } + + public String getHostName() { + return hostName; + } + + public Builder setServletPath(String servletPath) { + this.servletPath = checkNotNull(servletPath); + return this; + } + + public String getServletPath() { + return servletPath; + } + + public Builder setRepositoryName(String repositoryName) { + switch (type) { + case HOST_INDEX: + throw new IllegalStateException(String.format( + "cannot set repository name on %s view", type)); + default: + this.repositoryName = checkNotNull(repositoryName); + return this; + } + } + + public String getRepositoryName() { + return repositoryName; + } + + public Builder setRevision(Revision revision) { + switch (type) { + case HOST_INDEX: + case REPOSITORY_INDEX: + throw new IllegalStateException(String.format("cannot set revision on %s view", type)); + default: + this.revision = checkNotNull(revision); + return this; + } + } + + public Builder setRevision(String name) { + return setRevision(Revision.named(name)); + } + + public Builder setRevision(RevObject obj) { + return setRevision(Revision.peeled(obj.name(), obj)); + } + + public Builder setRevision(String name, RevObject obj) { + return setRevision(Revision.peeled(name, obj)); + } + + public Revision getRevision() { + return revision; + } + + public Builder setOldRevision(Revision revision) { + switch (type) { + case DIFF: + case LOG: + this.oldRevision = checkNotNull(revision); + return this; + default: + throw new IllegalStateException( + String.format("cannot set old revision on %s view", type)); + } + } + + public Builder setOldRevision(RevObject obj) { + return setOldRevision(Revision.peeled(obj.name(), obj)); + } + + public Builder setOldRevision(String name, RevObject obj) { + return setOldRevision(Revision.peeled(name, obj)); + } + + public Revision getOldRevision() { + return revision; + } + + public Builder setTreePath(String path) { + switch (type) { + case PATH: + case DIFF: + this.path = maybeTrimLeadingAndTrailingSlash(checkNotNull(path)); + return this; + case LOG: + this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null; + return this; + default: + throw new IllegalStateException(String.format("cannot set path on %s view", type)); + } + } + + public String getTreePath() { + return path; + } + + public Builder putParam(String key, String value) { + params.put(key, value); + return this; + } + + public Builder replaceParam(String key, String value) { + params.replaceValues(key, ImmutableList.of(value)); + return this; + } + + public Builder putAllParams(Map<String, String[]> params) { + for (Map.Entry<String, String[]> e : params.entrySet()) { + for (String v : e.getValue()) { + this.params.put(e.getKey(), v); + } + } + return this; + } + + public ListMultimap<String, String> getParams() { + return params; + } + + public Builder setAnchor(String anchor) { + this.anchor = anchor; + return this; + } + + public String getAnchor() { + return anchor; + } + + public GitilesView build() { + switch (type) { + case HOST_INDEX: + checkHostIndex(); + break; + case REPOSITORY_INDEX: + checkRepositoryIndex(); + break; + case REVISION: + checkRevision(); + break; + case PATH: + checkPath(); + break; + case DIFF: + checkDiff(); + break; + case LOG: + checkLog(); + break; + } + return new GitilesView(type, hostName, servletPath, repositoryName, revision, + oldRevision, path, params, anchor); + } + + public String toUrl() { + return build().toUrl(); + } + + private void checkHostIndex() { + checkState(hostName != null, "missing hostName on %s view", type); + checkState(servletPath != null, "missing hostName on %s view", type); + } + + private void checkRepositoryIndex() { + checkState(repositoryName != null, "missing repository name on %s view", type); + checkHostIndex(); + } + + private void checkRevision() { + checkState(revision != Revision.NULL, "missing revision on %s view", type); + checkRepositoryIndex(); + } + + private void checkDiff() { + checkPath(); + } + + private void checkLog() { + checkRevision(); + } + + private void checkPath() { + checkState(path != null, "missing path on %s view", type); + checkRevision(); + } + } + + public static Builder hostIndex() { + return new Builder(Type.HOST_INDEX); + } + + public static Builder repositoryIndex() { + return new Builder(Type.REPOSITORY_INDEX); + } + + public static Builder revision() { + return new Builder(Type.REVISION); + } + + public static Builder path() { + return new Builder(Type.PATH); + } + + public static Builder diff() { + return new Builder(Type.DIFF); + } + + public static Builder log() { + return new Builder(Type.LOG); + } + + private static String maybeTrimLeadingAndTrailingSlash(String str) { + if (str.startsWith("/")) { + str = str.substring(1); + } + return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str; + } + + private final Type type; + private final String hostName; + private final String servletPath; + private final String repositoryName; + private final Revision revision; + private final Revision oldRevision; + private final String path; + private final ListMultimap<String, String> params; + private final String anchor; + + private GitilesView(Type type, + String hostName, + String servletPath, + String repositoryName, + Revision revision, + Revision oldRevision, + String path, + ListMultimap<String, String> params, + String anchor) { + this.type = type; + this.hostName = hostName; + this.servletPath = servletPath; + this.repositoryName = repositoryName; + this.revision = Objects.firstNonNull(revision, Revision.NULL); + this.oldRevision = Objects.firstNonNull(oldRevision, Revision.NULL); + this.path = path; + this.params = Multimaps.unmodifiableListMultimap(params); + this.anchor = anchor; + } + + public String getHostName() { + return hostName; + } + + public String getServletPath() { + return servletPath; + } + + public String getRepositoryName() { + return repositoryName; + } + + public Revision getRevision() { + return revision; + } + + public Revision getOldRevision() { + return oldRevision; + } + + public String getRevisionRange() { + if (oldRevision == Revision.NULL) { + switch (type) { + case LOG: + case DIFF: + // For types that require two revisions, NULL indicates the empty + // tree/commit. + return revision.getName() + "^!"; + default: + // For everything else NULL indicates it is not a range, just a single + // revision. + return null; + } + } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) { + return revision.getName() + "^!"; + } else { + return oldRevision.getName() + ".." + revision.getName(); + } + } + + public String getTreePath() { + return path; + } + + public ListMultimap<String, String> getParameters() { + return params; + } + + public String getAnchor() { + return anchor; + } + + public Type getType() { + return type; + } + + /** @return an escaped, relative URL representing this view. */ + public String toUrl() { + StringBuilder url = new StringBuilder(servletPath).append('/'); + ListMultimap<String, String> params = this.params; + switch (type) { + case HOST_INDEX: + params = LinkedListMultimap.create(); + if (!this.params.containsKey("format")) { + params.put("format", FormatType.HTML.toString()); + } + params.putAll(this.params); + break; + case REPOSITORY_INDEX: + url.append(repositoryName).append('/'); + break; + case REVISION: + url.append(repositoryName).append("/+"); + if (!getRevision().nameIsId()) { + url.append("show"); // Default for /+/master is +log. + } + url.append('/').append(revision.getName()); + break; + case PATH: + url.append(repositoryName).append("/+/").append(revision.getName()).append('/') + .append(path); + break; + case DIFF: + url.append(repositoryName).append("/+/"); + if (isFirstParent(revision, oldRevision)) { + url.append(revision.getName()).append("^!"); + } else { + url.append(oldRevision.getName()).append("..").append(revision.getName()); + } + url.append('/').append(path); + break; + case LOG: + url.append(repositoryName).append("/+"); + if (getRevision().nameIsId() || oldRevision != Revision.NULL || path != null) { + // Default for /+/c0ffee/(...) is +show. + // Default for /+/c0ffee..deadbeef(/...) is +diff. + url.append("log"); + } + url.append('/'); + if (oldRevision != Revision.NULL) { + url.append(oldRevision.getName()).append(".."); + } + url.append(revision.getName()); + if (path != null) { + url.append('/').append(path); + } + break; + default: + throw new IllegalStateException("Unknown view type: " + type); + } + String baseUrl = NAME_ESCAPER.apply(url.toString()); + url = new StringBuilder(); + if (!params.isEmpty()) { + url.append('?').append(paramsToString(params)); + } + if (!Strings.isNullOrEmpty(anchor)) { + url.append('#').append(NAME_ESCAPER.apply(anchor)); + } + return baseUrl + url.toString(); + } + + public List<Map<String, String>> getBreadcrumbs() { + String path = this.path; + ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder(); + breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this))); + if (repositoryName != null) { + breadcrumbs.add(breadcrumb(repositoryName, repositoryIndex().copyFrom(this))); + } + if (type == Type.DIFF) { + // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render + // separate links in "old..new". + breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setTreePath(""))); + } else if (type == Type.LOG) { + // TODO(dborowitz): Add something in the navigation area (probably not + // a breadcrumb) to allow switching between /+log/ and /+/. + if (oldRevision == Revision.NULL) { + breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setTreePath(null))); + } else { + breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setTreePath(null))); + } + path = Strings.emptyToNull(path); + } else if (revision != Revision.NULL) { + breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this))); + } + if (path != null) { + if (type != Type.LOG) { // The "." breadcrumb would be no different for LOG. + breadcrumbs.add(breadcrumb(".", copyWithPath().setTreePath(""))); + } + StringBuilder cur = new StringBuilder(); + boolean first = true; + for (String part : RevisionParser.PATH_SPLITTER.omitEmptyStrings().split(path)) { + if (!first) { + cur.append('/'); + } else { + first = false; + } + cur.append(part); + breadcrumbs.add(breadcrumb(part, copyWithPath().setTreePath(cur.toString()))); + } + } + return breadcrumbs.build(); + } + + private static Map<String, String> breadcrumb(String text, Builder url) { + return ImmutableMap.of("text", text, "url", url.toUrl()); + } + + private Builder copyWithPath() { + Builder copy; + switch (type) { + case DIFF: + copy = diff(); + break; + case LOG: + copy = log(); + break; + default: + copy = path(); + break; + } + return copy.copyFrom(this); + } + + private static boolean isFirstParent(Revision rev1, Revision rev2) { + return rev2 == Revision.NULL + || rev2.getName().equals(rev1.getName() + "^") + || rev2.getName().equals(rev1.getName() + "~1"); + } + + private static String paramsToString(ListMultimap<String, String> params) { + try { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry<String, String> e : params.entries()) { + if (!first) { + sb.append('&'); + } else { + first = false; + } + sb.append(URLEncoder.encode(e.getKey(), Charsets.UTF_8.name())); + if (!"".equals(e.getValue())) { + sb.append('=') + .append(URLEncoder.encode(e.getValue(), Charsets.UTF_8.name())); + } + } + return sb.toString(); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java new file mode 100644 index 0000000..bf9ffd1 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
@@ -0,0 +1,199 @@ +// 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.checkNotNull; +import static com.google.gitiles.FormatType.JSON; +import static com.google.gitiles.FormatType.TEXT; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.net.HttpHeaders; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.google.template.soy.data.SoyListData; +import com.google.template.soy.data.SoyMapData; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves the top level index page for a Gitiles host. */ +public class HostIndexServlet extends BaseServlet { + private static final Logger log = LoggerFactory.getLogger(HostIndexServlet.class); + + protected final GitilesUrls urls; + private final GitilesAccess.Factory accessFactory; + + public HostIndexServlet(Renderer renderer, GitilesUrls urls, + GitilesAccess.Factory accessFactory) { + super(renderer); + this.urls = checkNotNull(urls, "urls"); + this.accessFactory = checkNotNull(accessFactory, "accessFactory"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + FormatType format; + try { + format = FormatType.getFormatType(req); + } catch (IllegalArgumentException err) { + res.sendError(SC_BAD_REQUEST); + return; + } + + Set<String> branches = parseShowBranch(req); + Map<String, RepositoryDescription> descs; + try { + descs = accessFactory.forRequest(req).listRepositories(branches); + } catch (RepositoryNotFoundException e) { + res.sendError(SC_NOT_FOUND); + return; + } catch (ServiceNotEnabledException e) { + res.sendError(SC_FORBIDDEN); + return; + } catch (ServiceNotAuthorizedException e) { + res.sendError(SC_UNAUTHORIZED); + return; + } catch (ServiceMayNotContinueException e) { + // TODO(dborowitz): Show the error message to the user. + res.sendError(SC_FORBIDDEN); + return; + } catch (IOException err) { + String name = urls.getHostName(req); + log.warn("Cannot scan repositories" + (name != null ? "for " + name : ""), err); + res.sendError(SC_SERVICE_UNAVAILABLE); + return; + } + + switch (format) { + case HTML: + case DEFAULT: + default: + displayHtml(req, res, descs); + break; + + case TEXT: + displayText(req, res, branches, descs); + break; + + case JSON: + displayJson(req, res, descs); + break; + } + } + + private SoyMapData toSoyMapData(RepositoryDescription desc, GitilesView view) { + return new SoyMapData( + "name", desc.name, + "description", Strings.nullToEmpty(desc.description), + "url", GitilesView.repositoryIndex() + .copyFrom(view) + .setRepositoryName(desc.name) + .toUrl()); + } + + private void displayHtml(HttpServletRequest req, HttpServletResponse res, + Map<String, RepositoryDescription> descs) throws IOException { + SoyListData repos = new SoyListData(); + for (RepositoryDescription desc : descs.values()) { + repos.add(toSoyMapData(desc, ViewFilter.getView(req))); + } + + render(req, res, "gitiles.hostIndex", ImmutableMap.of( + "hostName", urls.getHostName(req), + "baseUrl", urls.getBaseGitUrl(req), + "repositories", repos)); + } + + private void displayText(HttpServletRequest req, HttpServletResponse res, + Set<String> branches, Map<String, RepositoryDescription> descs) throws IOException { + res.setContentType(TEXT.getMimeType()); + res.setCharacterEncoding("UTF-8"); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment"); + setNotCacheable(res); + + PrintWriter writer = res.getWriter(); + for (RepositoryDescription repo : descs.values()) { + for (String name : branches) { + String ref = repo.branches.get(name); + if (ref == null) { + // Print stub (forty '-' symbols) + ref = "----------------------------------------"; + } + writer.print(ref); + writer.print(' '); + } + writer.print(GitilesUrls.NAME_ESCAPER.apply(repo.name)); + writer.print('\n'); + } + writer.flush(); + writer.close(); + } + + private void displayJson(HttpServletRequest req, HttpServletResponse res, + Map<String, RepositoryDescription> descs) throws IOException { + res.setContentType(JSON.getMimeType()); + res.setCharacterEncoding("UTF-8"); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment"); + setNotCacheable(res); + + PrintWriter writer = res.getWriter(); + new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .setPrettyPrinting() + .generateNonExecutableJson() + .create() + .toJson(descs, + new TypeToken<Map<String, RepositoryDescription>>() {}.getType(), + writer); + writer.print('\n'); + writer.close(); + } + + private static Set<String> parseShowBranch(HttpServletRequest req) { + // Roughly match Gerrit Code Review's /projects/ API by supporting + // both show-branch and b as query parameters. + Set<String> branches = Sets.newLinkedHashSet(); + String[] values = req.getParameterValues("show-branch"); + if (values != null) { + branches.addAll(Arrays.asList(values)); + } + values = req.getParameterValues("b"); + if (values != null) { + branches.addAll(Arrays.asList(values)); + } + return branches; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java new file mode 100644 index 0000000..63b1eb8 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
@@ -0,0 +1,128 @@ +// 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.checkNotNull; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.patch.FileHeader; +import org.eclipse.jgit.patch.FileHeader.PatchType; +import org.eclipse.jgit.util.RawParseUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** Formats a unified format patch as UTF-8 encoded HTML. */ +final class HtmlDiffFormatter extends DiffFormatter { + private static final byte[] DIFF_BEGIN = "<pre class=\"diff-unified\">".getBytes(Charsets.UTF_8); + private static final byte[] DIFF_END = "</pre>".getBytes(Charsets.UTF_8); + + private static final byte[] HUNK_BEGIN = "<span class=\"h\">".getBytes(Charsets.UTF_8); + private static final byte[] HUNK_END = "</span>".getBytes(Charsets.UTF_8); + + private static final byte[] LINE_INSERT_BEGIN = "<span class=\"i\">".getBytes(Charsets.UTF_8); + private static final byte[] LINE_DELETE_BEGIN = "<span class=\"d\">".getBytes(Charsets.UTF_8); + private static final byte[] LINE_CHANGE_BEGIN = "<span class=\"c\">".getBytes(Charsets.UTF_8); + private static final byte[] LINE_END = "</span>\n".getBytes(Charsets.UTF_8); + + private final Renderer renderer; + private int fileIndex; + + HtmlDiffFormatter(Renderer renderer, OutputStream out) { + super(out); + this.renderer = checkNotNull(renderer, "renderer"); + } + + @Override + public void format(List<? extends DiffEntry> entries) throws IOException { + for (fileIndex = 0; fileIndex < entries.size(); fileIndex++) { + format(entries.get(fileIndex)); + } + } + + @Override + public void format(FileHeader hdr, RawText a, RawText b) + throws IOException { + int start = hdr.getStartOffset(); + int end = hdr.getEndOffset(); + if (!hdr.getHunks().isEmpty()) { + end = hdr.getHunks().get(0).getStartOffset(); + } + renderHeader(RawParseUtils.decode(hdr.getBuffer(), start, end)); + + if (hdr.getPatchType() == PatchType.UNIFIED) { + getOutputStream().write(DIFF_BEGIN); + format(hdr.toEditList(), a, b); + getOutputStream().write(DIFF_END); + } + } + + private void renderHeader(String header) + throws IOException { + int lf = header.indexOf('\n'); + String first; + String rest; + if (0 <= lf) { + first = header.substring(0, lf); + rest = header.substring(lf + 1); + } else { + first = header; + rest = ""; + } + getOutputStream().write(renderer.newRenderer("gitiles.diffHeader") + .setData(ImmutableMap.of("first", first, "rest", rest, "fileIndex", fileIndex)) + .render() + .getBytes(Charsets.UTF_8)); + } + + @Override + protected void writeHunkHeader(int aStartLine, int aEndLine, + int bStartLine, int bEndLine) throws IOException { + getOutputStream().write(HUNK_BEGIN); + // TODO(sop): If hunk header starts including method names, escape it. + super.writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine); + getOutputStream().write(HUNK_END); + } + + @Override + protected void writeLine(char prefix, RawText text, int cur) + throws IOException { + // Manually render each line, rather than invoke a Soy template. This method + // can be called thousands of times in a single request. Avoid unnecessary + // overheads by formatting as-is. + OutputStream out = getOutputStream(); + switch (prefix) { + case '+': + out.write(LINE_INSERT_BEGIN); + break; + case '-': + out.write(LINE_DELETE_BEGIN); + break; + case ' ': + default: + out.write(LINE_CHANGE_BEGIN); + break; + } + out.write(StringEscapeUtils.escapeHtml4(text.getString(cur)).getBytes(Charsets.UTF_8)); + out.write(LINE_END); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java b/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java new file mode 100644 index 0000000..735e6f5 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java
@@ -0,0 +1,96 @@ +// 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.checkNotNull; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +/** Linkifier for blocks of text such as commit message descriptions. */ +public class Linkifier { + private static final Pattern LINK_PATTERN; + + static { + // HTTP URL regex adapted from com.google.gwtexpui.safehtml.client.SafeHtml. + String part = "[a-zA-Z0-9$_.+!*',%;:@=?#/~<>-]"; + String httpUrl = "https?://" + + part + "{2,}" + + "(?:[(]" + part + "*" + "[)])*" + + part + "*"; + String changeId = "\\bI[0-9a-f]{8,40}\\b"; + LINK_PATTERN = Pattern.compile(Joiner.on("|").join( + "(" + httpUrl + ")", + "(" + changeId + ")")); + } + + private final GitilesUrls urls; + + public Linkifier(GitilesUrls urls) { + this.urls = checkNotNull(urls, "urls"); + } + + public List<Map<String, String>> linkify(HttpServletRequest req, String message) { + String baseGerritUrl = urls.getBaseGerritUrl(req); + List<Map<String, String>> parsed = Lists.newArrayList(); + Matcher m = LINK_PATTERN.matcher(message); + int last = 0; + while (m.find()) { + addText(parsed, message.substring(last, m.start())); + if (m.group(1) != null) { + // Bare URL. + parsed.add(link(m.group(1), m.group(1))); + } else if (m.group(2) != null) { + if (baseGerritUrl != null) { + // Gerrit Change-Id. + parsed.add(link(m.group(2), baseGerritUrl + "#/q/" + m.group(2) + ",n,z")); + } else { + addText(parsed, m.group(2)); + } + } + last = m.end(); + } + addText(parsed, message.substring(last)); + return parsed; + } + + private static Map<String, String> link(String text, String url) { + return ImmutableMap.of("text", text, "url", url); + } + + private static void addText(List<Map<String, String>> parts, String text) { + if (text.isEmpty()) { + return; + } + if (parts.isEmpty()) { + parts.add(ImmutableMap.of("text", text)); + } else { + Map<String, String> old = parts.get(parts.size() - 1); + if (!old.containsKey("url")) { + parts.set(parts.size() - 1, ImmutableMap.of("text", old.get("text") + text)); + } else { + parts.add(ImmutableMap.of("text", text)); + } + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java new file mode 100644 index 0000000..07977f7 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -0,0 +1,193 @@ +// 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.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.gitiles.CommitSoyData.KeySet; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevWalkException; +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.FollowFilter; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves an HTML page with a shortlog for commits and paths. */ +public class LogServlet extends BaseServlet { + private static final Logger log = LoggerFactory.getLogger(LogServlet.class); + + private static final String START_PARAM = "s"; + + private final Linkifier linkifier; + private final int limit; + + public LogServlet(Renderer renderer, Linkifier linkifier) { + this(renderer, linkifier, 100); + } + + private LogServlet(Renderer renderer, Linkifier linkifier, int limit) { + super(renderer); + this.linkifier = checkNotNull(linkifier, "linkifier"); + checkArgument(limit >= 0, "limit must be positive: %s", limit); + this.limit = limit; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + GitilesView view = ViewFilter.getView(req); + Repository repo = ServletUtils.getRepository(req); + RevWalk walk = null; + try { + try { + walk = newWalk(repo, view); + } catch (IncorrectObjectTypeException e) { + res.setStatus(SC_NOT_FOUND); + return; + } + + Optional<ObjectId> start = getStart(view.getParameters(), walk.getObjectReader()); + if (start == null) { + res.setStatus(SC_NOT_FOUND); + return; + } + + Map<String, Object> data = Maps.newHashMapWithExpectedSize(5); + + if (!view.getRevision().nameIsId()) { + List<Map<String, Object>> tags = Lists.newArrayListWithExpectedSize(1); + for (RevObject o : RevisionServlet.listObjects(walk, view.getRevision().getId())) { + if (o instanceof RevTag) { + tags.add(new TagSoyData(linkifier, req).toSoyData((RevTag) o)); + } + } + if (!tags.isEmpty()) { + data.put("tags", tags); + } + } + + Paginator paginator = new Paginator(walk, limit, start.orNull()); + Map<AnyObjectId, Set<Ref>> refsById = repo.getAllRefsByPeeledObjectId(); + List<Map<String, Object>> entries = Lists.newArrayListWithCapacity(limit); + for (RevCommit c : paginator) { + entries.add(new CommitSoyData(null, req, repo, walk, view, refsById) + .toSoyData(c, KeySet.SHORTLOG)); + } + + String title = "Log - "; + if (view.getOldRevision() != Revision.NULL) { + title += view.getRevisionRange(); + } else { + title += view.getRevision().getName(); + } + + data.put("title", title); + data.put("entries", entries); + ObjectId next = paginator.getNextStart(); + if (next != null) { + data.put("nextUrl", copyAndCanonicalize(view) + .replaceParam(START_PARAM, next.name()) + .toUrl()); + } + ObjectId prev = paginator.getPreviousStart(); + if (prev != null) { + GitilesView.Builder prevView = copyAndCanonicalize(view); + if (!prevView.getRevision().getId().equals(prev)) { + prevView.replaceParam(START_PARAM, prev.name()); + } + data.put("previousUrl", prevView.toUrl()); + } + + render(req, res, "gitiles.logDetail", data); + } catch (RevWalkException e) { + log.warn("Error in rev walk", e); + res.setStatus(SC_INTERNAL_SERVER_ERROR); + return; + } finally { + if (walk != null) { + walk.release(); + } + } + } + + private static GitilesView.Builder copyAndCanonicalize(GitilesView view) { + // Canonicalize the view by using full SHAs. + GitilesView.Builder copy = GitilesView.log().copyFrom(view) + .setRevision(view.getRevision()); + if (view.getOldRevision() != Revision.NULL) { + copy.setOldRevision(view.getOldRevision()); + } + return copy; + } + + private static Optional<ObjectId> getStart(ListMultimap<String, String> params, + ObjectReader reader) throws IOException { + List<String> values = params.get(START_PARAM); + switch (values.size()) { + case 0: + return Optional.absent(); + case 1: + Collection<ObjectId> ids = reader.resolve(AbbreviatedObjectId.fromString(values.get(0))); + if (ids.size() != 1) { + return null; + } + return Optional.of(Iterables.getOnlyElement(ids)); + default: + return null; + } + } + + private static RevWalk newWalk(Repository repo, GitilesView view) + throws MissingObjectException, IncorrectObjectTypeException, IOException { + RevWalk walk = new RevWalk(repo); + walk.markStart(walk.parseCommit(view.getRevision().getId())); + if (view.getOldRevision() != Revision.NULL) { + walk.markUninteresting(walk.parseCommit(view.getOldRevision().getId())); + } + if (!Strings.isNullOrEmpty(view.getTreePath())) { + walk.setTreeFilter(FollowFilter.create(view.getTreePath())); + } + return walk; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Paginator.java b/gitiles-servlet/src/main/java/com/google/gitiles/Paginator.java new file mode 100644 index 0000000..0454556 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/Paginator.java
@@ -0,0 +1,176 @@ +// 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.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevWalkException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import javax.annotation.Nullable; + +/** + * Wrapper around {@link RevWalk} that paginates for Gitiles. + * + * A single page of a shortlog is defined by a revision range, such as "master" + * or "master..next", a page size, and a start commit, such as "c0ffee". The + * distance between the first commit in the walk ("next") and the first commit + * in the page may be arbitrarily long, but in order to present the commit list + * in a stable way, we must always start from the first commit in the walk. This + * is because there may be arbitrary merge commits between "c0ffee" and "next" + * that effectively insert arbitrary commits into the history starting from + * "c0ffee". + */ +class Paginator implements Iterable<RevCommit> { + private final RevWalk walk; + private final ObjectId start; + private final int limit; + private final Deque<ObjectId> prevBuffer; + + private boolean done; + private int i; + private int n; + private int foundIndex; + private ObjectId nextStart; + + /** + * @param walk revision walk. + * @param limit page size. + * @param start commit at which to start the walk, or null to start at the + * beginning. + */ + Paginator(RevWalk walk, int limit, @Nullable ObjectId start) { + this.walk = checkNotNull(walk, "walk"); + this.start = start; + checkArgument(limit > 0, "limit must be positive: %s", limit); + this.limit = limit; + prevBuffer = new ArrayDeque<ObjectId>(start != null ? limit : 0); + i = -1; + foundIndex = -1; + } + + /** + * Get the next element in this page of the walk. + * + * @return the next element, or null if the walk is finished. + * + * @throws MissingObjectException See {@link RevWalk#next()}. + * @throws IncorrectObjectTypeException See {@link RevWalk#next()}. + * @throws IOException See {@link RevWalk#next()}. + */ + public RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, + IOException { + RevCommit commit; + if (foundIndex < 0) { + while (true) { + commit = walk.next(); + if (commit == null) { + done = true; + return null; + } + i++; + if (start == null || start.equals(commit)) { + foundIndex = i; + break; + } + if (prevBuffer.size() == limit) { + prevBuffer.remove(); + } + prevBuffer.add(commit); + } + } else { + commit = walk.next(); + } + + if (++n == limit) { + done = true; + } else if (n == limit + 1 || commit == null) { + nextStart = commit; + done = true; + return null; + } + return commit; + } + + /** + * @return the ID at the start of the page of results preceding this one, or + * null if this is the first page. + */ + public ObjectId getPreviousStart() { + checkState(done, "getPreviousStart() invalid before walk done"); + return prevBuffer.pollFirst(); + } + + /** + * @return the ID at the start of the page of results after this one, or null + * if this is the last page. + */ + public ObjectId getNextStart() { + checkState(done, "getNextStart() invalid before walk done"); + return nextStart; + } + + /** + * @return an iterator over the commits in this walk. + * @throws RevWalkException if an error occurred, wrapping the checked + * exception from {@link #next()}. + */ + @Override + public Iterator<RevCommit> iterator() { + return new Iterator<RevCommit>() { + RevCommit next = nextUnchecked(); + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public RevCommit next() { + RevCommit r = next; + next = nextUnchecked(); + return r; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + private RevCommit nextUnchecked() { + try { + return next(); + } catch (MissingObjectException e) { + throw new RevWalkException(e); + } catch (IncorrectObjectTypeException e) { + throw new RevWalkException(e); + } catch (IOException e) { + throw new RevWalkException(e); + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java new file mode 100644 index 0000000..d228202 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
@@ -0,0 +1,276 @@ +// 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.gitiles.TreeSoyData.resolveTargetUrl; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +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 org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves an HTML page with detailed information about a path within a tree. */ +// TODO(dborowitz): Handle non-UTF-8 names. +public class PathServlet extends BaseServlet { + private static final Logger log = LoggerFactory.getLogger(PathServlet.class); + + /** + * Submodule URLs where we know there is a web page if the user visits the + * repository URL verbatim in a web browser. + */ + private static final Pattern VERBATIM_SUBMODULE_URL_PATTERN = + Pattern.compile("^(" + Joiner.on('|').join( + "https?://[^.]+.googlesource.com/.*", + "https?://[^.]+.googlecode.com/.*", + "https?://code.google.com/p/.*", + "https?://github.com/.*") + ")$", Pattern.CASE_INSENSITIVE); + + static enum FileType { + TREE(FileMode.TREE), + SYMLINK(FileMode.SYMLINK), + REGULAR_FILE(FileMode.REGULAR_FILE), + EXECUTABLE_FILE(FileMode.EXECUTABLE_FILE), + GITLINK(FileMode.GITLINK); + + private final FileMode mode; + + private FileType(FileMode mode) { + this.mode = mode; + } + + static FileType forEntry(TreeWalk tw) { + int mode = tw.getRawMode(0); + for (FileType type : values()) { + if (type.mode.equals(mode)) { + return type; + } + } + return null; + } + } + + public PathServlet(Renderer renderer) { + super(renderer); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + GitilesView view = ViewFilter.getView(req); + Repository repo = ServletUtils.getRepository(req); + + RevWalk rw = new RevWalk(repo); + try { + RevObject obj = rw.peel(rw.parseAny(view.getRevision().getId())); + RevTree root; + + switch (obj.getType()) { + case OBJ_COMMIT: + root = ((RevCommit) obj).getTree(); + break; + case OBJ_TREE: + root = (RevTree) obj; + break; + default: + res.setStatus(SC_NOT_FOUND); + return; + } + + TreeWalk tw; + FileType type; + String path = view.getTreePath(); + if (path.isEmpty()) { + tw = new TreeWalk(rw.getObjectReader()); + tw.addTree(root); + tw.setRecursive(false); + type = FileType.TREE; + } else { + tw = TreeWalk.forPath(rw.getObjectReader(), path, root); + if (tw == null) { + res.setStatus(SC_NOT_FOUND); + return; + } + type = FileType.forEntry(tw); + if (type == FileType.TREE) { + tw.enterSubtree(); + tw.setRecursive(false); + } + } + + switch (type) { + case TREE: + showTree(req, res, rw, tw, obj); + break; + case SYMLINK: + showSymlink(req, res, rw, tw); + break; + case REGULAR_FILE: + case EXECUTABLE_FILE: + showFile(req, res, rw, tw); + break; + case GITLINK: + showGitlink(req, res, rw, tw, root); + break; + default: + log.error("Bad file type: %s", type); + res.setStatus(SC_NOT_FOUND); + break; + } + } catch (LargeObjectException e) { + res.setStatus(SC_INTERNAL_SERVER_ERROR); + } finally { + rw.release(); + } + } + + private void showTree(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw, + ObjectId id) throws IOException { + GitilesView view = ViewFilter.getView(req); + // TODO(sop): Allow caching trees by SHA-1 when no S cookie is sent. + render(req, res, "gitiles.pathDetail", ImmutableMap.of( + "title", !view.getTreePath().isEmpty() ? view.getTreePath() : "/", + "type", FileType.TREE.toString(), + "data", new TreeSoyData(rw, view).toSoyData(id, tw))); + } + + private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw) + throws IOException { + GitilesView view = ViewFilter.getView(req); + // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent. + render(req, res, "gitiles.pathDetail", ImmutableMap.of( + "title", ViewFilter.getView(req).getTreePath(), + "type", FileType.forEntry(tw).toString(), + "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0)))); + } + + private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw, + TreeWalk tw) throws IOException { + GitilesView view = ViewFilter.getView(req); + ObjectId id = tw.getObjectId(0); + Map<String, Object> data = Maps.newHashMap(); + + ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB); + String target; + try { + target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE)); + } catch (LargeObjectException.OutOfMemory e) { + throw e; + } catch (LargeObjectException e) { + data.put("sha", ObjectId.toString(id)); + data.put("data", null); + data.put("size", Long.toString(loader.getSize())); + render(req, res, "gitiles.pathDetail", ImmutableMap.of( + "title", ViewFilter.getView(req).getTreePath(), + "type", FileType.REGULAR_FILE.toString(), + "data", data)); + return; + } + + String url = resolveTargetUrl( + GitilesView.path() + .copyFrom(view) + .setTreePath(dirname(view.getTreePath())) + .build(), + target); + data.put("title", view.getTreePath()); + data.put("target", target); + if (url != null) { + data.put("targetUrl", url); + } + + // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent. + render(req, res, "gitiles.pathDetail", ImmutableMap.of( + "title", ViewFilter.getView(req).getTreePath(), + "type", FileType.SYMLINK.toString(), + "data", data)); + } + + private static String dirname(String path) { + while (path.charAt(path.length() - 1) == '/') { + path = path.substring(0, path.length() - 1); + } + int lastSlash = path.lastIndexOf('/'); + if (lastSlash > 0) { + return path.substring(0, lastSlash - 1); + } else if (lastSlash == 0) { + return "/"; + } else { + return "."; + } + } + + private void showGitlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw, + TreeWalk tw, RevTree root) throws IOException { + GitilesView view = ViewFilter.getView(req); + SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root, + view.getTreePath()); + + String remoteUrl; + try { + remoteUrl = sw.getRemoteUrl(); + } catch (ConfigInvalidException e) { + throw new IOException(e); + } finally { + sw.release(); + } + + Map<String, Object> data = Maps.newHashMap(); + data.put("sha", ObjectId.toString(tw.getObjectId(0))); + data.put("remoteUrl", remoteUrl); + + // TODO(dborowitz): Guess when we can put commit SHAs in the URL. + String httpUrl = resolveHttpUrl(remoteUrl); + if (httpUrl != null) { + data.put("httpUrl", httpUrl); + } + + // TODO(sop): Allow caching links by SHA-1 when no S cookie is sent. + render(req, res, "gitiles.pathDetail", ImmutableMap.of( + "title", view.getTreePath(), + "type", FileType.GITLINK.toString(), + "data", data)); + } + + private static String resolveHttpUrl(String remoteUrl) { + return VERBATIM_SUBMODULE_URL_PATTERN.matcher(remoteUrl).matches() ? remoteUrl : null; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java new file mode 100644 index 0000000..42ce635 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -0,0 +1,107 @@ +// 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.checkNotNull; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.template.soy.tofu.SoyTofu; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +/** Renderer for Soy templates used by Gitiles. */ +public abstract class Renderer { + private static final List<String> SOY_FILENAMES = ImmutableList.of( + "Common.soy", + "DiffDetail.soy", + "HostIndex.soy", + "LogDetail.soy", + "ObjectDetail.soy", + "PathDetail.soy", + "RevisionDetail.soy", + "RepositoryIndex.soy"); + + public static final Map<String, String> STATIC_URL_GLOBALS = ImmutableMap.of( + "gitiles.CSS_URL", "gitiles.css", + "gitiles.PRETTIFY_CSS_URL", "prettify/prettify.css", + "gitiles.PRETTIFY_JS_URL", "prettify/prettify.js"); + + protected static final URL toFileURL(String filename) { + if (filename == null) { + return null; + } + try { + return new File(filename).toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + protected ImmutableList<URL> templates; + protected ImmutableMap<String, String> globals; + + protected Renderer(Function<String, URL> resourceMapper, Map<String, String> globals, + String staticPrefix, URL customTemplates) { + checkNotNull(staticPrefix, "staticPrefix"); + List<URL> allTemplates = Lists.newArrayListWithCapacity(SOY_FILENAMES.size() + 1); + for (String filename : SOY_FILENAMES) { + allTemplates.add(resourceMapper.apply(filename)); + } + if (customTemplates != null) { + allTemplates.add(customTemplates); + } else { + allTemplates.add(resourceMapper.apply("DefaultCustomTemplates.soy")); + } + templates = ImmutableList.copyOf(allTemplates); + + Map<String, String> allGlobals = Maps.newHashMap(); + for (Map.Entry<String, String> e : STATIC_URL_GLOBALS.entrySet()) { + allGlobals.put(e.getKey(), staticPrefix + e.getValue()); + } + allGlobals.putAll(globals); + this.globals = ImmutableMap.copyOf(allGlobals); + } + + public void render(HttpServletResponse res, String templateName) throws IOException { + render(res, templateName, ImmutableMap.<String, Object> of()); + } + + public void render(HttpServletResponse res, String templateName, Map<String, ?> soyData) + throws IOException { + res.setContentType("text/html"); + res.setCharacterEncoding("UTF-8"); + byte[] data = newRenderer(templateName).setData(soyData).render().getBytes(Charsets.UTF_8); + res.setContentLength(data.length); + res.getOutputStream().write(data); + } + + SoyTofu.Renderer newRenderer(String templateName) { + return getTofu().newRenderer(templateName); + } + + protected abstract SoyTofu getTofu(); +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryDescription.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryDescription.java new file mode 100644 index 0000000..06ee4da --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryDescription.java
@@ -0,0 +1,25 @@ +// 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 java.util.Map; + +/** Describes a repository in the {@link HostIndexServlet} JSON output. */ +public class RepositoryDescription { + public String name; + public String cloneUrl; + public String description; + public Map<String, String> branches; +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java new file mode 100644 index 0000000..325068a --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
@@ -0,0 +1,80 @@ +// 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.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; +import org.eclipse.jgit.lib.RefDatabase; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves the index page for a repository, if accessed directly by a browser. */ +public class RepositoryIndexServlet extends BaseServlet { + private final GitilesAccess.Factory accessFactory; + + public RepositoryIndexServlet(Renderer renderer, GitilesAccess.Factory accessFactory) { + super(renderer); + this.accessFactory = checkNotNull(accessFactory, "accessFactory"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + render(req, res, "gitiles.repositoryIndex", buildData(req)); + } + + @VisibleForTesting + Map<String, ?> buildData(HttpServletRequest req) throws IOException { + RepositoryDescription desc = accessFactory.forRequest(req).getRepositoryDescription(); + return ImmutableMap.of( + "cloneUrl", desc.cloneUrl, + "description", Strings.nullToEmpty(desc.description), + "branches", getRefs(req, Constants.R_HEADS), + "tags", getRefs(req, Constants.R_TAGS)); + } + + private List<Map<String, String>> getRefs(HttpServletRequest req, String prefix) + throws IOException { + RefDatabase refdb = ServletUtils.getRepository(req).getRefDatabase(); + String repoName = ViewFilter.getView(req).getRepositoryName(); + Collection<Ref> refs = RefComparator.sort(refdb.getRefs(prefix).values()); + List<Map<String, String>> result = Lists.newArrayListWithCapacity(refs.size()); + + for (Ref ref : refs) { + String name = ref.getName().substring(prefix.length()); + boolean needPrefix = !ref.getName().equals(refdb.getRef(name).getName()); + result.add(ImmutableMap.of( + "url", GitilesView.log().copyFrom(req).setRevision( + Revision.unpeeled(needPrefix ? ref.getName() : name, ref.getObjectId())).toUrl(), + "name", name)); + } + + return result; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Revision.java b/gitiles-servlet/src/main/java/com/google/gitiles/Revision.java new file mode 100644 index 0000000..05778e0 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/Revision.java
@@ -0,0 +1,138 @@ +// 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.checkArgument; +import static org.eclipse.jgit.lib.Constants.OBJ_BAD; +import static org.eclipse.jgit.lib.Constants.OBJ_TAG; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.io.IOException; + +/** + * Object encapsulating a single revision as seen by Gitiles. + * <p> + * A single revision consists of a name, an ID, and a type. Name parsing is done + * once per request by {@link RevisionParser}. + */ +public class Revision { + /** Sentinel indicating a missing or empty revision. */ + public static final Revision NULL = peeled("", ObjectId.zeroId(), OBJ_BAD); + + /** Common default branch given to clients. */ + public static final Revision HEAD = named("HEAD"); + + private final String name; + private final ObjectId id; + private final int type; + private final ObjectId peeledId; + private final int peeledType; + + public static Revision peeled(String name, RevObject obj) { + return peeled(name, obj, obj.getType()); + } + + public static Revision unpeeled(String name, ObjectId id) { + return peeled(name, id, OBJ_BAD); + } + + public static Revision named(String name) { + return peeled(name, null, OBJ_BAD); + } + + public static Revision peel(String name, ObjectId id, RevWalk walk) + throws MissingObjectException, IOException { + RevObject obj = walk.parseAny(id); + RevObject peeled = walk.peel(obj); + return new Revision(name, obj, obj.getType(), peeled, peeled.getType()); + } + + private static Revision peeled(String name, ObjectId id, int type) { + checkArgument(type != OBJ_TAG, "expected non-tag for %s/%s", name, id); + return new Revision(name, id, type, id, type); + } + + @VisibleForTesting + Revision(String name, ObjectId id, int type, ObjectId peeledId, int peeledType) { + this.name = name; + this.id = id; + this.type = type; + this.peeledId = peeledId; + this.peeledType = peeledType; + } + + public String getName() { + return name; + } + + public int getType() { + return type; + } + + public ObjectId getId() { + return id; + } + + public ObjectId getPeeledId() { + return peeledId; + } + + public int getPeeledType() { + return peeledType; + } + + public boolean nameIsId() { + return AbbreviatedObjectId.isId(name) + && (AbbreviatedObjectId.fromString(name).prefixCompare(id) == 0); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Revision) { + Revision r = (Revision) o; + return Objects.equal(name, r.name) + && Objects.equal(id, r.id) + && Objects.equal(type, r.type) + && Objects.equal(peeledId, r.peeledId) + && Objects.equal(peeledType, r.peeledType); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(name, id, type, peeledId, peeledType); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .omitNullValues() + .add("name", name) + .add("id", id) + .add("type", type) + .add("peeledId", peeledId) + .add("peeledType", peeledType) + .toString(); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RevisionParser.java b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionParser.java new file mode 100644 index 0000000..3e0acfa --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionParser.java
@@ -0,0 +1,214 @@ +// 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.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CharMatcher; +import com.google.common.base.Objects; +import com.google.common.base.Splitter; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.io.IOException; + +/** Object to parse revisions out of Gitiles paths. */ +class RevisionParser { + static final Splitter PATH_SPLITTER = Splitter.on('/'); + private static final Splitter OPERATOR_SPLITTER = Splitter.on(CharMatcher.anyOf("^~")); + + static class Result { + private final Revision revision; + private final Revision oldRevision; + private final int pathStart; + + @VisibleForTesting + Result(Revision revision) { + this(revision, null, revision.getName().length()); + } + + @VisibleForTesting + Result(Revision revision, Revision oldRevision, int pathStart) { + this.revision = revision; + this.oldRevision = oldRevision; + this.pathStart = pathStart; + } + + public Revision getRevision() { + return revision; + } + + public Revision getOldRevision() { + return oldRevision; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Result) { + Result r = (Result) o; + return Objects.equal(revision, r.revision) + && Objects.equal(oldRevision, r.oldRevision) + && Objects.equal(pathStart, r.pathStart); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(revision, oldRevision, pathStart); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .omitNullValues() + .add("revision", revision) + .add("oldRevision", oldRevision) + .add("pathStart", pathStart) + .toString(); + } + + int getPathStart() { + return pathStart; + } + } + + private final Repository repo; + private final GitilesAccess access; + private final VisibilityCache cache; + + RevisionParser(Repository repo, GitilesAccess access, VisibilityCache cache) { + this.repo = checkNotNull(repo, "repo"); + this.access = checkNotNull(access, "access"); + this.cache = checkNotNull(cache, "cache"); + } + + Result parse(String path) throws IOException { + RevWalk walk = new RevWalk(repo); + try { + Revision oldRevision = null; + + StringBuilder b = new StringBuilder(); + boolean first = true; + for (String part : PATH_SPLITTER.split(path)) { + if (part.isEmpty()) { + return null; // No valid revision contains empty segments. + } + if (!first) { + b.append('/'); + } + + if (oldRevision == null) { + int dots = part.indexOf(".."); + int firstParent = part.indexOf("^!"); + if (dots == 0 || firstParent == 0) { + return null; + } else if (dots > 0) { + b.append(part.substring(0, dots)); + String oldName = b.toString(); + if (!isValidRevision(oldName)) { + return null; + } else { + ObjectId old = repo.resolve(oldName); + if (old == null) { + return null; + } + oldRevision = Revision.peel(oldName, old, walk); + } + part = part.substring(dots + 2); + b = new StringBuilder(); + } else if (firstParent > 0) { + if (firstParent != part.length() - 2) { + return null; + } + b.append(part.substring(0, part.length() - 2)); + String name = b.toString(); + if (!isValidRevision(name)) { + return null; + } + ObjectId id = repo.resolve(name); + if (id == null) { + return null; + } + RevCommit c; + try { + c = walk.parseCommit(id); + } catch (IncorrectObjectTypeException e) { + return null; // Not a commit, ^! is invalid. + } + if (c.getParentCount() > 0) { + oldRevision = Revision.peeled(name + "^", c.getParent(0)); + } else { + oldRevision = Revision.NULL; + } + Result result = new Result(Revision.peeled(name, c), oldRevision, name.length() + 2); + return isVisible(walk, result) ? result : null; + } + } + b.append(part); + + String name = b.toString(); + if (!isValidRevision(name)) { + return null; + } + ObjectId id = repo.resolve(name); + if (id != null) { + int pathStart; + if (oldRevision == null) { + pathStart = name.length(); // foo + } else { + // foo..bar (foo may be empty) + pathStart = oldRevision.getName().length() + 2 + name.length(); + } + Result result = new Result(Revision.peel(name, id, walk), oldRevision, pathStart); + return isVisible(walk, result) ? result : null; + } + first = false; + } + return null; + } finally { + walk.release(); + } + } + + private static boolean isValidRevision(String revision) { + // Disallow some uncommon but valid revision expressions that either we + // don't support or we represent differently in our URLs. + return revision.indexOf(':') < 0 + && revision.indexOf("^{") < 0 + && revision.indexOf('@') < 0; + } + + private boolean isVisible(RevWalk walk, Result result) throws IOException { + String maybeRef = OPERATOR_SPLITTER.split(result.getRevision().getName()).iterator().next(); + if (repo.getRef(maybeRef) != null) { + // Name contains a visible ref; skip expensive reachability check. + return true; + } + if (!cache.isVisible(repo, walk, access, result.getRevision().getId())) { + return false; + } + if (result.getOldRevision() != null && result.getOldRevision() != Revision.NULL) { + return cache.isVisible(repo, walk, access, result.getOldRevision().getId()); + } else { + return true; + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java new file mode 100644 index 0000000..66a7086 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java
@@ -0,0 +1,136 @@ +// 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.checkNotNull; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TAG; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gitiles.CommitSoyData.KeySet; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.http.server.ServletUtils; +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.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves an HTML page with detailed information about a ref. */ +public class RevisionServlet extends BaseServlet { + private static final Logger log = LoggerFactory.getLogger(RevisionServlet.class); + + private final Linkifier linkifier; + + public RevisionServlet(Renderer renderer, Linkifier linkifier) { + super(renderer); + this.linkifier = checkNotNull(linkifier, "linkifier"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { + GitilesView view = ViewFilter.getView(req); + Repository repo = ServletUtils.getRepository(req); + + RevWalk walk = new RevWalk(repo); + try { + List<RevObject> objects = listObjects(walk, view.getRevision().getId()); + List<Map<String, ?>> soyObjects = Lists.newArrayListWithCapacity(objects.size()); + boolean hasBlob = false; + + // TODO(sop): Allow caching commits by SHA-1 when no S cookie is sent. + for (RevObject obj : objects) { + try { + switch (obj.getType()) { + case OBJ_COMMIT: + soyObjects.add(ImmutableMap.of( + "type", Constants.TYPE_COMMIT, + "data", new CommitSoyData(linkifier, req, repo, walk, view) + .toSoyData((RevCommit) obj, KeySet.DETAIL_DIFF_TREE))); + break; + case OBJ_TREE: + soyObjects.add(ImmutableMap.of( + "type", Constants.TYPE_TREE, + "data", new TreeSoyData(walk, view).toSoyData(obj))); + break; + case OBJ_BLOB: + soyObjects.add(ImmutableMap.of( + "type", Constants.TYPE_BLOB, + "data", new BlobSoyData(walk, view).toSoyData(obj))); + hasBlob = true; + break; + case OBJ_TAG: + soyObjects.add(ImmutableMap.of( + "type", Constants.TYPE_TAG, + "data", new TagSoyData(linkifier, req).toSoyData((RevTag) obj))); + break; + default: + log.warn("Bad object type for %s: %s", ObjectId.toString(obj.getId()), obj.getType()); + res.setStatus(SC_NOT_FOUND); + return; + } + } catch (MissingObjectException e) { + log.warn("Missing object " + ObjectId.toString(obj.getId()), e); + res.setStatus(SC_NOT_FOUND); + return; + } catch (IncorrectObjectTypeException e) { + log.warn("Incorrect object type for " + ObjectId.toString(obj.getId()), e); + res.setStatus(SC_NOT_FOUND); + return; + } + } + + render(req, res, "gitiles.revisionDetail", ImmutableMap.of( + "title", view.getRevision().getName(), + "objects", soyObjects, + "hasBlob", hasBlob)); + } finally { + walk.release(); + } + } + + // TODO(dborowitz): Extract this. + static List<RevObject> listObjects(RevWalk walk, ObjectId id) + throws MissingObjectException, IOException { + List<RevObject> objects = Lists.newArrayListWithExpectedSize(1); + while (true) { + RevObject cur = walk.parseAny(id); + objects.add(cur); + if (cur.getType() == Constants.OBJ_TAG) { + id = ((RevTag) cur).getObject(); + } else { + break; + } + } + return objects; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/TagSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/TagSoyData.java new file mode 100644 index 0000000..9ddf1e5 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/TagSoyData.java
@@ -0,0 +1,50 @@ +// 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 com.google.common.collect.Maps; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.util.GitDateFormatter; +import org.eclipse.jgit.util.GitDateFormatter.Format; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +/** Soy data converter for git tags. */ +public class TagSoyData { + private final Linkifier linkifier; + private final HttpServletRequest req; + private final GitDateFormatter dateFormatter; + + public TagSoyData(Linkifier linkifier, HttpServletRequest req) { + this.linkifier = linkifier; + this.req = req; + this.dateFormatter = new GitDateFormatter(Format.DEFAULT); + } + + public Map<String, Object> toSoyData(RevTag tag) { + Map<String, Object> data = Maps.newHashMapWithExpectedSize(4); + data.put("sha", ObjectId.toString(tag)); + if (tag.getTaggerIdent() != null) { + data.put("tagger", CommitSoyData.toSoyData(tag.getTaggerIdent(), dateFormatter)); + } + data.put("object", ObjectId.toString(tag.getObject())); + data.put("message", linkifier.linkify(req, tag.getFullMessage())); + return data; + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java new file mode 100644 index 0000000..6cbf541 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
@@ -0,0 +1,160 @@ +// 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.gitiles.RevisionParser.PATH_SPLITTER; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import com.google.gitiles.PathServlet.FileType; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +/** Soy data converter for git trees. */ +public class TreeSoyData { + /** + * Number of characters to display for a symlink target. Targets longer than + * this are abbreviated for display in a tree listing. + */ + private static final int MAX_SYMLINK_TARGET_LENGTH = 72; + + /** + * Maximum number of bytes to load from a blob that claims to be a symlink. If + * the blob is larger than this byte limit it will be displayed as a binary + * file instead of as a symlink. + */ + static final int MAX_SYMLINK_SIZE = 16 << 10; + + static String resolveTargetUrl(GitilesView view, String target) { + if (target.startsWith("/")) { + return null; + } + + // simplifyPath() normalizes "a/../../" to "a", so manually check whether + // the path leads above the git root. + int depth = new StringTokenizer(view.getTreePath(), "/").countTokens(); + for (String part : PATH_SPLITTER.split(target)) { + if (part.equals("..")) { + depth--; + if (depth < 0) { + return null; + } + } else if (!part.isEmpty() && !part.equals(".")) { + depth++; + } + } + + String path = Files.simplifyPath(view.getTreePath() + "/" + target); + return GitilesView.path() + .copyFrom(view) + .setTreePath(!path.equals(".") ? path : "") + .toUrl(); + } + + @VisibleForTesting + static String getTargetDisplayName(String target) { + if (target.length() <= MAX_SYMLINK_TARGET_LENGTH) { + return target; + } else { + int lastSlash = target.lastIndexOf('/'); + // TODO(dborowitz): Doesn't abbreviate a long last path component. + return lastSlash >= 0 ? "..." + target.substring(lastSlash) : target; + } + } + + private final RevWalk rw; + private final GitilesView view; + + public TreeSoyData(RevWalk rw, GitilesView view) { + this.rw = rw; + this.view = view; + } + + public Map<String, Object> toSoyData(ObjectId treeId, TreeWalk tw) throws MissingObjectException, + IOException { + List<Object> entries = Lists.newArrayList(); + GitilesView.Builder urlBuilder = GitilesView.path().copyFrom(view); + while (tw.next()) { + FileType type = FileType.forEntry(tw); + String name = tw.getNameString(); + + switch (view.getType()) { + case PATH: + urlBuilder.setTreePath(view.getTreePath() + "/" + name); + break; + case REVISION: + // Got here from a tag pointing at a tree. + urlBuilder.setTreePath(name); + break; + default: + throw new IllegalStateException(String.format( + "Cannot render TreeSoyData from %s view", view.getType())); + } + + String url = urlBuilder.toUrl(); + if (type == FileType.TREE) { + name += "/"; + url += "/"; + } + Map<String, String> entry = Maps.newHashMapWithExpectedSize(4); + entry.put("type", type.toString()); + entry.put("name", name); + entry.put("url", url); + if (type == FileType.SYMLINK) { + String target = new String( + rw.getObjectReader().open(tw.getObjectId(0)).getCachedBytes(), + Charsets.UTF_8); + // TODO(dborowitz): Merge Shawn's changes before copying these methods + // in. + entry.put("targetName", getTargetDisplayName(target)); + String targetUrl = resolveTargetUrl(view, target); + if (targetUrl != null) { + entry.put("targetUrl", targetUrl); + } + } + entries.add(entry); + } + + Map<String, Object> data = Maps.newHashMapWithExpectedSize(3); + data.put("sha", treeId.name()); + data.put("entries", entries); + + if (view.getType() == GitilesView.Type.PATH + && view.getRevision().getPeeledType() == OBJ_COMMIT) { + data.put("logUrl", GitilesView.log().copyFrom(view).toUrl()); + } + + return data; + } + + public Map<String, Object> toSoyData(ObjectId treeId) throws MissingObjectException, IOException { + TreeWalk tw = new TreeWalk(rw.getObjectReader()); + tw.addTree(treeId); + tw.setRecursive(false); + return toSoyData(treeId, tw); + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java new file mode 100644 index 0000000..8874b1d --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -0,0 +1,160 @@ +// 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.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.http.server.glue.WrappedRequest; +import org.eclipse.jgit.lib.Constants; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Filter to parse URLs and convert them to {@link GitilesView}s. */ +public class ViewFilter extends AbstractHttpFilter { + // TODO(dborowitz): Make this public in JGit (or implement getRegexGroup + // upstream). + private static final String REGEX_GROUPS_ATTRIBUTE = + "org.eclipse.jgit.http.server.glue.MetaServlet.serveRegex"; + + private static final String VIEW_ATTIRBUTE = ViewFilter.class.getName() + "/View"; + + private static final String CMD_AUTO = "+"; + private static final String CMD_DIFF = "+diff"; + private static final String CMD_LOG = "+log"; + private static final String CMD_SHOW = "+show"; + + public static GitilesView getView(HttpServletRequest req) { + return (GitilesView) req.getAttribute(VIEW_ATTIRBUTE); + } + + static String getRegexGroup(HttpServletRequest req, int groupId) { + WrappedRequest[] groups = (WrappedRequest[]) req.getAttribute(REGEX_GROUPS_ATTRIBUTE); + return checkNotNull(groups)[groupId].getPathInfo(); + } + + static void setView(HttpServletRequest req, GitilesView view) { + req.setAttribute(VIEW_ATTIRBUTE, view); + } + + static String trimLeadingSlash(String str) { + checkArgument(str.startsWith("/"), "expected string starting with a slash: %s", str); + return str.substring(1); + } + + private final GitilesUrls urls; + private final GitilesAccess.Factory accessFactory; + private final VisibilityCache visibilityCache; + + public ViewFilter(GitilesAccess.Factory accessFactory, GitilesUrls urls, + VisibilityCache visibilityCache) { + this.urls = checkNotNull(urls, "urls"); + this.accessFactory = checkNotNull(accessFactory, "accessFactory"); + this.visibilityCache = checkNotNull(visibilityCache, "visibilityCache"); + } + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws IOException, ServletException { + GitilesView.Builder view = parse(req); + if (view == null) { + res.setStatus(SC_NOT_FOUND); + return; + } + @SuppressWarnings("unchecked") + Map<String, String[]> params = req.getParameterMap(); + view.setHostName(urls.getHostName(req)) + .setServletPath(req.getContextPath() + req.getServletPath()) + .putAllParams(params); + setView(req, view.build()); + try { + chain.doFilter(req, res); + } finally { + req.removeAttribute(VIEW_ATTIRBUTE); + } + } + + private GitilesView.Builder parse(HttpServletRequest req) throws IOException { + String repoName = trimLeadingSlash(getRegexGroup(req, 1)); + String command = getRegexGroup(req, 2); + String path = getRegexGroup(req, 3); + + // Non-path cases. + if (repoName.isEmpty()) { + return GitilesView.hostIndex(); + } else if (command.isEmpty()) { + return GitilesView.repositoryIndex().setRepositoryName(repoName); + } else if (path.isEmpty()) { + return null; // Command but no path. + } + + path = trimLeadingSlash(path); + RevisionParser revParser = new RevisionParser( + ServletUtils.getRepository(req), accessFactory.forRequest(req), visibilityCache); + RevisionParser.Result result = revParser.parse(path); + if (result == null) { + return null; + } + path = path.substring(result.getPathStart()); + + command = getCommand(command, result, path); + GitilesView.Builder view; + if (CMD_LOG.equals(command)) { + view = GitilesView.log().setTreePath(path); + } else if (CMD_SHOW.equals(command)) { + if (path.isEmpty()) { + view = GitilesView.revision(); + } else { + view = GitilesView.path().setTreePath(path); + } + } else if (CMD_DIFF.equals(command)) { + view = GitilesView.diff().setTreePath(path); + } else { + return null; // Bad command. + } + if (result.getOldRevision() != null) { // May be NULL. + view.setOldRevision(result.getOldRevision()); + } + view.setRepositoryName(repoName) + .setRevision(result.getRevision()); + return view; + } + + private String getCommand(String command, RevisionParser.Result result, String path) { + // Note: if you change the mapping for +, make sure to change + // GitilesView.toUrl() correspondingly. + if (!CMD_AUTO.equals(command)) { + return command; + } else if (result.getOldRevision() != null) { + return CMD_DIFF; + } + Revision rev = result.getRevision(); + if (rev.getPeeledType() != Constants.OBJ_COMMIT + || !path.isEmpty() + || result.getRevision().nameIsId()) { + return CMD_SHOW; + } else { + return CMD_LOG; + } + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/VisibilityCache.java b/gitiles-servlet/src/main/java/com/google/gitiles/VisibilityCache.java new file mode 100644 index 0000000..22969d1 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/VisibilityCache.java
@@ -0,0 +1,189 @@ +// 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.checkNotNull; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Collections2.filter; + +import com.google.common.base.Objects; +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** Cache of per-user object visibility. */ +public class VisibilityCache { + private static class Key { + private final Object user; + private final String repositoryName; + private final ObjectId objectId; + + private Key(Object user, String repositoryName, ObjectId objectId) { + this.user = checkNotNull(user, "user"); + this.repositoryName = checkNotNull(repositoryName, "repositoryName"); + this.objectId = checkNotNull(objectId, "objectId"); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key k = (Key) o; + return Objects.equal(user, k.user) + && Objects.equal(repositoryName, k.repositoryName) + && Objects.equal(objectId, k.objectId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(user, repositoryName, objectId); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("user", user) + .add("repositoryName", repositoryName) + .add("objectId", objectId) + .toString(); + } + } + + private final Cache<Key, Boolean> cache; + private final boolean topoSort; + + public static CacheBuilder<Object, Object> newBuilder() { + return CacheBuilder.newBuilder() + .maximumSize(1 << 10) + .expireAfterWrite(30, TimeUnit.MINUTES); + } + + public VisibilityCache(boolean topoSort) { + this(topoSort, newBuilder()); + } + + public VisibilityCache(boolean topoSort, CacheBuilder<Object, Object> builder) { + this.cache = builder.build(); + this.topoSort = topoSort; + } + + public Cache<?, Boolean> getCache() { + return cache; + } + + boolean isVisible(final Repository repo, final RevWalk walk, GitilesAccess access, + final ObjectId id) throws IOException { + try { + return cache.get( + new Key(access.getUserKey(), access.getRepositoryName(), id), + new Callable<Boolean>() { + @Override + public Boolean call() throws IOException { + return isVisible(repo, walk, id); + } + }); + } catch (ExecutionException e) { + Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + throw new IOException(e); + } + } + + private boolean isVisible(Repository repo, RevWalk walk, ObjectId id) throws IOException { + RevCommit commit; + try { + commit = walk.parseCommit(id); + } catch (IncorrectObjectTypeException e) { + return false; + } + + // If any reference directly points at the requested object, permit display. + // Common for displays of pending patch sets in Gerrit Code Review, or + // bookmarks to the commit a tag points at. + Collection<Ref> allRefs = repo.getRefDatabase().getRefs(RefDatabase.ALL).values(); + for (Ref ref : allRefs) { + ref = repo.getRefDatabase().peel(ref); + if (id.equals(ref.getObjectId()) || id.equals(ref.getPeeledObjectId())) { + return true; + } + } + + // Check heads first under the assumption that most requests are for refs + // close to a head. Tags tend to be much further back in history and just + // clutter up the priority queue in the common case. + return isReachableFrom(walk, commit, filter(allRefs, refStartsWith(Constants.R_HEADS))) + || isReachableFrom(walk, commit, filter(allRefs, refStartsWith(Constants.R_TAGS))) + || isReachableFrom(walk, commit, filter(allRefs, not(refStartsWith("refs/changes/")))); + } + + private static Predicate<Ref> refStartsWith(final String prefix) { + return new Predicate<Ref>() { + @Override + public boolean apply(Ref ref) { + return ref.getName().startsWith(prefix); + } + }; + } + + private boolean isReachableFrom(RevWalk walk, RevCommit commit, Collection<Ref> refs) + throws IOException { + walk.reset(); + if (topoSort) { + walk.sort(RevSort.TOPO); + } + walk.markStart(commit); + for (Ref ref : refs) { + if (ref.getPeeledObjectId() != null) { + markUninteresting(walk, ref.getPeeledObjectId()); + } else { + markUninteresting(walk, ref.getObjectId()); + } + } + // If the commit is reachable from any branch head, it will appear to be + // uninteresting to the RevWalk and no output will be produced. + return walk.next() == null; + } + + private static void markUninteresting(RevWalk walk, ObjectId id) throws IOException { + if (id == null) { + return; + } + try { + walk.markUninteresting(walk.parseCommit(id)); + } catch (IncorrectObjectTypeException e) { + // Do nothing, doesn't affect reachability. + } catch (MissingObjectException e) { + // Do nothing, doesn't affect reachability. + } + } +}
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 new file mode 100644 index 0000000..0c57519 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
@@ -0,0 +1,319 @@ +/** + * 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. + */ + +/* Common styles and definitions. */ + +h1 { + position: absolute; + top: 0; + white-space: nowrap; + margin-top: 5px; +} +.menu { + position: absolute; + top: 0; + right: 0; + font-size: 10pt; + white-space: nowrap; + text-align: right; + margin-top: 5px; + margin-right: 5px; +} +.menu .entry { + padding-right: 5px; + border-right: 1px solid black; + margin-right: 0; +} +h2 { + margin-top: 3em; +} +.breadcrumbs { + margin-top: 3em; + font-size: 150%; + border-bottom: #ddd solid 1px; /* BORDER */ +} +table.list { + margin-top: 1em; + width: 90%; +} +table.list tr.no-hover:hover { + background: #fff; +} +table.list tr:hover, ol.list li:hover, pre.prettyprint li:hover { + background: #eee; /* HOVER_BACKGROUND */ +} +table.list td { + white-space: nowrap; + padding-top: 0.25em; + padding-bottom: 0.25em; +} +.log-link { + margin-left: 0.5em +} + + +/* Styles for the host index page. */ + +.instructions { + width: 45em; + margin-left: 1em; + margin-right: 1em; + border-top: 1px solid #555; + border-bottom: 1px solid #555; + color: #555; +} +.instructions pre { + display: block; + margin-left: 1em; + border-left: 2px solid #060; + padding-left: 1em; + white-space: nowrap; +} +.footer { + text-align: right; + background: #eee; + padding-right: 1em; + width: 90%; +} +.footer a { + color: black; + font-weight: bold; + font-size: 70%; +} + + +/* Styles for the repository index page. */ + +.repository-description { + border-bottom: #ddd solid 1px; /* BORDER */ + padding-bottom: 5px; /* VPADDING */ +} +.repository-refs { + width: 450px; +} +.repository-branches { + float: left; + width: 200px; +} +.repository-tags { + float: right; + width: 250px; +} +.clone-line { + background-color: #e5ecf9; /* BOX_BACKGROUND */ + border: none; + margin: 5px /* VPADDING */ 0 0 0; + padding: 5px 2em; /* PADDING */ + font-size: 9pt; +} + + +/* Styles for the object detail templates. */ + +.sha1 { + color: #666; + font-size: 9pt; +} +div.sha1 { + padding-top: 5px; /* VPADDING */ +} + +.git-commit, .git-tag { + font-size: 9pt; + border-bottom: #ddd solid 1px; /* BORDER */ + padding: 5px 2em; /* PADDING */ +} +.git-commit table, .git-tag table { + margin: 0; +} +.git-commit table th, .git-tag table th { + text-align: right; +} +pre.commit-message, pre.tag-message { + border-bottom: #ddd solid 1px; /* BORDER */ + padding: 5px 2em; /* PADDING */ + color: #000; + font-size: 9pt; + margin: 0; +} + +ul.diff-tree { + font-size: 9pt; + list-style-type: none; + margin: 0; + padding: 5px 2em; /* PADDING */ +} +ul.diff-tree .add { + color: #060; +} +ul.diff-tree .delete { + color: #600; +} +ul.diff-tree .rename, ul.diff-tree .copy { + color: #006; +} +span.diff-link, ul.diff-tree .add, ul.diff-tree .modify, ul.diff-tree .delete, + ul.diff-tree .rename, ul.diff-tree .copy { + margin-left: 0.5em; +} +.diff-summary { + font-size: 9pt; + font-style: italic; + padding: 5px 2em; /* PADDING */ + border-bottom: #ddd solid 1px; /* BORDER */ +} + +ol.files { + list-style-type: none; + margin-left: 1em; + font-size: 10pt; + line-height: normal; +} + +/* Tree icons are taken from the public domain Apache standard icons: + * http://www.apache.org/icons/ */ +ol.files li.git-tree{ + /* small/folder.png */ + list-style-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAA+klEQVQ4y63TMWvCQBjG8f8r+RouRSe7ONchuCTQsZS6FNLBSQrdi1PmFhcnhwpOUjoK3iIvRWcdnP0q16FcSOSijfSZjht+PNy9r6gqAGEYWgBVFSpEANtq1nl7TQCIk5Q8dA4OAPZmxHX0AsByOiw0Wk6HRJ020vBDAlh7+MouHOQauUSdNma9JU7SAhIcV9qbkRcy6633DQJKUgb9GTiG5Ooue598aucAAPO9A+Djc3UZcCr/A/Se37OalQFVlfliQ5ykF0E1+J2uKtCg/2jdufCN+fmfLzY83N7wdN8tRcaTmXjnwAeVRdw6n4pbLIBWsw7AeDITgB/OTHEfF3G6oAAAAABJRU5ErkJggg==); +} +ol.files li.symlink{ + /* small/forward.png */ + list-style-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAAAEgAAABIAEbJaz4AAAAJdnBBZwAAABAAAAAQAFzGrcMAAACtSURBVCjPfZGxEYMwDEWffNTswApcGs+RY4KUtDSZgIbWJRNwWSHnjibHCtkhCyiFDYYE+Krk/0/S9xdPQq4f4QfZuukpFC6yI3jpIz4UexPuyiFMrsMJDebJOcwbx5WSMl4QTsyXqVkBNFig5AagwU+1dTECI446unBM2ga7nXJQVj0ePPWpxEAlHfbQhfglg0EnxhVlaSUK5qhg/pUm0htBkkFPJasV/0jBfwHsdko9kUCoMwAAAABJRU5ErkJggg==); +} +ol.files li.regular-file{ + /* small/text.png */ + list-style-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAAAEgAAABIAEbJaz4AAAAJdnBBZwAAABAAAAAQAFzGrcMAAACDSURBVCjPYzzAgB8woQuc+Y/KZzz33xCJe55BkmEpgwkjHhMkGKJRTMFQcJ7hOYMJI0IJCwMDA4MVXPoYhP2fgcEEWcE6JBOOMRxkZGBYjWpCEJI0A4P9/xdIGtBMeMHAwCDBIIGuIIgBFRzDbQImoJcJx/CZ8Imx9/99DOFzDKFQFgBj6yGwhtEzOwAAAABJRU5ErkJggg==) +} +ol.files li.executable-file{ + /* small/patch.png */ + list-style-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAAJdnBBZwAAABAAAAAQAFzGrcMAAACDSURBVCjPlY+xDYMwEEW/I4agZYRQsgojkC4ZAzp2ojMjuPQYjwJEcvgSKV+yzrKenv9J/2bk8hD5TCRjkdvVUKs3lgJYlfUKbyRE7gbojrkEtwOAJA18/UKC7HWYjrMXLYBJSQ+lEymAp+x0DHY6hkazGsfgrAnwc02bqg0jqfSetw2j7lch7TZvfQAAACR0RVh0Y29tbWVudABieSBNYXJ0aW4uS3JhZW1lckBNY2guU05JLkRlaCPF6QAAAABJRU5ErkJggg==); +} +ol.files li.gitlink{ + /* small/continued.png */ + list-style-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAAAEgAAABIAEbJaz4AAAAJdnBBZwAAABAAAAAQAFzGrcMAAACPSURBVCjPzdCxbcNAFIPh7wQvkTIjBEjj1iscXGUSTZDGLjVCKuFWCK5Lc/AKGoUppNq1Wf18IB4Ilu65Ji8SmDOy2znzQeOgiTmLZoQ1VbWGkWYxh2mk+vBdGh6+XDzQLN5UIyc2d9vx+Ad/4BdNpTtHzrmlu0UctF+70rHm3WeBNVzLXnJzLfbAKyz5TP/rekGK5PrZfgAAAABJRU5ErkJggg==); +} + + +/* Styles for the path detail page. */ + +.symlink-detail, .gitlink-detail { + margin-left: 1em; + color: #666; + font-style: italic; + font-size: 10pt; +} + + +/* Styles for the log detail page. */ + +ol.shortlog { + list-style-type: none; + margin: 0; + padding: 5px 2em; /* PADDING */ +} +ol.shortlog li { + border-bottom: #ddd solid 1px; /* BORDER */ + padding-top: 2px; + padding-bottom: 2px; +} +ol.shortlog li.first { + border-top: #ddd solid 1px; /* BORDER */ +} +ol.shortlog li:hover { + background: #eee; /* HOVER_BACKGROUND */ +} +ol.shortlog .sha1 { + font-family: monospace; +} +.log-nav { + margin-top: 5px; + text-align: center; +} +.author { + padding-left: 3px; +} +.time { + font-size: 9pt; /* SHORTLOG_SMALL_FONT_SIZE */ + font-style: italic; +} +.branch-label, .tag-label { + font-size: 9pt; /* SHORTLOG_SMALL_FONT_SIZE */ + margin-left: 3px; +} +a.branch-label { + color: #dd4b39; +} +a.tag-label { + color: #009933; +} + + +/* Styles for the diff detail template. */ + +.diff-header { +} +.diff-git { + color: #444; + font-weight: bold; +} +a.diff-git:hover { + text-decoration: none; +} +.diff-header, .diff-unified { + color: #000; + font-size: 9pt; + margin: 0; + padding-left: 2em; +} +.diff-unified { + border-bottom: #ddd solid 1px; /* BORDER */ +} +.diff-unified .h { + color: darkblue; +} +.diff-unified .d { + color: darkred; +} +.diff-unified .i { + color: darkgreen; +} + + +/* Override some styles from the default prettify.css. */ + +/* Line numbers on all lines. */ +li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { + list-style-type: decimal; +} + +/* Disable alternating line background color. */ +li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { + background: #fff; +} + +pre.git-blob { + border-top: #ddd solid 1px; /* BORDER */ + border-bottom: #ddd solid 1px; /* BORDER */ + border-left: none; + border-right: none; + padding-left: 1em; + padding-bottom: 5px; + font-family: monospace; + font-size: 8pt; +} +pre.prettyprint ol { + color: grey; +}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/COPYING b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/COPYING new file mode 100644 index 0000000..37a41c0 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/COPYING
@@ -0,0 +1,214 @@ +google-code-prettify downloaded from: +http://google-code-prettify.googlecode.com/files/prettify-small-1-Jun-2011.tar.bz2 + +All files under the Apache License, with the following copyrights: +prettify.js: Copyright (C) 2006 Google Inc. +lang-apollo.js: Copyright (C) 2009 Onno Hommes. +lang-clj.js: * @license Copyright (C) 2011 Google Inc. +lang-css.js: Copyright (C) 2009 Google Inc. +lang-go.js: Copyright (C) 2010 Google Inc. +lang-hs.js: Copyright (C) 2009 Google Inc. +lang-lisp.js: Copyright (C) 2008 Google Inc. +lang-lua.js: Copyright (C) 2008 Google Inc. +lang-ml.js: Copyright (C) 2008 Google Inc. +lang-n.js: Copyright (C) 2011 Zimin A.V. +lang-proto.js: Copyright (C) 2006 Google Inc. +lang-scala.js: Copyright (C) 2010 Google Inc. +lang-sql.js: Copyright (C) 2008 Google Inc. +lang-tex.js: Copyright (C) 2011 Martin S. +lang-vb.js: Copyright (C) 2009 Google Inc. +lang-wiki.js: Copyright (C) 2009 Google Inc. +lang-xq.js: Copyright (C) 2011 Patrick Wied + +=============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2011 Mike Samuel et al + + 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.
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-apollo.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-apollo.js new file mode 100644 index 0000000..7098baf --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-apollo.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\n\r]*/,null,"#"],["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/, +null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[ES]?BANK=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[!-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["apollo","agc","aea"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-clj.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-clj.js new file mode 100644 index 0000000..542a220 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-clj.js
@@ -0,0 +1,18 @@ +/* + Copyright (C) 2011 Google Inc. + + 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. +*/ +var a=null; +PR.registerLangHandler(PR.createSimpleLexer([["opn",/^[([{]+/,a,"([{"],["clo",/^[)\]}]+/,a,")]}"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \xa0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/,a], +["typ",/^:[\dA-Za-z-]+/]]),["clj"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-css.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-css.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-go.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-go.js new file mode 100644 index 0000000..fc18dc0 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-go.js
@@ -0,0 +1 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-hs.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-hs.js new file mode 100644 index 0000000..9d77b08 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-hs.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/, +null],["pln",/^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/],["pun",/^[^\d\t-\r "'A-Za-z]+/]]),["hs"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lisp.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lisp.js new file mode 100644 index 0000000..02a30e8 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lisp.js
@@ -0,0 +1,3 @@ +var a=null; +PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(+/,a,"("],["clo",/^\)+/,a,")"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \xa0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,a], +["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["cl","el","lisp","scm"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lua.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lua.js new file mode 100644 index 0000000..e83a3c4 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-lua.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\S\s]*?(?:]\1]|$)|[^\n\r]*)/],["str",/^\[(=*)\[[\S\s]*?(?:]\1]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i], +["pln",/^[_a-z]\w*/i],["pun",/^[^\w\t\n\r \xa0][^\w\t\n\r "'+=\xa0-]*/]]),["lua"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-ml.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-ml.js new file mode 100644 index 0000000..6df02d7 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-ml.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["com",/^#(?:if[\t\n\r \xa0]+(?:[$_a-z][\w']*|``[^\t\n\r`]*(?:``|$))|else|endif|light)/i,null,"#"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])(?:'|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\(\*[\S\s]*?\*\))/],["kwd",/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/], +["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^(?:[_a-z][\w']*[!#?]?|``[^\t\n\r`]*(?:``|$))/i],["pun",/^[^\w\t\n\r "'\xa0]+/]]),["fs","ml"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-n.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-n.js new file mode 100644 index 0000000..6c2e85b --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-n.js
@@ -0,0 +1,4 @@ +var a=null; +PR.registerLangHandler(PR.createSimpleLexer([["str",/^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,a,'"'],["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,a,"#"],["pln",/^\s+/,a," \r\n\t\xa0"]],[["str",/^@"(?:[^"]|"")*(?:"|$)/,a],["str",/^<#[^#>]*(?:#>|$)/,a],["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,a],["com",/^\/\/[^\n\r]*/,a],["com",/^\/\*[\S\s]*?(?:\*\/|$)/, +a],["kwd",/^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/, +a],["typ",/^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/,a],["lit",/^@[$_a-z][\w$@]*/i,a],["typ",/^@[A-Z]+[a-z][\w$@]*/,a],["pln",/^'?[$_a-z][\w$@]*/i,a],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,a,"0123456789"],["pun",/^.[^\s\w"-$'./@`]*/,a]]),["n","nemerle"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-proto.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-proto.js new file mode 100644 index 0000000..f006ad8 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-proto.js
@@ -0,0 +1 @@ +PR.registerLangHandler(PR.sourceDecorator({keywords:"bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true",types:/^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/,cStyleComments:!0}),["proto"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-scala.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-scala.js new file mode 100644 index 0000000..60d034d --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-scala.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["str",/^"(?:""(?:""?(?!")|[^"\\]|\\.)*"{0,3}|(?:[^\n\r"\\]|\\.)*"?)/,null,'"'],["lit",/^`(?:[^\n\r\\`]|\\.)*`?/,null,"`"],["pun",/^[!#%&(--:-@[-^{-~]+/,null,"!#%&()*+,-:;<=>?@[\\]^{|}~"]],[["str",/^'(?:[^\n\r'\\]|\\(?:'|[^\n\r']+))'/],["lit",/^'[$A-Z_a-z][\w$]*(?![\w$'])/],["kwd",/^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/], +["lit",/^(?:true|false|null|this)\b/],["lit",/^(?:0(?:[0-7]+|x[\da-f]+)l?|(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:e[+-]?\d+)?f?|l?)|\\.\d+(?:e[+-]?\d+)?f?)/i],["typ",/^[$_]*[A-Z][\d$A-Z_]*[a-z][\w$]*/],["pln",/^[$A-Z_a-z][\w$]*/],["com",/^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/],["pun",/^(?:\.+|\/)/]]),["scala"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-sql.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-sql.js new file mode 100644 index 0000000..da705b0 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-sql.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|merge|national|nocheck|nonclustered|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|percent|plan|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rule|save|schema|select|session_user|set|setuser|shutdown|some|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|union|unique|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|writetext)(?=[^\w-]|$)/i, +null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-tex.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-tex.js new file mode 100644 index 0000000..ce96fbb --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-tex.js
@@ -0,0 +1 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"],["com",/^%[^\n\r]*/,null,"%"]],[["kwd",/^\\[@-Za-z]+/],["kwd",/^\\./],["typ",/^[$&]/],["lit",/[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i],["pun",/^[()=[\]{}]+/]]),["latex","tex"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vb.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vb.js new file mode 100644 index 0000000..07506b0 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vb.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0\u2028\u2029]+/,null,"\t\n\r Â\xa0â¨â©"],["str",/^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i,null,'"ââ'],["com",/^['\u2018\u2019].*/,null,"'ââ"]],[["kwd",/^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i, +null],["com",/^rem.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vhdl.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vhdl.js new file mode 100644 index 0000000..128b5b6 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-vhdl.js
@@ -0,0 +1,3 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r Â\xa0"]],[["str",/^(?:[box]?"(?:[^"]|"")*"|'.')/i],["com",/^--[^\n\r]*/],["kwd",/^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i, +null],["typ",/^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i,null],["typ",/^'(?:active|ascending|base|delayed|driving|driving_value|event|high|image|instance_name|last_active|last_event|last_value|left|leftof|length|low|path_name|pos|pred|quiet|range|reverse_range|right|rightof|simple_name|stable|succ|transaction|val|value)(?=[^\w-]|$)/i,null],["lit",/^\d+(?:_\d+)*(?:#[\w.\\]+#(?:[+-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:e[+-]?\d+(?:_\d+)*)?)/i], +["pln",/^(?:[a-z]\w*|\\[^\\]*\\)/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'\xa0-]*/]]),["vhdl","vhd"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-wiki.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-wiki.js new file mode 100644 index 0000000..9b0b448 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-wiki.js
@@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\d\t a-gi-z\xa0]+/,null,"\t Â\xa0abcdefgijklmnopqrstuvwxyz0123456789"],["pun",/^[*=[\]^~]+/,null,"=*~^[]"]],[["lang-wiki.meta",/(?:^^|\r\n?|\n)(#[a-z]+)\b/],["lit",/^[A-Z][a-z][\da-z]+[A-Z][a-z][^\W_]+\b/],["lang-",/^{{{([\S\s]+?)}}}/],["lang-",/^`([^\n\r`]+)`/],["str",/^https?:\/\/[^\s#/?]*(?:\/[^\s#?]*)?(?:\?[^\s#]*)?(?:#\S*)?/i],["pln",/^(?:\r\n|[\S\s])[^\n\r#*=A-[^`h{~]*/]]),["wiki"]); +PR.registerLangHandler(PR.createSimpleLexer([["kwd",/^#[a-z]+/i,null,"#"]],[]),["wiki.meta"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-xq.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-xq.js new file mode 100644 index 0000000..e323ae3 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-xq.js
@@ -0,0 +1,3 @@ +PR.registerLangHandler(PR.createSimpleLexer([["var pln",/^\$[\w-]+/,null,"$"]],[["pln",/^[\s=][<>][\s=]/],["lit",/^@[\w-]+/],["tag",/^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["com",/^\(:[\S\s]*?:\)/],["pln",/^[(),/;[\]{}]$/],["str",/^(?:"(?:[^"\\{]|\\[\S\s])*(?:"|$)|'(?:[^'\\{]|\\[\S\s])*(?:'|$))/,null,"\"'"],["kwd",/^(?:xquery|where|version|variable|union|typeswitch|treat|to|then|text|stable|sortby|some|self|schema|satisfies|returns|return|ref|processing-instruction|preceding-sibling|preceding|precedes|parent|only|of|node|namespace|module|let|item|intersect|instance|in|import|if|function|for|follows|following-sibling|following|external|except|every|else|element|descending|descendant-or-self|descendant|define|default|declare|comment|child|cast|case|before|attribute|assert|ascending|as|ancestor-or-self|ancestor|after|eq|order|by|or|and|schema-element|document-node|node|at)\b/], +["typ",/^(?:xs:yearMonthDuration|xs:unsignedLong|xs:time|xs:string|xs:short|xs:QName|xs:Name|xs:long|xs:integer|xs:int|xs:gYearMonth|xs:gYear|xs:gMonthDay|xs:gDay|xs:float|xs:duration|xs:double|xs:decimal|xs:dayTimeDuration|xs:dateTime|xs:date|xs:byte|xs:boolean|xs:anyURI|xf:yearMonthDuration)\b/,null],["fun pln",/^(?:xp:dereference|xinc:node-expand|xinc:link-references|xinc:link-expand|xhtml:restructure|xhtml:clean|xhtml:add-lists|xdmp:zip-manifest|xdmp:zip-get|xdmp:zip-create|xdmp:xquery-version|xdmp:word-convert|xdmp:with-namespaces|xdmp:version|xdmp:value|xdmp:user-roles|xdmp:user-last-login|xdmp:user|xdmp:url-encode|xdmp:url-decode|xdmp:uri-is-file|xdmp:uri-format|xdmp:uri-content-type|xdmp:unquote|xdmp:unpath|xdmp:triggers-database|xdmp:trace|xdmp:to-json|xdmp:tidy|xdmp:subbinary|xdmp:strftime|xdmp:spawn-in|xdmp:spawn|xdmp:sleep|xdmp:shutdown|xdmp:set-session-field|xdmp:set-response-encoding|xdmp:set-response-content-type|xdmp:set-response-code|xdmp:set-request-time-limit|xdmp:set|xdmp:servers|xdmp:server-status|xdmp:server-name|xdmp:server|xdmp:security-database|xdmp:security-assert|xdmp:schema-database|xdmp:save|xdmp:role-roles|xdmp:role|xdmp:rethrow|xdmp:restart|xdmp:request-timestamp|xdmp:request-status|xdmp:request-cancel|xdmp:request|xdmp:redirect-response|xdmp:random|xdmp:quote|xdmp:query-trace|xdmp:query-meters|xdmp:product-edition|xdmp:privilege-roles|xdmp:privilege|xdmp:pretty-print|xdmp:powerpoint-convert|xdmp:platform|xdmp:permission|xdmp:pdf-convert|xdmp:path|xdmp:octal-to-integer|xdmp:node-uri|xdmp:node-replace|xdmp:node-kind|xdmp:node-insert-child|xdmp:node-insert-before|xdmp:node-insert-after|xdmp:node-delete|xdmp:node-database|xdmp:mul64|xdmp:modules-root|xdmp:modules-database|xdmp:merging|xdmp:merge-cancel|xdmp:merge|xdmp:md5|xdmp:logout|xdmp:login|xdmp:log-level|xdmp:log|xdmp:lock-release|xdmp:lock-acquire|xdmp:load|xdmp:invoke-in|xdmp:invoke|xdmp:integer-to-octal|xdmp:integer-to-hex|xdmp:http-put|xdmp:http-post|xdmp:http-options|xdmp:http-head|xdmp:http-get|xdmp:http-delete|xdmp:hosts|xdmp:host-status|xdmp:host-name|xdmp:host|xdmp:hex-to-integer|xdmp:hash64|xdmp:hash32|xdmp:has-privilege|xdmp:groups|xdmp:group-serves|xdmp:group-servers|xdmp:group-name|xdmp:group-hosts|xdmp:group|xdmp:get-session-field-names|xdmp:get-session-field|xdmp:get-response-encoding|xdmp:get-response-code|xdmp:get-request-username|xdmp:get-request-user|xdmp:get-request-url|xdmp:get-request-protocol|xdmp:get-request-path|xdmp:get-request-method|xdmp:get-request-header-names|xdmp:get-request-header|xdmp:get-request-field-names|xdmp:get-request-field-filename|xdmp:get-request-field-content-type|xdmp:get-request-field|xdmp:get-request-client-certificate|xdmp:get-request-client-address|xdmp:get-request-body|xdmp:get-current-user|xdmp:get-current-roles|xdmp:get|xdmp:function-name|xdmp:function-module|xdmp:function|xdmp:from-json|xdmp:forests|xdmp:forest-status|xdmp:forest-restore|xdmp:forest-restart|xdmp:forest-name|xdmp:forest-delete|xdmp:forest-databases|xdmp:forest-counts|xdmp:forest-clear|xdmp:forest-backup|xdmp:forest|xdmp:filesystem-file|xdmp:filesystem-directory|xdmp:exists|xdmp:excel-convert|xdmp:eval-in|xdmp:eval|xdmp:estimate|xdmp:email|xdmp:element-content-type|xdmp:elapsed-time|xdmp:document-set-quality|xdmp:document-set-property|xdmp:document-set-properties|xdmp:document-set-permissions|xdmp:document-set-collections|xdmp:document-remove-properties|xdmp:document-remove-permissions|xdmp:document-remove-collections|xdmp:document-properties|xdmp:document-locks|xdmp:document-load|xdmp:document-insert|xdmp:document-get-quality|xdmp:document-get-properties|xdmp:document-get-permissions|xdmp:document-get-collections|xdmp:document-get|xdmp:document-forest|xdmp:document-delete|xdmp:document-add-properties|xdmp:document-add-permissions|xdmp:document-add-collections|xdmp:directory-properties|xdmp:directory-locks|xdmp:directory-delete|xdmp:directory-create|xdmp:directory|xdmp:diacritic-less|xdmp:describe|xdmp:default-permissions|xdmp:default-collections|xdmp:databases|xdmp:database-restore-validate|xdmp:database-restore-status|xdmp:database-restore-cancel|xdmp:database-restore|xdmp:database-name|xdmp:database-forests|xdmp:database-backup-validate|xdmp:database-backup-status|xdmp:database-backup-purge|xdmp:database-backup-cancel|xdmp:database-backup|xdmp:database|xdmp:collection-properties|xdmp:collection-locks|xdmp:collection-delete|xdmp:collation-canonical-uri|xdmp:castable-as|xdmp:can-grant-roles|xdmp:base64-encode|xdmp:base64-decode|xdmp:architecture|xdmp:apply|xdmp:amp-roles|xdmp:amp|xdmp:add64|xdmp:add-response-header|xdmp:access|trgr:trigger-set-recursive|trgr:trigger-set-permissions|trgr:trigger-set-name|trgr:trigger-set-module|trgr:trigger-set-event|trgr:trigger-set-description|trgr:trigger-remove-permissions|trgr:trigger-module|trgr:trigger-get-permissions|trgr:trigger-enable|trgr:trigger-disable|trgr:trigger-database-online-event|trgr:trigger-data-event|trgr:trigger-add-permissions|trgr:remove-trigger|trgr:property-content|trgr:pre-commit|trgr:post-commit|trgr:get-trigger-by-id|trgr:get-trigger|trgr:document-scope|trgr:document-content|trgr:directory-scope|trgr:create-trigger|trgr:collection-scope|trgr:any-property-content|thsr:set-entry|thsr:remove-term|thsr:remove-synonym|thsr:remove-entry|thsr:query-lookup|thsr:lookup|thsr:load|thsr:insert|thsr:expand|thsr:add-synonym|spell:suggest-detailed|spell:suggest|spell:remove-word|spell:make-dictionary|spell:load|spell:levenshtein-distance|spell:is-correct|spell:insert|spell:double-metaphone|spell:add-word|sec:users-collection|sec:user-set-roles|sec:user-set-password|sec:user-set-name|sec:user-set-description|sec:user-set-default-permissions|sec:user-set-default-collections|sec:user-remove-roles|sec:user-privileges|sec:user-get-roles|sec:user-get-description|sec:user-get-default-permissions|sec:user-get-default-collections|sec:user-doc-permissions|sec:user-doc-collections|sec:user-add-roles|sec:unprotect-collection|sec:uid-for-name|sec:set-realm|sec:security-version|sec:security-namespace|sec:security-installed|sec:security-collection|sec:roles-collection|sec:role-set-roles|sec:role-set-name|sec:role-set-description|sec:role-set-default-permissions|sec:role-set-default-collections|sec:role-remove-roles|sec:role-privileges|sec:role-get-roles|sec:role-get-description|sec:role-get-default-permissions|sec:role-get-default-collections|sec:role-doc-permissions|sec:role-doc-collections|sec:role-add-roles|sec:remove-user|sec:remove-role-from-users|sec:remove-role-from-role|sec:remove-role-from-privileges|sec:remove-role-from-amps|sec:remove-role|sec:remove-privilege|sec:remove-amp|sec:protect-collection|sec:privileges-collection|sec:privilege-set-roles|sec:privilege-set-name|sec:privilege-remove-roles|sec:privilege-get-roles|sec:privilege-add-roles|sec:priv-doc-permissions|sec:priv-doc-collections|sec:get-user-names|sec:get-unique-elem-id|sec:get-role-names|sec:get-role-ids|sec:get-privilege|sec:get-distinct-permissions|sec:get-collection|sec:get-amp|sec:create-user-with-role|sec:create-user|sec:create-role|sec:create-privilege|sec:create-amp|sec:collections-collection|sec:collection-set-permissions|sec:collection-remove-permissions|sec:collection-get-permissions|sec:collection-add-permissions|sec:check-admin|sec:amps-collection|sec:amp-set-roles|sec:amp-remove-roles|sec:amp-get-roles|sec:amp-doc-permissions|sec:amp-doc-collections|sec:amp-add-roles|search:unparse|search:suggest|search:snippet|search:search|search:resolve-nodes|search:resolve|search:remove-constraint|search:parse|search:get-default-options|search:estimate|search:check-options|prof:value|prof:reset|prof:report|prof:invoke|prof:eval|prof:enable|prof:disable|prof:allowed|ppt:clean|pki:template-set-request|pki:template-set-name|pki:template-set-key-type|pki:template-set-key-options|pki:template-set-description|pki:template-in-use|pki:template-get-version|pki:template-get-request|pki:template-get-name|pki:template-get-key-type|pki:template-get-key-options|pki:template-get-id|pki:template-get-description|pki:need-certificate|pki:is-temporary|pki:insert-trusted-certificates|pki:insert-template|pki:insert-signed-certificates|pki:insert-certificate-revocation-list|pki:get-trusted-certificate-ids|pki:get-template-ids|pki:get-template-certificate-authority|pki:get-template-by-name|pki:get-template|pki:get-pending-certificate-requests-xml|pki:get-pending-certificate-requests-pem|pki:get-pending-certificate-request|pki:get-certificates-for-template-xml|pki:get-certificates-for-template|pki:get-certificates|pki:get-certificate-xml|pki:get-certificate-pem|pki:get-certificate|pki:generate-temporary-certificate-if-necessary|pki:generate-temporary-certificate|pki:generate-template-certificate-authority|pki:generate-certificate-request|pki:delete-template|pki:delete-certificate|pki:create-template|pdf:make-toc|pdf:insert-toc-headers|pdf:get-toc|pdf:clean|p:status-transition|p:state-transition|p:remove|p:pipelines|p:insert|p:get-by-id|p:get|p:execute|p:create|p:condition|p:collection|p:action|ooxml:runs-merge|ooxml:package-uris|ooxml:package-parts-insert|ooxml:package-parts|msword:clean|mcgm:polygon|mcgm:point|mcgm:geospatial-query-from-elements|mcgm:geospatial-query|mcgm:circle|math:tanh|math:tan|math:sqrt|math:sinh|math:sin|math:pow|math:modf|math:log10|math:log|math:ldexp|math:frexp|math:fmod|math:floor|math:fabs|math:exp|math:cosh|math:cos|math:ceil|math:atan2|math:atan|math:asin|math:acos|map:put|map:map|map:keys|map:get|map:delete|map:count|map:clear|lnk:to|lnk:remove|lnk:insert|lnk:get|lnk:from|lnk:create|kml:polygon|kml:point|kml:interior-polygon|kml:geospatial-query-from-elements|kml:geospatial-query|kml:circle|kml:box|gml:polygon|gml:point|gml:interior-polygon|gml:geospatial-query-from-elements|gml:geospatial-query|gml:circle|gml:box|georss:point|georss:geospatial-query|georss:circle|geo:polygon|geo:point|geo:interior-polygon|geo:geospatial-query-from-elements|geo:geospatial-query|geo:circle|geo:box|fn:zero-or-one|fn:years-from-duration|fn:year-from-dateTime|fn:year-from-date|fn:upper-case|fn:unordered|fn:true|fn:translate|fn:trace|fn:tokenize|fn:timezone-from-time|fn:timezone-from-dateTime|fn:timezone-from-date|fn:sum|fn:subtract-dateTimes-yielding-yearMonthDuration|fn:subtract-dateTimes-yielding-dayTimeDuration|fn:substring-before|fn:substring-after|fn:substring|fn:subsequence|fn:string-to-codepoints|fn:string-pad|fn:string-length|fn:string-join|fn:string|fn:static-base-uri|fn:starts-with|fn:seconds-from-time|fn:seconds-from-duration|fn:seconds-from-dateTime|fn:round-half-to-even|fn:round|fn:root|fn:reverse|fn:resolve-uri|fn:resolve-QName|fn:replace|fn:remove|fn:QName|fn:prefix-from-QName|fn:position|fn:one-or-more|fn:number|fn:not|fn:normalize-unicode|fn:normalize-space|fn:node-name|fn:node-kind|fn:nilled|fn:namespace-uri-from-QName|fn:namespace-uri-for-prefix|fn:namespace-uri|fn:name|fn:months-from-duration|fn:month-from-dateTime|fn:month-from-date|fn:minutes-from-time|fn:minutes-from-duration|fn:minutes-from-dateTime|fn:min|fn:max|fn:matches|fn:lower-case|fn:local-name-from-QName|fn:local-name|fn:last|fn:lang|fn:iri-to-uri|fn:insert-before|fn:index-of|fn:in-scope-prefixes|fn:implicit-timezone|fn:idref|fn:id|fn:hours-from-time|fn:hours-from-duration|fn:hours-from-dateTime|fn:floor|fn:false|fn:expanded-QName|fn:exists|fn:exactly-one|fn:escape-uri|fn:escape-html-uri|fn:error|fn:ends-with|fn:encode-for-uri|fn:empty|fn:document-uri|fn:doc-available|fn:doc|fn:distinct-values|fn:distinct-nodes|fn:default-collation|fn:deep-equal|fn:days-from-duration|fn:day-from-dateTime|fn:day-from-date|fn:data|fn:current-time|fn:current-dateTime|fn:current-date|fn:count|fn:contains|fn:concat|fn:compare|fn:collection|fn:codepoints-to-string|fn:codepoint-equal|fn:ceiling|fn:boolean|fn:base-uri|fn:avg|fn:adjust-time-to-timezone|fn:adjust-dateTime-to-timezone|fn:adjust-date-to-timezone|fn:abs|feed:unsubscribe|feed:subscription|feed:subscribe|feed:request|feed:item|feed:description|excel:clean|entity:enrich|dom:set-pipelines|dom:set-permissions|dom:set-name|dom:set-evaluation-context|dom:set-domain-scope|dom:set-description|dom:remove-pipeline|dom:remove-permissions|dom:remove|dom:get|dom:evaluation-context|dom:domains|dom:domain-scope|dom:create|dom:configuration-set-restart-user|dom:configuration-set-permissions|dom:configuration-set-evaluation-context|dom:configuration-set-default-domain|dom:configuration-get|dom:configuration-create|dom:collection|dom:add-pipeline|dom:add-permissions|dls:retention-rules|dls:retention-rule-remove|dls:retention-rule-insert|dls:retention-rule|dls:purge|dls:node-expand|dls:link-references|dls:link-expand|dls:documents-query|dls:document-versions-query|dls:document-version-uri|dls:document-version-query|dls:document-version-delete|dls:document-version-as-of|dls:document-version|dls:document-update|dls:document-unmanage|dls:document-set-quality|dls:document-set-property|dls:document-set-properties|dls:document-set-permissions|dls:document-set-collections|dls:document-retention-rules|dls:document-remove-properties|dls:document-remove-permissions|dls:document-remove-collections|dls:document-purge|dls:document-manage|dls:document-is-managed|dls:document-insert-and-manage|dls:document-include-query|dls:document-history|dls:document-get-permissions|dls:document-extract-part|dls:document-delete|dls:document-checkout-status|dls:document-checkout|dls:document-checkin|dls:document-add-properties|dls:document-add-permissions|dls:document-add-collections|dls:break-checkout|dls:author-query|dls:as-of-query|dbk:convert|dbg:wait|dbg:value|dbg:stopped|dbg:stop|dbg:step|dbg:status|dbg:stack|dbg:out|dbg:next|dbg:line|dbg:invoke|dbg:function|dbg:finish|dbg:expr|dbg:eval|dbg:disconnect|dbg:detach|dbg:continue|dbg:connect|dbg:clear|dbg:breakpoints|dbg:break|dbg:attached|dbg:attach|cvt:save-converted-documents|cvt:part-uri|cvt:destination-uri|cvt:basepath|cvt:basename|cts:words|cts:word-query-weight|cts:word-query-text|cts:word-query-options|cts:word-query|cts:word-match|cts:walk|cts:uris|cts:uri-match|cts:train|cts:tokenize|cts:thresholds|cts:stem|cts:similar-query-weight|cts:similar-query-nodes|cts:similar-query|cts:shortest-distance|cts:search|cts:score|cts:reverse-query-weight|cts:reverse-query-nodes|cts:reverse-query|cts:remainder|cts:registered-query-weight|cts:registered-query-options|cts:registered-query-ids|cts:registered-query|cts:register|cts:query|cts:quality|cts:properties-query-query|cts:properties-query|cts:polygon-vertices|cts:polygon|cts:point-longitude|cts:point-latitude|cts:point|cts:or-query-queries|cts:or-query|cts:not-query-weight|cts:not-query-query|cts:not-query|cts:near-query-weight|cts:near-query-queries|cts:near-query-options|cts:near-query-distance|cts:near-query|cts:highlight|cts:geospatial-co-occurrences|cts:frequency|cts:fitness|cts:field-words|cts:field-word-query-weight|cts:field-word-query-text|cts:field-word-query-options|cts:field-word-query-field-name|cts:field-word-query|cts:field-word-match|cts:entity-highlight|cts:element-words|cts:element-word-query-weight|cts:element-word-query-text|cts:element-word-query-options|cts:element-word-query-element-name|cts:element-word-query|cts:element-word-match|cts:element-values|cts:element-value-ranges|cts:element-value-query-weight|cts:element-value-query-text|cts:element-value-query-options|cts:element-value-query-element-name|cts:element-value-query|cts:element-value-match|cts:element-value-geospatial-co-occurrences|cts:element-value-co-occurrences|cts:element-range-query-weight|cts:element-range-query-value|cts:element-range-query-options|cts:element-range-query-operator|cts:element-range-query-element-name|cts:element-range-query|cts:element-query-query|cts:element-query-element-name|cts:element-query|cts:element-pair-geospatial-values|cts:element-pair-geospatial-value-match|cts:element-pair-geospatial-query-weight|cts:element-pair-geospatial-query-region|cts:element-pair-geospatial-query-options|cts:element-pair-geospatial-query-longitude-name|cts:element-pair-geospatial-query-latitude-name|cts:element-pair-geospatial-query-element-name|cts:element-pair-geospatial-query|cts:element-pair-geospatial-boxes|cts:element-geospatial-values|cts:element-geospatial-value-match|cts:element-geospatial-query-weight|cts:element-geospatial-query-region|cts:element-geospatial-query-options|cts:element-geospatial-query-element-name|cts:element-geospatial-query|cts:element-geospatial-boxes|cts:element-child-geospatial-values|cts:element-child-geospatial-value-match|cts:element-child-geospatial-query-weight|cts:element-child-geospatial-query-region|cts:element-child-geospatial-query-options|cts:element-child-geospatial-query-element-name|cts:element-child-geospatial-query-child-name|cts:element-child-geospatial-query|cts:element-child-geospatial-boxes|cts:element-attribute-words|cts:element-attribute-word-query-weight|cts:element-attribute-word-query-text|cts:element-attribute-word-query-options|cts:element-attribute-word-query-element-name|cts:element-attribute-word-query-attribute-name|cts:element-attribute-word-query|cts:element-attribute-word-match|cts:element-attribute-values|cts:element-attribute-value-ranges|cts:element-attribute-value-query-weight|cts:element-attribute-value-query-text|cts:element-attribute-value-query-options|cts:element-attribute-value-query-element-name|cts:element-attribute-value-query-attribute-name|cts:element-attribute-value-query|cts:element-attribute-value-match|cts:element-attribute-value-geospatial-co-occurrences|cts:element-attribute-value-co-occurrences|cts:element-attribute-range-query-weight|cts:element-attribute-range-query-value|cts:element-attribute-range-query-options|cts:element-attribute-range-query-operator|cts:element-attribute-range-query-element-name|cts:element-attribute-range-query-attribute-name|cts:element-attribute-range-query|cts:element-attribute-pair-geospatial-values|cts:element-attribute-pair-geospatial-value-match|cts:element-attribute-pair-geospatial-query-weight|cts:element-attribute-pair-geospatial-query-region|cts:element-attribute-pair-geospatial-query-options|cts:element-attribute-pair-geospatial-query-longitude-name|cts:element-attribute-pair-geospatial-query-latitude-name|cts:element-attribute-pair-geospatial-query-element-name|cts:element-attribute-pair-geospatial-query|cts:element-attribute-pair-geospatial-boxes|cts:document-query-uris|cts:document-query|cts:distance|cts:directory-query-uris|cts:directory-query-depth|cts:directory-query|cts:destination|cts:deregister|cts:contains|cts:confidence|cts:collections|cts:collection-query-uris|cts:collection-query|cts:collection-match|cts:classify|cts:circle-radius|cts:circle-center|cts:circle|cts:box-west|cts:box-south|cts:box-north|cts:box-east|cts:box|cts:bearing|cts:arc-intersection|cts:and-query-queries|cts:and-query-options|cts:and-query|cts:and-not-query-positive-query|cts:and-not-query-negative-query|cts:and-not-query|css:get|css:convert|cpf:success|cpf:failure|cpf:document-set-state|cpf:document-set-processing-status|cpf:document-set-last-updated|cpf:document-set-error|cpf:document-get-state|cpf:document-get-processing-status|cpf:document-get-last-updated|cpf:document-get-error|cpf:check-transition|alert:spawn-matching-actions|alert:rule-user-id-query|alert:rule-set-user-id|alert:rule-set-query|alert:rule-set-options|alert:rule-set-name|alert:rule-set-description|alert:rule-set-action|alert:rule-remove|alert:rule-name-query|alert:rule-insert|alert:rule-id-query|alert:rule-get-user-id|alert:rule-get-query|alert:rule-get-options|alert:rule-get-name|alert:rule-get-id|alert:rule-get-description|alert:rule-get-action|alert:rule-action-query|alert:remove-triggers|alert:make-rule|alert:make-log-action|alert:make-config|alert:make-action|alert:invoke-matching-actions|alert:get-my-rules|alert:get-all-rules|alert:get-actions|alert:find-matching-rules|alert:create-triggers|alert:config-set-uri|alert:config-set-trigger-ids|alert:config-set-options|alert:config-set-name|alert:config-set-description|alert:config-set-cpf-domain-names|alert:config-set-cpf-domain-ids|alert:config-insert|alert:config-get-uri|alert:config-get-trigger-ids|alert:config-get-options|alert:config-get-name|alert:config-get-id|alert:config-get-description|alert:config-get-cpf-domain-names|alert:config-get-cpf-domain-ids|alert:config-get|alert:config-delete|alert:action-set-options|alert:action-set-name|alert:action-set-module-root|alert:action-set-module-db|alert:action-set-module|alert:action-set-description|alert:action-remove|alert:action-insert|alert:action-get-options|alert:action-get-name|alert:action-get-module-root|alert:action-get-module-db|alert:action-get-module|alert:action-get-description|zero-or-one|years-from-duration|year-from-dateTime|year-from-date|upper-case|unordered|true|translate|trace|tokenize|timezone-from-time|timezone-from-dateTime|timezone-from-date|sum|subtract-dateTimes-yielding-yearMonthDuration|subtract-dateTimes-yielding-dayTimeDuration|substring-before|substring-after|substring|subsequence|string-to-codepoints|string-pad|string-length|string-join|string|static-base-uri|starts-with|seconds-from-time|seconds-from-duration|seconds-from-dateTime|round-half-to-even|round|root|reverse|resolve-uri|resolve-QName|replace|remove|QName|prefix-from-QName|position|one-or-more|number|not|normalize-unicode|normalize-space|node-name|node-kind|nilled|namespace-uri-from-QName|namespace-uri-for-prefix|namespace-uri|name|months-from-duration|month-from-dateTime|month-from-date|minutes-from-time|minutes-from-duration|minutes-from-dateTime|min|max|matches|lower-case|local-name-from-QName|local-name|last|lang|iri-to-uri|insert-before|index-of|in-scope-prefixes|implicit-timezone|idref|id|hours-from-time|hours-from-duration|hours-from-dateTime|floor|false|expanded-QName|exists|exactly-one|escape-uri|escape-html-uri|error|ends-with|encode-for-uri|empty|document-uri|doc-available|doc|distinct-values|distinct-nodes|default-collation|deep-equal|days-from-duration|day-from-dateTime|day-from-date|data|current-time|current-dateTime|current-date|count|contains|concat|compare|collection|codepoints-to-string|codepoint-equal|ceiling|boolean|base-uri|avg|adjust-time-to-timezone|adjust-dateTime-to-timezone|adjust-date-to-timezone|abs)\b/], +["pln",/^[\w:-]+/],["pln",/^[\t\n\r \xa0]+/]]),["xq","xquery"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-yaml.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-yaml.js new file mode 100644 index 0000000..c38729b --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/lang-yaml.js
@@ -0,0 +1,2 @@ +var a=null; +PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:>?|]+/,a,":|>?"],["dec",/^%(?:YAML|TAG)[^\n\r#]+/,a,"%"],["typ",/^&\S+/,a,"&"],["typ",/^!\S*/,a,"!"],["str",/^"(?:[^"\\]|\\.)*(?:"|$)/,a,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,a,"'"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^\s+/,a," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\n\r]|$)/],["pun",/^-/],["kwd",/^\w+:[\n\r ]/],["pln",/^\w+/]]),["yaml","yml"]);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.css new file mode 100644 index 0000000..d44b3a2 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.css
@@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} \ No newline at end of file
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.js b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/prettify/prettify.js
@@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c< +f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&& +(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r= +{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length, +t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b=== +"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value", +m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m= +a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue= +j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m, +250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit", +PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy new file mode 100644 index 0000000..4297d31 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy
@@ -0,0 +1,95 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Common header for Gitiles. + * + * @param title title for this page. Always suffixed with repository name and a + * sitewide title. + * @param? repositoryName repository name for this page, if applicable. + * @param? menuEntries optional list of menu entries with "text" and optional + * "url" keys. + * @param breadcrumbs navigation breadcrumbs for this page. + * @param? css optional list of CSS URLs to include. + * @param? js optional list of Javascript URLs to include. + * @param? onLoad optional Javascript to execute in the body's onLoad handler. + * Warning: not autoescaped. + */ +{template .header} +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <title> + {$title} + {if $repositoryName} + {sp}- {$repositoryName} + {/if} + {sp}- {msg desc="name of the application"}Git at Google{/msg} + </title> + <link rel="stylesheet" type="text/css" href="//www.google.com/css/go.css" /> + + {if $css and length($css)} + {foreach $url in $css} + <link rel="stylesheet" type="text/css" href="{$url}" /> + {/foreach} + {/if} + // Include default CSS after custom CSS so it can override defaults in third- + // party stylesheets (e.g. prettify). + <link rel="stylesheet" type="text/css" href="{gitiles.CSS_URL}" /> + + {if $js and length($js)} + {foreach $url in $js} + <script src="{$url}" type="text/javascript"></script> + {/foreach} + {/if} +</head> +<body {if $onLoad}onload="{$onLoad|id}"{/if}> + {call .customHeader /} + + {if $menuEntries and length($menuEntries)} + <div class="menu"> + {foreach $entry in $menuEntries} + {sp} + {if $entry.url} + <a href="{$entry.url}"{if not isLast($entry)} class="entry"{/if}>{$entry.text}</a> + {else} + <span{if not isLast($entry)} class="entry"{/if}>{$entry.text}</span> + {/if} + {/foreach} + {sp} + </div> + {/if} + + {if $breadcrumbs and length($breadcrumbs)} + <div class="breadcrumbs"> + {foreach $entry in $breadcrumbs} + {if not isFirst($entry)}{sp}/{sp}{/if} + {if not isLast($entry)} + <a href="{$entry.url}">{$entry.text}</a> + {else} + {$entry.text} + {/if} + {/foreach} + </div> + {/if} +{/template} + +/** + * Standard footer. + */ +{template .footer} +</body> +</html> +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DefaultCustomTemplates.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DefaultCustomTemplates.soy new file mode 100644 index 0000000..a1f5d9e --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DefaultCustomTemplates.soy
@@ -0,0 +1,21 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Default custom header implementation for Gitiles. + */ +{template .customHeader} +<h1>{msg desc="short name of the application"}Gitiles{/msg}</h1> +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy new file mode 100644 index 0000000..dfd8af2 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
@@ -0,0 +1,49 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Detail page showing diffs for a single commit. + * + * @param title human-readable revision name. + * @param repositoryName name of this repository. + * @param? menuEntries menu entries. + * @param breadcrumbs breadcrumbs for this page. + * @param? commit optional commit for which diffs are displayed, with keys + * corresponding to the gitiles.commitDetail template (minus "diffTree"). + */ +{template .diffDetail} +{call .header data="all" /} + +{if $commit} + {call .commitDetail data="$commit" /} +{/if} +<div id="DIFF_OUTPUT_BLOCK" /> + +{call .footer /} +{/template} + +/** + * File header for a single unified diff patch. + * + * @param first the first line of the header, with no trailing LF. + * @param rest remaining lines of the header, if any. + * @param fileIndex position of the file within the difference. + */ +{template .diffHeader} +<pre class="diff-header"> +<a name="F{$fileIndex}" class="diff-git">{$first}</a>{\n} +{$rest} +</pre> +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy new file mode 100644 index 0000000..1ee9616 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/HostIndex.soy
@@ -0,0 +1,77 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * HTML page for /. + * + * @param hostName host name. + * @param? menuEntries menu entries. + * @param baseUrl base URL for repositories. + * @param repositories list of repository description maps with name, cloneUrl, + * and optional description values. + */ +{template .hostIndex} +{call .header} + {param title: $hostName ? $hostName + ' Git repositories' : 'Git repositories' /} + {param menuEntries: $menuEntries /} + {param breadcrumbs: null /} +{/call} + +{if length($repositories)} + + <h2> + {msg desc="Git repositories available on the host"} + {$hostName} Git repositories + {/msg} + </h2> + + <div class="instructions"> + {msg desc="description on how to use this repository"} + To clone one of these repositories, install{sp} + <a href="http://www.git-scm.com/">git</a>, and run: + <pre>git clone {$baseUrl}<em>name</em></pre> + {/msg} + </div> + + <table class="list"> + <tr class="no-hover"> + <th width="25%"> + {msg desc="column header for repository name"} + Name + {/msg} + </th> + <th> + {msg desc="column header for repository description"} + Description + {/msg} + </th> + </tr> + {foreach $repo in $repositories} + <tr> + <td> + <a href="{$repo.url}">{$repo.name}</a> + </td> + <td>{$repo.description}</td> + </tr> + {/foreach} + </table> + <div class="footer"> + <a href="?format=TEXT">{msg desc="text format"}TXT{/msg}</a> + {sp} + <a href="?format=JSON">{msg desc="JSON format"}JSON{/msg}</a> + </div> +{/if} +{call .footer /} +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/LogDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/LogDetail.soy new file mode 100644 index 0000000..b9ae9c7 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/LogDetail.soy
@@ -0,0 +1,95 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Detail page showing a shortlog for a commit. + * + * @param title human-readable revision name. + * @param repositoryName name of this repository. + * @param? menuEntries menu entries. + * @param breadcrumbs breadcrumbs for this page. + * @param? tags optional list of tags encountered when peeling this object, with + * keys corresponding to gitiles.tagDetail. + * @param entries list of log entries; see .logEntry. + * @param? nextUrl URL for the next page of results. + * @param? previousUrl URL for the previous page of results. + */ +{template .logDetail} +{call .header data="all" /} + +{if $tags} + {foreach $tag in $tags} + {call gitiles.tagDetail data="$tag" /} + {/foreach} +{/if} + +{if $previousUrl} + <div class="log-nav"> + <a href="{$previousUrl}">{msg desc="text for previous URL"}« Previous{/msg}</a> + </div> +{/if} + +{if length($entries)} + <ol class="shortlog"> + {foreach $entry in $entries} + <li{if $previousUrl and isFirst($entry)} class="first"{/if}> + {call .logEntry data="$entry" /} + </li> + {/foreach} + </ol> +{else} + <p>{msg desc="informational text for when the log is empty"}No commits.{/msg}</p> +{/if} + +{if $nextUrl} + <div class="log-nav"> + <a href="{$nextUrl}">{msg desc="text for next URL"}Next »{/msg}</a> + </div> +{/if} + +{call .footer /} +{/template} + +/** + * Single shortlog entry. + * + * @param abbrevSha abbreviated SHA-1. + * @param url URL to commit detail page. + * @param shortMessage short commit message. + * @param author author information with at least "name" and "relativeTime" keys. + * @param branches list of branches for this entry, with "name" and "url" keys. + * @param tags list of tags for this entry, with "name" and "url" keys. + */ +{template .logEntry} +<a href="{$url}"> + <span class="sha1">{$abbrevSha}</span> + // nbsp instad of CSS padding/margin because those cause a break in the + // underline. + + {sp}<span class="commit-message">{$shortMessage}</span> +</a> +{sp}<span class="author">{msg desc="commit author name"}by {$author.name}{/msg}</span> +{sp}<span class="time">- {$author.relativeTime}</span> +{if length($branches)} + {foreach $branch in $branches} + {sp}<a href="{$branch.url}" class="branch-label">{$branch.name}</a> + {/foreach} +{/if} +{if length($tags)} + {foreach $tag in $tags} + {sp}<a href="{$tag.url}" class="tag-label">{$tag.name}</a> + {/foreach} +{/if} +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy new file mode 100644 index 0000000..badf43c --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -0,0 +1,294 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Detailed listing of a commit. + * + * @param author map with "name", "email", and "time" keys for the commit author. + * @param committer map with "name", "email", and "time" keys for the committer. + * @param sha commit SHA-1. + * @param tree tree SHA-1. + * @param treeUrl tree URL. + * @param parents list of parent objects with the following keys: + * sha: SHA-1. + * url: URL to view the parent commit. + * diffUrl: URL to display diffs relative to this parent. + * @param message list of commit message parts, where each part contains: + * text: raw text of the part. + * url: optional URL that should be linked to from the part. + * @param diffTree list of changed tree entries with the following keys: + * changeType: string matching an org.eclipse.jgit.diff.DiffEntry.ChangeType + * constant. + * path: (new) path of the tree entry. + * oldPath: old path, only for renames and copies. + * url: URL to a detail page for the tree entry. + * diffUrl: URL to a diff page for the tree entry's diff in this commit. + * @param logUrl URL to a log page starting at this commit. + */ +{template .commitDetail} +<div class="git-commit"> + <table> + <tr> + <th>{msg desc="Header for commit SHA entry"}commit{/msg}</th> + <td class="sha"> + {$sha} + <span class="log-link"> + [<a href="{$logUrl}">{msg desc="text for the log link"}log{/msg}</a>] + </span> + </td> + <td>{sp}</td> + </tr> + <tr> + <th>{msg desc="Header for commit author"}author{/msg}</th> + <td>{call .person_ data="$author" /}</td> + <td>{$author.time}</td> + </tr> + <tr> + <th>{msg desc="Header for committer"}committer{/msg}</th> + <td>{call .person_ data="$committer" /}</td> + <td>{$committer.time}</td> + </tr> + <tr> + <th>{msg desc="Header for tree SHA entry"}tree{/msg}</th> + <td class="sha"><a href="{$treeUrl}">{$tree}</a></td> + </tr> + {foreach $parent in $parents} + <tr> + <th>{msg desc="Header for parent SHA"}parent{/msg}</th> + <td> + <a href="{$parent.url}">{$parent.sha}</a> + <span class="diff-link"> + [<a href="{$parent.diffUrl}">{msg desc="text for the parent diff link"}diff{/msg}</a>] + </span> + </td> + </tr> + {/foreach} + </table> +</div> +{call .message_} + {param className: 'commit-message' /} + {param message: $message /} +{/call} + +{if $diffTree and length($diffTree)} + <ul class="diff-tree"> + {foreach $entry in $diffTree} + <li> + <a href="{$entry.url}">{$entry.path}</a> + {switch $entry.changeType} + {case 'ADD'} + <span class="add"> + {msg desc="Text for a new tree entry"} + [Added - <a href="{$entry.diffUrl}">diff</a>] + {/msg} + </span> + {case 'MODIFY'} + <span class="modify"> + {msg desc="Text for a modified tree entry"} + [<a href="{$entry.diffUrl}">diff</a>] + {/msg} + </span> + {case 'DELETE'} + <span class="delete"> + {msg desc="Text for a deleted tree entry"} + [Deleted - <a href="{$entry.diffUrl}">diff</a>] + {/msg} + </span> + {case 'RENAME'} + <span class="rename"> + {msg desc="Text for a renamed tree entry"} + [Renamed from {$entry.oldPath} - <a href="{$entry.diffUrl}">diff</a>] + {/msg} + </span> + {case 'COPY'} + <span class="copy"> + {msg desc="Text for a copied tree entry"} + [Copied from {$entry.oldPath} - <a href="{$entry.diffUrl}">diff</a>] + {/msg} + </span> + {default} + {/switch} + </li> + {/foreach} + </ul> + <div class="diff-summary"> + {if length($diffTree) == 1} + {msg desc="1 file changed"}1 file changed{/msg} + {else} + {msg desc="number of files changed"}{length($diffTree)} files changed{/msg} + {/if} + </div> +{/if} + +{/template} + +/** + * Detailed listing of a tree. + * + * @param sha SHA of this path's tree. + * @param? logUrl optional URL to a log for this path. + * @param entries list of entries with the following keys: + * type: entry type, matching one of the constant names defined in + * org.eclipse.jgit.lib.FileMode. + * name: tree entry name. + * url: URL to link to. + * targetName: name of a symlink target, required only if type == 'SYMLINK'. + * targetUrl: optional url of a symlink target, required only if + * type == 'SYMLINK'. + */ +{template .treeDetail} +<div class="sha1"> + {msg desc="SHA-1 for the path's tree"}tree: {$sha}{/msg} + {if $logUrl}{sp}[<a href="{$logUrl}">{msg desc="history for a path"}path history{/msg}</a>]{/if} +</div> + +{if length($entries)} + <ol class="list files"> + {foreach $entry in $entries} + <li class=" + {switch $entry.type} + {case 'TREE'}git-tree + {case 'SYMLINK'}symlink + {case 'REGULAR_FILE'}regular-file + {case 'EXECUTABLE_FILE'}executable-file + {case 'GITLINK'}gitlink + {default}regular-file + {/switch} + " title=" + {switch $entry.type} + {case 'TREE'}{msg desc="Alt text for tree icon"}Tree{/msg} + {case 'SYMLINK'}{msg desc="Alt text for symlink icon"}Symlink{/msg} + {case 'REGULAR_FILE'}{msg desc="Alt text for regular file icon"}Regular file{/msg} + {case 'EXECUTABLE_FILE'} + {msg desc="Alt text for executable file icon"}Executable file{/msg} + {case 'GITLINK'} + {msg desc="Alt text for git submodule link icon"}Git submodule link{/msg} + {default}{msg desc="Alt text for other file icon"}Other{/msg} + {/switch} + - {$entry.name}"> + <a href="{$entry.url}">{$entry.name}</a> + {if $entry.type == 'SYMLINK'} + {sp}⇨{sp} + {if $entry.targetUrl} + <a href="{$entry.targetUrl}">{$entry.targetName}</a> + {else} + {$entry.targetName} + {/if} + {/if} + // TODO(dborowitz): Something reasonable for gitlinks. + </li> + {/foreach} + </table> +{else} + <p>{msg desc="Informational text for when a tree is empty"}This tree is empty.{/msg}</p> +{/if} +{/template} + +/** + * Detailed listing of a blob. + * + * @param sha SHA of this file's blob. + * @param? logUrl optional URL to a log for this file. + * @param data file data (may be empty), or null for a binary file. + * @param? lang prettyprint language extension for text file. + * @param? size for binary files only, size in bytes. + */ +{template .blobDetail} +<div class="sha1"> + {msg desc="SHA-1 for the file's blob"}blob: {$sha}{/msg} + {if $logUrl}{sp}[<a href="{$logUrl}">{msg desc="history for a file"}file history{/msg}</a>]{/if} +</div> + +{if $data != null} + {if $data} + {if $lang != null} + <pre class="git-blob prettyprint linenums lang-{$lang}">{$data}</pre> + {else} + <pre class="git-blob">{$data}</pre> + {/if} + {else} + <div class="file-empty">Empty file</div> + {/if} +{else} + <div class="file-binary"> + {msg desc="size of binary file in bytes"}{$size}-byte binary file{/msg} + </div> +{/if} +{/template} + +/** + * Detailed listing of an annotated tag. + * + * @param sha SHA of this tag. + * @param? tagger optional map with "name", "email", and "time" keys for the + * tagger. + * @param object SHA of the object this tag points to. + * @param message tag message. + */ +{template .tagDetail} +<div class="git-tag"> + <table> + <tr> + <th>{msg desc="Header for tag SHA entry"}tag{/msg}</th> + <td class="sha">{$sha}</td> + <td>{sp}</td> + </tr> + {if $tagger} + <tr> + <th>{msg desc="Header for tagger"}tagger{/msg}</th> + <td>{call .person_ data="$tagger" /}</td> + <td>{$tagger.time}</td> + </tr> + {/if} + <tr> + <th>{msg desc="Header for tagged object SHA"}object{/msg}</th> + <td class="sha">{$object}</td> + <td>{sp}</td> + </tr> + </table> +</div> +{if $message and length($message)} + {call .message_} + {param className: 'tag-message' /} + {param message: $message /} + {/call} +{/if} +{/template} + +/** + * Line about a git person identity. + * + * @param name name. + * @param email email. + */ +{template .person_ private="true"} +{$name}{if $email} <{$email}>{/if} +{/template} + +/** + * Preformatted message, possibly containing hyperlinks. + * + * @param className CSS class name for <pre> block. + * @param message list of message parts, where each part contains: + * text: raw text of the part. + * url: optional URL that should be linked to from the part. + */ +{template .message_ private="true"} +<pre class="{$className|id}"> + {foreach $part in $message} + {if $part.url}<a href="{$part.url}">{$part.text}</a>{else}{$part.text}{/if} + {/foreach} +</pre> +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/PathDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/PathDetail.soy new file mode 100644 index 0000000..33d38a2 --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/PathDetail.soy
@@ -0,0 +1,84 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Detail page for a path within a tree. + * + * @param title human-readable name of this path. + * @param repositoryName name of this repository. + * @param? menuEntries menu entries. + * @param breadcrumbs breadcrumbs for this page. + * @param type path type, matching one of the constant names defined in + * org.eclipse.jgit.lib.FileMode. + * @param data path data, matching the params for one of .treeDetail, + * .blobDetail, .symlinkDetail, or .gitlinkDetail as appropriate. + */ +{template .pathDetail} +{if $type == 'REGULAR_FILE' or $type == 'EXECUTABLE_FILE'} + {call .header} + {param title: $title /} + {param repositoryName: $repositoryName /} + {param menuEntries: $menuEntries /} + {param breadcrumbs: $breadcrumbs /} + {param css: [gitiles.PRETTIFY_CSS_URL] /} + {param js: [gitiles.PRETTIFY_JS_URL] /} + {param onLoad: 'prettyPrint()' /} + {/call} +{else} + {call .header data="all" /} +{/if} + +{switch $type} + {case 'TREE'}{call .treeDetail data="$data" /} + {case 'SYMLINK'}{call .symlinkDetail data="$data" /} + {case 'REGULAR_FILE'}{call .blobDetail data="$data" /} + {case 'EXECUTABLE_FILE'}{call .blobDetail data="$data" /} + {case 'GITLINK'}{call .gitlinkDetail data="$data" /} + {default} + <div class="error"> + {msg desc="Error message for an unknown object type"}Object has unknown type.{/msg} + </div> +{/switch} + +{call .footer /} +{/template} + +/** + * Detail for a symbolic link. + * + * @param target target of this symlink. + * @param? targetUrl optional URL for the target, if it is within this repo. + */ +{template .symlinkDetail} +<div class="symlink-detail"> + {msg desc="Lead-in text for the symbolic link target."}Symbolic link to{/msg} + {sp}{if $targetUrl}<a href="{$targetUrl}">{$target}</a>{else}{$target}{/if} +</div> +{/template} + +/** + * Detail for a git submodule link. + * + * @param sha submodule commit SHA. + * @param remoteUrl URL of the remote repository. + * @param? httpUrl optional HTTP URL pointing to a web-browser-compatible URL of + * the remote repository. + */ +{template .gitlinkDetail} +<div class="gitlink-detail"> + {msg desc="Lead-in text for the git link URL"}Submodule link to {$sha} of{/msg} + {sp}{if $httpUrl}<a href="{$httpUrl}">{$remoteUrl}</a>{else}{$remoteUrl}{/if} +</div> +{/template}
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 new file mode 100644 index 0000000..02bb4da --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
@@ -0,0 +1,79 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Index page for a repository. + * + * @param repositoryName name of this repository. + * @param? menuEntries menu entries. + * @param breadcrumbs breadcrumbs for this page. + * @param cloneUrl clone URL for this repository. + * @param description description text of the repository. + * @param? branches list of branch objects with url and name keys. + * @param? tags list of tag objects with url and name keys. + */ +{template .repositoryIndex} +{call .header} + {param title: $repositoryName /} + {param repositoryName: null /} + {param menuEntries: $menuEntries /} + {param breadcrumbs: $breadcrumbs /} +{/call} + +{if $description} + <div class="repository-description">{$description}</div> +{/if} + +<textarea rows="1" cols="150" class="clone-line" + onclick="this.focus();this.select();" + readonly="readonly"> + git clone {$cloneUrl} +</textarea> + +<div class="repository-refs"> + {if $branches and length($branches)} + <div class="repository-branches"> + <h3>Branches</h3> + <ul class="branch-list"> + {foreach $branch in $branches} + {call .ref_ data="$branch" /} + {/foreach} + </ul> + </div> + {/if} + + {if $tags and length($tags)} + <div class="repository-tags"> + <h3>Tags</h3> + <ul class="branch-list"> + {foreach $tag in $tags} + {call .ref_ data="$tag" /} + {/foreach} + </ul> + </div> + {/if} +</div> +{call .footer /} +{/template} + +/** + * Detail for a single ref. + * + * @param url URL for ref detail page. + * @param name ref name. + */ +{template .ref_} +<li><a href="{$url}">{$name}</a></li> +{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RevisionDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RevisionDetail.soy new file mode 100644 index 0000000..1abed6b --- /dev/null +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RevisionDetail.soy
@@ -0,0 +1,63 @@ +// 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. +{namespace gitiles autoescape="contextual"} + +/** + * Detail page about a single revision. + * + * @param title human-readable revision name. + * @param repositoryName name of this repository. + * @param? menuEntries menu entries. + * @param breadcrumbs breadcrumbs for this page. + * @param? hasBlob set to true if the revision or its peeled value is a blob. + * @param objects list of objects encountered when peeling this object. Each + * object has a "type" key with one of the + * org.eclipse.jgit.lib.Contants.TYPE_* constant strings, and a "data" key + * with an object whose keys correspond to the appropriate object detail + * template from ObjectDetail.soy. + */ +{template .revisionDetail} +{if $hasBlob} + {call .header} + {param title: $title /} + {param repositoryName: $repositoryName /} + {param menuEntries: $menuEntries /} + {param breadcrumbs: $breadcrumbs /} + {param css: [gitiles.PRETTIFY_CSS_URL] /} + {param js: [gitiles.PRETTIFY_JS_URL] /} + {param onLoad: 'prettyPrint()' /} + {/call} +{else} + {call .header data="all" /} +{/if} + +{foreach $object in $objects} + {switch $object.type} + {case 'commit'} + {call .commitDetail data="$object.data" /} + {case 'tree'} + {call .treeDetail data="$object.data" /} + {case 'blob'} + {call .blobDetail data="$object.data" /} + {case 'tag'} + {call .tagDetail data="$object.data" /} + {default} + <div class="error"> + {msg desc="Error message for an unknown object type"}Object has unknown type.{/msg} + </div> + {/switch} +{/foreach} + +{call .footer /} +{/template}