Factor out the shortlog generation code and templates

Change-Id: Ibf98fce0669fb47c142c421fa9a371ea02b6cbf0
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
index 52b4ea8..748c5c9 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -14,18 +14,19 @@
 
 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 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;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -45,34 +46,26 @@
 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;
+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.gitiles.CommitSoyData.KeySet;
 
 /** Serves an HTML page with a shortlog for commits and paths. */
 public class LogServlet extends BaseServlet {
   private static final long serialVersionUID = 1L;
   private static final Logger log = LoggerFactory.getLogger(LogServlet.class);
 
-  private static final String START_PARAM = "s";
+  static final String START_PARAM = "s";
+  private static final int LIMIT = 100;
 
   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
@@ -94,7 +87,8 @@
         return;
       }
 
-      Map<String, Object> data = Maps.newHashMapWithExpectedSize(5);
+      Map<String, Object> data = new LogSoyData(req, repo, view)
+        .toSoyData(walk, LIMIT, null, start.orNull());
 
       if (!view.getRevision().nameIsId()) {
         List<Map<String, Object>> tags = Lists.newArrayListWithExpectedSize(1);
@@ -108,9 +102,9 @@
         }
       }
 
-      Paginator paginator = new Paginator(walk, limit, start.orNull());
+      Paginator paginator = new Paginator(walk, LIMIT, start.orNull());
       Map<AnyObjectId, Set<Ref>> refsById = repo.getAllRefsByPeeledObjectId();
-      List<Map<String, Object>> entries = Lists.newArrayListWithCapacity(limit);
+      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));
@@ -124,21 +118,6 @@
       }
 
       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) {
@@ -152,16 +131,6 @@
     }
   }
 
-  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);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogSoyData.java
new file mode 100644
index 0000000..dbeb2e9
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogSoyData.java
@@ -0,0 +1,90 @@
+// 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 java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+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 com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gitiles.CommitSoyData.KeySet;
+
+public class LogSoyData {
+  private final HttpServletRequest req;
+  private final Repository repo;
+  private final GitilesView view;
+
+  public LogSoyData(HttpServletRequest req, Repository repo, GitilesView view) {
+    this.req = req;
+    this.repo = repo;
+    this.view = view;
+  }
+
+  public Map<String, Object> toSoyData(RevWalk walk, int limit, @Nullable String revision,
+      @Nullable ObjectId start) throws IOException {
+    Map<String, Object> data = Maps.newHashMapWithExpectedSize(3);
+
+    Paginator paginator = new Paginator(walk, limit, start);
+    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));
+    }
+
+    data.put("entries", entries);
+    ObjectId next = paginator.getNextStart();
+    if (next != null) {
+      data.put("nextUrl", copyAndCanonicalize(view, revision)
+          .replaceParam(LogServlet.START_PARAM, next.name())
+          .toUrl());
+    }
+    ObjectId prev = paginator.getPreviousStart();
+    if (prev != null) {
+      GitilesView.Builder prevView = copyAndCanonicalize(view, revision);
+      if (!prevView.getRevision().getId().equals(prev)) {
+        prevView.replaceParam(LogServlet.START_PARAM, prev.name());
+      }
+      data.put("previousUrl", prevView.toUrl());
+    }
+    return data;
+  }
+
+  private static GitilesView.Builder copyAndCanonicalize(GitilesView view, String revision) {
+    // Canonicalize the view by using full SHAs.
+    GitilesView.Builder copy = GitilesView.log().copyFrom(view);
+    if (view.getRevision() != Revision.NULL) {
+      copy.setRevision(view.getRevision());
+    } else {
+      copy.setRevision(Revision.named(revision));
+    }
+    if (view.getOldRevision() != Revision.NULL) {
+      copy.setOldRevision(view.getOldRevision());
+    }
+    return copy;
+  }
+}
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
index b9ae9c7..fd08196 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/LogDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/LogDetail.soy
@@ -35,6 +35,19 @@
   {/foreach}
 {/if}
 
+{call .logEntries data="all" /}
+
+{call .footer /}
+{/template}
+
+/**
+ * List of log entries.
+ *
+ * @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 .logEntries}
 {if $previousUrl}
   <div class="log-nav">
     <a href="{$previousUrl}">{msg desc="text for previous URL"}&laquo; Previous{/msg}</a>
@@ -58,10 +71,9 @@
     <a href="{$nextUrl}">{msg desc="text for next URL"}Next &raquo;{/msg}</a>
   </div>
 {/if}
-
-{call .footer /}
 {/template}
 
+
 /**
  * Single shortlog entry.
  *