Display logs in a streaming fashion

JGit even on a fast workstation can only walk a few thousand commits
per second with rename detection on (and in a loaded server
environment it might be much slower). Loading a full page of 100 log
results for a file therefore might take many seconds.

Stream the output one log entry at a time so the page becomes
interactive slightly faster. Each HTTP chunk is a full <li></li> tag,
so browsers should be able to render incrementally.

This is much simpler than an alternative solution involving AJAX to
make multiple requests to the server, particularly in a multi-server
cluster environment where the client is not guaranteed to talk to the
same server (with the necessary RevWalk state in memory) on
consecutive requests.

Change-Id: I63c4bc655efd00453b6db60f333ba3dd5041e70a
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
index 25c0754..e2adfc2 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
@@ -316,6 +316,12 @@
   color: #009933;
 }
 
+ol.log > li.empty:hover, ol.log > li.empty {
+  background: inherit;
+  padding: 0px;
+  border: 0px;
+}
+
 
 /* Styles for the diff detail 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
index 71ac795..6c4de74 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
@@ -23,10 +23,6 @@
  * @param breadcrumbs breadcrumbs for this page.
  * @param? tags optional list of tags encountered when peeling this object, with
  *     keys corresponding to gitiles.tagDetail.
- * @param? logEntryVariant variant name for log entry template.
- * @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" /}
@@ -37,40 +33,52 @@
   {/foreach}
 {/if}
 
-{call .logEntries data="all" /}
+{call .streamingPlaceholder /}
 
 {call .footer /}
 {/template}
 
+
 /**
- * List of log entries.
+ * Header for list of log entries.
  *
- * @param? logEntryVariant variant name for log entry template.
- * @param? logEntryPretty base "pretty" format for the log entry template.
- * @param entries list of log entries; see .logEntry.
- * @param? nextUrl URL for the next page of results.
+ * @param? pretty base "pretty" format for the log entry template.
  * @param? previousUrl URL for the previous page of results.
  */
-{template .logEntries}
+{template .logEntriesHeader}
 {if $previousUrl}
   <div class="log-nav">
     <a href="{$previousUrl}">{msg desc="text for previous URL"}&laquo; Previous{/msg}</a>
   </div>
 {/if}
 
-{if length($entries)}
-  <ol class="{$logEntryPretty ?: 'default'} log">
-    {foreach $entry in $entries}
-      <li{if $previousUrl and isFirst($entry)} class="first"{/if}>
-        {delcall gitiles.logEntry variant="$logEntryVariant ?: 'default'"
-            data="$entry" /}
-      </li>
-    {/foreach}
-  </ol>
-{else}
-  <p>{msg desc="informational text for when the log is empty"}No commits.{/msg}</p>
-{/if}
+<ol class="{$pretty ?: 'default'} log">
+{/template}
 
+
+/**
+ * Wrapper for a single log entry with pretty format and variant.
+ *
+ * @param firstWithPrevious whether this entry is the first in the current list,
+ *     but also comes below a "Previous" link.
+ * @param variant variant name for log entry template.
+ * @param entry log entry; see .logEntry.
+ */
+{template .logEntryWrapper}
+// TODO(dborowitz): Better CSS instead of this firstWithPrevious hack.
+<li{if $firstWithPrevious} class="first"{/if}>
+  {delcall gitiles.logEntry variant="$variant ?: 'default'" data="$entry" /}
+</li>
+{/template}
+
+
+/**
+ * Footer for the list of log entries.
+ *
+ * @param? nextUrl URL for the next page of results.
+ */
+{template .logEntriesFooter}
+</ol>
 {if $nextUrl}
   <div class="log-nav">
     <a href="{$nextUrl}">{msg desc="text for next URL"}Next &raquo;{/msg}</a>
@@ -80,6 +88,14 @@
 
 
 /**
+ * Single log entry indicating the full log is empty.
+ */
+{template .emptyLog}
+<li class="empty">{msg desc="informational text for when the log is empty"}No commits.{/msg}</p>
+{/template}
+
+
+/**
  * Single pretty log entry, similar to --pretty=oneline.
  *
  * @param abbrevSha abbreviated SHA-1.
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
index 10414a5..aaba64b 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
@@ -27,10 +27,7 @@
  * @param? moreBranchesUrl URL to show more branches, if necessary.
  * @param tags list of tag objects with url and name keys.
  * @param? moreTagsUrl URL to show more branches, if necessary.
- * @param? nextUrl URL for the next page of log results.
- * @param? previousUrl URL for the previous page of log results.
- * @param? logEntryVariant variant name for log entry template.
- * @param entries list of log entries; see .logEntry.
+ * @param hasLog whether a log should be shown for HEAD.
  */
 {template .repositoryIndex}
 {call .header}
@@ -60,10 +57,10 @@
     git clone {$cloneUrl}
 </textarea>
 
-{if length($entries) and (length($branches) or length($tags))}
+{if $hasLog and (length($branches) or length($tags))}
   <div class="repository-shortlog-wrapper">
     <div class="repository-shortlog">
-      {call .logEntries data="all" /}
+      {call .streamingPlaceholder /}
     </div>
   </div>
 
@@ -71,8 +68,8 @@
     {call .branches_ data="all" /}
     {call .tags_ data="all" /}
   </div>
-{elseif length($entries)}
-  {call .logEntries data="all" /}
+{elseif $hasLog}
+  {call .streamingPlaceholder /}
 {elseif length($branches) or length($tags)}
   {call .branches_ data="all" /}
   {call .tags_ data="all" /}