diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 What is gipeda?
 ---------------
 
-Gitpeda is a a tool that presents data from your program’s benchmark suite (or
+Gipeda is a a tool that presents data from your program’s benchmark suite (or
 any other source), with nice tables and shiny graphs.
 
 
@@ -22,17 +22,21 @@
 
 Do you want to see it live? Check out these:
 
- * [Demo page], visualizing fairly boring stuff about gipedia itself.
+ * [Demo page], visualizing fairly boring stuff about gipeda itself.
  * [GHC’s gipeda installation].
 
-[Demo page]: http://nomeata.github.io/gipeda/
-[GHC’s gipeda installation]: https://perf.haskell.org/
+[Demo page]: http://perf.haskell.org/gipeda
+[GHC’s gipeda installation]: https://perf.haskell.org/ghc
 
 Setting it up
 -------------
 
- * Clone gipedia somewhere, possibly directly into your webspace.
+ * Clone gipeda somewhere, possibly directly into your webspace.
  * Install a Haskell compiler, including the `cablal` tool.
+ * Install a few packages
+
+        apt-get install git unzip libfile-slurp-perl libipc-run-perl
+
  * Install the dependencies:
 
         cabal install --only-dependencies
@@ -83,7 +87,7 @@
 pull` or, if it is a bare clone, `git -C repository fetch origin
 "+refs/heads/*:refs/heads/*" --prune`.
 
-Using gipedia
+Using gipeda
 -------------
 
 Finally, you simply point your browser to the `site/index.html`. The page
@@ -92,6 +96,29 @@
 the `=`.
 
 To host this on a webserver, just put the `site/` directory in your webspace.
+
+Hacking on gipeda
+-----------------
+
+Gipeda doesn't do much; it mostly assembles the data and creates nice reports.
+The rough pipeline is as follows:
+
+ * Directory `logs/` contains project-specific data per git commit that has
+   been benchmarked. gipeda will run `log2csv` on these files to generate the
+   files in `site/out/results`. `logs` may be a normal directory, or (for disk
+   space efficiency) a bare git repository. This step is optional.
+ * Directory `site/out/results` contains one csv file per git commit. The
+   format is simple, as there are two columns: benchmark name and a numerical
+   value.
+ * From these files, gipeda generates a number of JSON files, some per commit
+   (`report`, `summaries`), some global (`settings`, `latest-summaries`).
+
+   A crucial idea here is that these JSON files are all but fragments of a
+   theoretical global JSON document. In other words: You could combine them
+   (using a naive JSON object merge) and there would be no conflicts, and the
+   result could be used by the client as well.
+ * The client (`site/index.html` and `site/js/gipeda.js`) is a fairly standard
+   HTML+JS application using jquery, bootstrap, handlebars.
 
 Bugs, Code, Contact
 -------------------
diff --git a/gipeda.cabal b/gipeda.cabal
--- a/gipeda.cabal
+++ b/gipeda.cabal
@@ -1,9 +1,9 @@
 name:                gipeda
-version:             0.1.0.2
+version:             0.1.1
 category:            Development
 synopsis:            Git Performance Dashboard
 description:
- Gitpeda is a a tool that presents data from your program’s benchmark suite
+ Gipeda is a a tool that presents data from your program’s benchmark suite
  (or any other source), with nice tables and shiny graphs.
  .
  So it is up to you whether you have a polling shell script loop, a post-commit
@@ -46,7 +46,6 @@
     BenchmarksInCSV,
     BenchNames,
     GraphReport,
-    IndexReport,
     JsonSettings,
     JsonUtils,
     ParentMap,
@@ -56,8 +55,12 @@
     RevReport,
     Shake,
     Summary,
-    WithLatestLogs
+    GraphSummaries,
+    WithLatestLogs,
+    Development.Shake.Gitlib,
+    Data.Text.Binary
 
+
   build-depends:
       base                 >= 4.6  && <4.9,
       bytestring           >= 0.10 && <0.11,
@@ -71,12 +74,20 @@
       vector               >= 0.10 && <0.11,
       cassava              >= 0.4  && <0.5,
       yaml                 >= 0.8  && <0.9,
-      aeson                >= 0.7  && <0.9
+      aeson                >= 0.7  && <0.10,
+      scientific           >= 0.3  && <0.4,
+      gitlib               >= 3.1  && <3.2,
+      gitlib-libgit2,
+      tagged               >= 0.7  && <0.9,
+      extra                >= 1    && <1.2,
 
+      -- https://github.com/jwiegley/gitlib/issues/46
+      conduit-combinators  < 1
+
   hs-source-dirs: src
 
   default-language: Haskell2010
 
 source-repository head
   type:     git
-  location: https://github.com/nomeat/gipeda
+  location: https://github.com/nomeata/gipeda
diff --git a/install-jslibs.sh b/install-jslibs.sh
--- a/install-jslibs.sh
+++ b/install-jslibs.sh
@@ -12,6 +12,12 @@
 	wget -c http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v2.0.0.js
 test -e jquery-1.11.2.min.js ||
 	wget -c http://code.jquery.com/jquery-1.11.2.min.js
+test -d jquery-ui || {
+        wget -c http://jqueryui.com/resources/download/jquery-ui-1.11.4.zip
+	unzip jquery-ui-1.11.4.zip
+	mv jquery-ui-1.11.4 jquery-ui
+	rm -f jquery-ui-1.11.4.zip
+	}
 test -e jquery.timeago.js ||
 	wget -c http://timeago.yarp.com/jquery.timeago.js
 test -d flot || {
@@ -23,7 +29,9 @@
 	mkdir bootstrap
 	cd bootstrap
 	wget -c https://github.com/twbs/bootstrap/releases/download/v3.3.1/bootstrap-3.3.1-dist.zip
-	unzip bootstrap-3.3.1-dist.zip 
-	rm -f bootstrap-3.3.1-dist.zip 
+	unzip bootstrap-3.3.1-dist.zip
+	rm -f bootstrap-3.3.1-dist.zip
 	cd ..
 	}
+test -e naturalSort.j ||
+	wget -c https://raw.githubusercontent.com/overset/javascript-natural-sort/master/naturalSort.js
diff --git a/site/index.html b/site/index.html
--- a/site/index.html
+++ b/site/index.html
@@ -11,6 +11,9 @@
 <script type="text/javascript" src="js/handlebars-v2.0.0.js"></script>
 <script src="js/jquery-1.11.2.min.js"></script>
 <script src="js/jquery.timeago.js"></script>
+<script src="js/jquery-ui/jquery-ui.min.js"></script>
+<script src="js/naturalSort.js"></script>
+<link rel="stylesheet" href="js/jquery-ui/jquery-ui.min.css">
 
 <script src="js/flot/jquery.flot.min.js"></script>
 <script src="js/flot/jquery.flot.resize.min.js"></script>
@@ -57,7 +60,7 @@
     border-collapse:collapse; 
     
 }
-.summary-row-collapsed + .summary-row td {
+.summary-table .summary-row-collapsed + .summary-row td {
     border-top: 4px dotted #DDD;
 
 }
@@ -110,18 +113,24 @@
     <div class="navbar-header pull-right">
       <p class="navbar-text nav-loading pull-left">Loading data...</p>
 
+      <div class="navbar-text nav-compare" role="group">
+       <a id="go-to-compare" href="#" class="navbar-link">
+        <code>
+         <span title="Drop a revision id to compare two revisions" id="compare-from">???????</span>..<span title="Drop a revision id to compare two revisions" id="compare-to">???????</span>
+         </code>
+       </a>
+      </div>
+
       <ul class="nav pull-left">
        <li class="pull-left">
 	<div class="btn-group" role="group">
 	 <button type="button" class="btn btn-default navbar-btn active benchSelector" id="show-improvements" title="Show improvements">
-	  <!-- <span class="glyphicon glyphicon-plus text-success"></span> -->
 	  <span class="text-success" style="font-weight:bold">+</span>
 	 </button>
 	 <button type="button" class="btn btn-default navbar-btn benchSelector" id="show-boring" title="Show unchanged">
 	  <span style="font-weight:bold">=</span>
 	 </button>
 	 <button type="button" class="btn btn-default navbar-btn active benchSelector" id="show-regressions" title="Show regressions">
-	  <!-- <span class="glyphicon glyphicon-minus text-warning"></span> -->
 	  <span class="text-warning" style="font-weight:bold">-</span>
 	 </button>
 	</div>
@@ -147,19 +156,26 @@
  </nav>
 </script>
 
+<script id="rev-id" type="text/x-handlebars-template">
+  <code data-rev="{{hash}}" class="rev-draggable">{{shortRev hash}}</code>
+</script>
+
+
 <script id="summary-icons"  type="text/x-handlebars-template">
-  <span  title="number of benchmarks">
+  <span title="{{id summaryDesc}}">
+  <span>
    {{totalCount}}
    <span class="glyphicon glyphicon-stats"></span>
   </span>
-  <span title="number of improvements"> 
+  <span>
    {{improvementCount}}
    <span class="glyphicon glyphicon-plus text-success"></span>
   </span>
-  <span title="number of regressions">
+  <span>
    {{regressionCount}}
    <span class="glyphicon glyphicon-minus text-warning"></span>
   </span>
+  </span>
 </script>
 
 <script id="summary-list"  type="text/x-handlebars-template">
@@ -178,7 +194,7 @@
      </td>
      <td class="col-md-1">
       <a href="{{revisionLink hash}}">
-        <code>{{shortRev hash}}</code>
+        {{> rev-id hash=hash}}
       </a>
      </td>
      <td class="col-md-7">
@@ -193,8 +209,53 @@
   </table>
 </script>
 
+<script id="tags"  type="text/x-handlebars-template">
+  <h2>Tags</h2>
+  <table class="table tag-table">
+   {{#each_unnaturally tags}}
+     {{#with (lookup ../revisions this)}}
+      {{#with this.summary}}
+      <tr class="tag-row">
+       <td class="col-md-2 text-right">
+	 <abbrv class="timeago" title="{{ iso8601 this.gitDate }}">{{ humanDate this.gitDate}}</abbrv>
+       </td>
+       <td class="col-md-1">
+	<a href="{{revisionLink hash}}">
+          {{> rev-id hash=hash}}
+	</a>
+       </td>
+       <td class="col-md-2">
+	 <strong>{{ @key }}</strong>
+       </td>
+       <td class="col-md-7">
+	 {{ gitSubject }}
+       </td>
+      </tr>
+      {{/with}}
+     {{else}}
+      <tr
+       title="This tag has not been benchmarked yet"
+       class="tag-row">
+       <td class="col-md-2 text-right">
+       </td>
+       <td class="col-md-1">
+          {{> rev-id hash=this}}
+       </td>
+       <td class="col-md-2">
+	 {{ @key }}
+       </td>
+       <td class="col-md-7 text-right">
+       </td>
+      </tr>
+     {{/with}}
+   {{/each_unnaturally}}
+  </table>
+</script>
+
 <script id="revTooltip" type="text/x-handlebars-template">
- <a href="{{revisionLink hash}}"><code>{{shortRev hash}}</code></a>:
+ <a href="{{revisionLink hash}}">
+  {{> rev-id hash=hash}}
+ </a>:
  {{ value }}<br/>
  {{ humanDate gitDate}}<br/>
  {{ gitSubject }}</br>
@@ -207,9 +268,10 @@
   <h1>Benchmarks</h1>
  </div>
  <div class="container">
+    {{> nothing }}
     <div class="panel-group" role="tablist">
      {{#each benchGroups}}
-      <div class="panel panel-default">
+      <div class="panel panel-default graph-list-panel">
        <div class="panel-heading" role="tab" id="heading-{{@index}}">
         <h4 class="panel-title">
          <a class="accordion-toggle" data-toggle="collapse" href="#table-{{@index}}">
@@ -228,13 +290,32 @@
           </thead>
           <tbody>
            {{#each groupMembers}}
-            <tr>
-            <td class="benchmark-name">
-	     {{this}}
-	     <a class="graph-link" title="Graphs" href="{{graphLink this}}">
-	      <span class="glyphicon glyphicon-signal"/>
-	     </a>
-	    </td>
+            <tr class="
+              summary-row
+              {{#with (lookup ../../graphSummaries this)}}
+               {{#if improvements}}summary-improvement{{/if}}
+               {{#if regressions}}summary-regression{{/if}}
+              {{/with}}
+              "
+            >
+             <td class="benchmark-name">
+              {{this}}
+              <a class="graph-link" title="Graphs" href="{{graphLink this}}">
+               <span class="glyphicon glyphicon-signal"/>
+              </a>
+             </td>
+             <td class="col-md-2 text-right">
+              {{#with (lookup ../../graphSummaries this)}}
+               <span>
+                {{improvements}}
+                <span class="glyphicon glyphicon-plus text-success"></span>
+               </span>
+               <span>
+                {{regressions}}
+                <span class="glyphicon glyphicon-minus text-warning"></span>
+               </span>
+              {{/with}}
+             </td>
             </tr>
            {{/each}}
           </tbody>
@@ -252,6 +333,7 @@
  <div class="container">
   <h1>Recent commits</h1>
   {{> summary-list (recentCommits revisions)}}
+  {{> tags }}
  </div>
  <div class="container">
   <p class="text-center">
@@ -265,6 +347,7 @@
  <div class="container">
   <h1>All commits</h1>
   {{> summary-list (allCommits revisions)}}
+  {{> tags }}
  </div>
 </script>
 
@@ -281,12 +364,16 @@
     </h2>
     <p>
     {{#if rev.summary.parents}}
-    Displaying changes since:  
+    Displaying changes since:
     {{#with (lookup rev.summary.parents 0)}}
-      <a href="{{revisionLink this}}"><code>{{shortRev this}}</code></a> – 
+      <a href="{{revisionLink this}}">
+	{{> rev-id hash=this}}
+      </a> –
       <a href="{{diffLink this ../rev.summary.hash}}">View diff</a>
     {{/with}} –
-      <a href="{{logLink rev.summary.hash}}">View buildlog</a> –
+    {{#logLink rev.summary.hash }}
+      <a href="{{link}}">View buildlog</a> –
+    {{/logLink}}
       {{> summary-icons rev.summary.stats }}
     {{else}}
     No parent commit found.
@@ -319,10 +406,11 @@
          <table class="table table-condensed benchmark-table">
           <thead>
           <tr>
-          <th class="col-md-6">Benchmark name</th>
+          <th class="col-md-5">Benchmark name</th>
           <th class="col-md-2 text-right">previous</th>
-          <th class="col-md-2 text-right"></th>
-          <th class="col-md-2 text-right">value</th>
+          <th class="col-md-2 text-right">change</th>
+          <th class="col-md-2 text-right">now</th>
+          <th class="col-md-1 text-left"></th>
           </tr>
           </thead>
           <tbody>
@@ -337,6 +425,7 @@
             <td class="text-right">{{previous}}</td>
             <td class="text-right">{{change}}</td>
             <td class="text-right">{{value}}</td>
+            <td class="text-left">{{unit}}</td>
             </tr>
            {{/each}}
           </tbody>
@@ -357,6 +446,117 @@
  <h2>{{benchName}}</h2>
 
  <div id="benchChart" style="width:100%; height:400px">
+ </div>
+
+</script>
+
+<script id="compare" type="text/x-handlebars-template">
+ {{> nav}}
+
+ <div class="container">
+  <div class="row">
+   <div class="col-md-6 col-md-push-6">
+    {{#if rev1.summary}}{{#if rev2.summary}}
+    <h2>
+     Comparing
+      {{> rev-id hash=rev1.summary.hash}}..{{> rev-id hash=rev2.summary.hash}}
+    </h2>
+    <p>
+     Parents:
+     {{#each rev1.summary.parents}}
+      <a href="{{compareLink this ../rev2.summary.hash}}">{{shortRev this}}</a>
+     {{/each}}</br>
+      <a href="{{revisionLink rev1.summary.hash}}">{{> rev-id hash=rev1.summary.hash}}</a> <abbrv class="timeago" title="{{ iso8601 gitDate }}">{{ humanDate rev1.summary.gitDate}}</abbrv>:<br/>
+     <strong>{{ rev1.summary.gitSubject }}</strong><br/>
+     {{#logLink rev1.summary.hash }}
+      <a href="{{link}}">buildlog</a>
+     {{/logLink}}
+    </p>
+    <p class="text-center">
+      ⁞<br/>
+      <a href="{{diffLink rev1.summary.hash rev2.summary.hash}}">View diff</a><br/>
+      ⁞
+    </p>
+    <p>
+     Parents:
+     {{#each rev2.summary.parents}}
+      <a href="{{compareLink ../rev1.summary.hash this}}">{{shortRev this}}</a>
+     {{/each}}</br>
+      <a href="{{revisionLink rev2.summary.hash}}">{{> rev-id hash=rev2.summary.hash}}</a> <abbrv class="timeago" title="{{ iso8601 gitDate }}">{{ humanDate rev2.summary.gitDate}}</abbrv>:<br/>
+     <strong>{{ rev2.summary.gitSubject }}</strong><br/>
+     {{#logLink rev2.summary.hash }}
+      <a href="{{link}}">buildlog</a>
+     {{/logLink}}
+    </p>
+    {{/if}} {{/if}}
+   </div>
+
+   <div class="col-md-6 col-md-pull-6">
+    {{> nothing }}
+    <div class="panel-group" role="tablist">
+     {{#each groups}}
+      <div class="panel panel-default bench-panel">
+       <div class="panel-heading" role="tab" id="heading-{{@index}}">
+        <h4 class="panel-title">
+         <a class="accordion-toggle" data-toggle="collapse" href="#table-{{@index}}">
+         {{groupName}}
+         <span class="stats pull-right">
+          {{> summary-icons groupStats}}
+          <span class="indicator-toggled glyphicon glyphicon-chevron-down text-grey"/>
+          <span class="indicator-untoggled glyphicon glyphicon-chevron-right text-grey"/>
+         </span>
+         </a>
+        </h4>
+       </div>
+
+       <div id="table-{{@index}}" class="panel-collapse collapse in" role="tabpanel">
+        <div class="panel-body">
+         <table class="table table-condensed benchmark-table">
+          <thead>
+          <tr>
+          <th class="col-md-5">Benchmark name</th>
+          <th class="col-md-2 text-right">previous</th>
+          <th class="col-md-2 text-right">change</th>
+          <th class="col-md-2 text-right">now</th>
+          <th class="col-md-1 text-right"></th>
+          </tr>
+          </thead>
+          <tbody>
+           {{#each benchResults}}
+            <tr class="row-result row-{{changeType}}">
+            <td class="benchmark-name">
+	     {{name}}
+	     <a class="graph-link" title="Graphs" href="{{graphLink name ../../rev1.summary.hash ../../rev2.summary.hash}}">
+	      <span class="glyphicon glyphicon-signal"/>
+	     </a>
+	    </td>
+            <td class="text-right">{{previous}}</td>
+            <td class="text-right">{{change}}</td>
+            <td class="text-right">{{value}}</td>
+            <td class="text-left">{{unit}}</td>
+            </tr>
+           {{/each}}
+          </tbody>
+         </table>
+        </div>
+       </div> 
+      </div>
+     {{/each}}
+    </div>
+   </div>
+  </div>
+</script>
+
+<script id="graph" type="text/x-handlebars-template">
+ {{> nav}}
+
+ <div class="container">
+ <h2>{{benchName}}</h2>
+
+ <div id="benchChart" style="width:100%; height:400px">
+ </div>
+
+ <p><button id="loadMore">Load older revisions</button></p>
  </div>
 </script>
 
diff --git a/site/js/gipeda.js b/site/js/gipeda.js
--- a/site/js/gipeda.js
+++ b/site/js/gipeda.js
@@ -12,7 +12,11 @@
 	boring: false,
 	regressions: true,
     },
-    collapsedGroups: [true, false, false, false]
+    collapsedGroups: [true, false, false, false],
+    compare: {
+	from: null,
+	to: null,
+    }
 };
 
 // Signals
@@ -24,7 +28,7 @@
 // Routes
 
 var routes = {
-    index: 
+    index:
         { regex: /^$/,
           download: ['out/latest-summaries.json'],
           url: function () {return ""},
@@ -36,7 +40,7 @@
         },
     graphIndex:
         { regex: /^graphs$/,
-          download: ['out/benchNames.json'],
+          download: ['out/benchNames.json', 'out/graph-summaries.json'],
           url: function () {return "graphs"},
         },
     revision:
@@ -47,6 +51,17 @@
           },
           url: function (hash) { return "revision/" + hash; },
         },
+    compare:
+        { regex: /^compare\/([a-f0-9]+)\/([a-f0-9]+)$/,
+          viewData: function (match) { return { hash1: match[1], hash2: match[2] }; },
+          download: function () {
+            return ['out/benchNames.json',
+	            'out/reports/' + viewData.hash1 + '.json',
+	            'out/reports/' + viewData.hash2 + '.json'
+		    ];
+          },
+          url: function (hash1, hash2) { return "compare/" + hash1 + "/" + hash2; },
+        },
     graph:
         { regex: /^graph\/(.*)$/,
           viewData: function (match, options) {
@@ -129,13 +144,13 @@
 var templates = {};
 
 $(function ()  {
-  var template_ids =  ["revision", "index", "complete", "graphIndex", "graph", "revTooltip"];
+  var template_ids =  ["revision", "compare", "index", "complete", "graphIndex", "graph", "revTooltip"];
   template_ids.forEach(function(id) {
     var source = $("#" + id).html();
     templates[id] = Handlebars.compile(source);
   });
 
-  var partials_ids =  ["nav", "summary-icons", "summary-list", "nothing"];
+  var partials_ids =  ["nav", "summary-icons", "summary-list", "rev-id", "nothing", "tags"];
   partials_ids.forEach(function(id) {
     var source = $("#" + id).html();
     Handlebars.registerPartial(id, source);
@@ -147,18 +162,26 @@
   if (!hash) { return "#"; }
   return "#" + routes.revision.url(hash);
 });
-Handlebars.registerHelper('graphLink', function(benchName, hl) {
-    if (hl) {
-        return "#" + routes.graph.url(benchName,[hl]);
-    } else {
-        return "#" + routes.graph.url(benchName);
-    }
+Handlebars.registerHelper('compareLink', function(hash1,hash2) {
+  if (!hash1) { return "#"; }
+  if (!hash2) { return "#"; }
+  return "#" + routes.compare.url(hash1,hash2);
 });
+Handlebars.registerHelper('graphLink', function(benchName, hl1, hl2) {
+    hls = [];
+    if (hl1 && typeof(hl1) == 'string') {hls.push(hl1)};
+    if (hl2 && typeof(hl2) == 'string') {hls.push(hl2)};
+    return "#" + routes.graph.url(benchName,hls);
+});
 Handlebars.registerHelper('diffLink', function(rev1, rev2) {
-  return data.settings.cgitLink + "/commitdiff/" + rev2
+    return Handlebars.compile(data.settings.diffLink)({base: rev1, rev: rev2});
 });
-Handlebars.registerHelper('logLink', function(rev) {
-  return Handlebars.compile(data.settings.logLink)({rev:rev});
+Handlebars.registerHelper('logLink', function(rev, options) {
+  if (data.settings.logLink) {
+    var link = Handlebars.compile(data.settings.logLink)({rev: rev});
+    $.extend(this,{link:link});
+    return options.fn(this);
+  }
 });
 Handlebars.registerHelper('indexLink', function() {
   return "#" + routes.index.url();
@@ -180,16 +203,48 @@
   return rev.substr(0,7);
 }
 Handlebars.registerHelper('shortRev', shortRev);
+Handlebars.registerHelper('id', function (text) {
+    if (text) {
+	lines = text.split(/\r?\n/);
+	return new Handlebars.SafeString(lines.map(Handlebars.escapeExpression).join('&#10;'))
+    }
+});
 Handlebars.registerHelper('iso8601', function(timestamp) {
   if (!timestamp) { return '' };
-  return new Date(timestamp*1000).toISOString();	   
+  return new Date(timestamp*1000).toISOString();
 });
 Handlebars.registerHelper('humanDate', function(timestamp) {
   return new Date(timestamp*1000).toString();
 });
+// inspired by http://stackoverflow.com/a/17935019/946226
+Handlebars.registerHelper('each_naturally', function(context,options){
+    var output = '';
+    if (context) {
+	console.log(context);
+	var keys = jQuery.map(context, function(v,k) {return k});
+	var sorted_keys = keys.sort(naturalSort);
+	sorted_keys.map(function (k,i) {
+	    output += options.fn(context[k], {data: {key: k, index: i}});
+	});
+    }
+    return output;
+});
+Handlebars.registerHelper('each_unnaturally', function(context,options){
+    var output = '';
+    if (context) {
+	console.log(context);
+	var keys = jQuery.map(context, function(v,k) {return k});
+	var sorted_keys = keys.sort(naturalSort).reverse();
+	sorted_keys.map(function (k,i) {
+	    output += options.fn(context[k], {data: {key: k, index: i}});
+	});
+    }
+    return output;
+});
 
 // We cache everything
 var jsonSeen = {};
+var jsonFetching = {};
 function getJSON(url, callback, options) {
     var opts = {
         block: true,
@@ -198,16 +253,25 @@
     if (jsonSeen[url]) {
 	console.log("Not fetching "+url+" again.");
 	if (callback) callback();
+    } else if (jsonFetching[url]) {
+	console.log("Already fetching "+url+".");
+	if (callback) jsonFetching[url].push(callback);
     } else {
 	console.log("Fetching "+url+".");
+	jsonFetching[url] = [];
+	if (callback) jsonFetching[url].push(callback);
         $.ajax(url, {
             success: function (newdata) {
 		console.log("Fetched "+url+".");
 	        jsonSeen[url] = true;
 	        $.extend(true, data, newdata);
 	        dataChanged.dispatch();
-	        if (callback) callback();
+		$.each(jsonFetching[url], function (i,c) {c()});
+		delete jsonFetching[url];
             },
+            error: function (e) {
+                console.log("Failure fetching "+url,e);
+            },
 	    cache: false,
             dataType: 'json',
         });
@@ -225,6 +289,102 @@
   }
 }
 
+function benchmark_name_matches(pattern, name) {
+    if (pattern[pattern.length-1] == "*") {
+        return pattern.substr(0, pattern.length-1) == name.substr(0, pattern.length-1);
+    } else {
+        return pattern == name;
+    }
+}
+
+// The following logic should be kept in sync with BenchmarkSettings.hs
+function setting_for(name) {
+    var benchSettings = {
+        smallerIsBetter: true,
+        unit: "",
+        type: "integral",
+        group: "",
+        threshold: 3,
+        important: true
+    };
+
+    data.settings.benchmarks.map(function (s) {
+        if (benchmark_name_matches(s.match, name)) {
+            $.extend(benchSettings, s);
+        }
+    })
+
+    return benchSettings;
+}
+
+
+// The following logic should be kept in sync with toResult in ReportTypes.hs
+function compareResults (res1, res2) {
+    if (!res1 && !res2) { return };
+
+    name = res1? res1.name : res2.name;
+    s = setting_for(name);
+
+    res = {
+        name: name,
+        previous:    res1 ? res1.value : null,
+        value:       res2 ? res2.value : null,
+        unit:        s.unit,
+        important:   s.important,
+        changeType: "Boring",
+        change: "foobar",
+    };
+
+    if (res1 && res2){
+        if (s.type == "integral" || s.type == "float"){
+            if (res1.value == 0 && res2.value == 0) {
+                res.change = "=";
+            } else if (res1.value == 0) {
+                res.change = "+  ∞";
+                res.changeType = "Improvement";
+            } else {
+                var perc = 100.0 * (res2.value - res1.value) / res1.value;
+                var percS = Math.round (perc * 100.0) / 100;
+                if (Math.abs(perc) < 0.01) {
+                    res.change = "=";
+                } else if (perc >= 0) {
+                    res.change = "+ " + percS + "%";
+                } else {
+                    res.change = "- " + (-percS) + "%";
+                }
+
+                if (Math.abs(perc) >= s.threshold) {
+                    if (perc >= 0) {
+                        s.changeType = "Improvement";
+                    } else {
+                        s.changeType = "Regression";
+                    }
+                }
+            }
+        } else if (s.type == "small integral") {
+            if (res1.value == res2.value) {
+                res.change = "=";
+            } else if (res2.value > res1.value) {
+                res.change = "+" + (res2.value - res1.value);
+                res.changeType = "Improvement";
+            } else if (res1.value > res2.value) {
+                res.change = "-" + (res1.value - res2.value);
+                res.changeType = "Regression";
+            }
+        }
+
+        if (s.smallerIsBetter) {
+            if (res.changeType == "Improvement") {
+                res.changeType = "Regression";
+            } else if (res.changeType == "Regression") {
+                res.changeType = "Improvement";
+            }
+        }
+    }
+
+    return res;
+}
+
 // Some views require the data to be prepared in ways that are 
 // too complicated for the template, so lets do it here.
 dataViewPrepare = {
@@ -250,8 +410,64 @@
       groups : groups,
     };
   },
+  'compare': function (data, viewData) {
+    if (!data.benchGroups || !data.revisions) return {};
+    var hash1 = viewData.hash1;
+    var hash2 = viewData.hash2;
+    var rev1 = data.revisions[hash1];
+    var rev2 = data.revisions[hash2];
+    if (!rev1) return {};
+    if (!rev1.benchResults) return {};
+    if (!rev2) return {};
+    if (!rev2.benchResults) return {};
+
+    var groups = data.benchGroups.map(function (group) {
+      var benchmarks = group.groupMembers.map(function (bn) {
+	var r1 = rev1.benchResults[bn];
+	var r2 = rev2.benchResults[bn];
+	return compareResults(r1,r2);
+      }).filter(function (br) {return br});
+      return {
+	groupName: group.groupName,
+	benchResults: benchmarks,
+	groupStats: groupStats(benchmarks),
+      };
+    });
+    return {
+      rev1 : rev1,
+      rev2 : rev2,
+      groups : groups,
+    };
+  },
 }
 
+function remember_from_to() {
+    settings.compare.from = $('#compare-from').data('rev');
+    settings.compare.to = $('#compare-to').data('rev');
+}
+
+function recall_from_to() {
+    if (settings.compare.from) {
+	$('#compare-from').data('rev', settings.compare.from);
+	$('#compare-from').text(shortRev(settings.compare.from));
+    }
+    if (settings.compare.to) {
+	$('#compare-to').data('rev', settings.compare.to);
+	$('#compare-to').text(shortRev(settings.compare.to));
+    }
+    $('#go-to-compare').attr('href',current_compare_link());
+}
+
+function current_compare_link () {
+    var rev1 = settings.compare.from;
+    var rev2 = settings.compare.to;
+    if (rev1 && rev2) {
+	return "#" + routes.compare.url(rev1, rev2);
+    } else {
+	return 'javascript:alert("Please drag two revisions here to compare them")';
+    }
+}
+
 function load_template () {
     console.log('Rebuilding page');
     var context = {};
@@ -267,15 +483,33 @@
     updateCollapsedGroups();
     $('abbrv.timeago').timeago();
 
+    // Code to implement the compare-revision-drag'n'drop interface
+    $('.rev-draggable').draggable({
+        revert: true,
+	revertDuration: 0,
+    });
+    $('#compare-from, #compare-to').droppable({
+	accept: ".rev-draggable",
+        activeClass: "ui-state-highlight",
+        hoverClass: "ui-state-active",
+        drop: function( event, ui ) {
+          $( this ).text(ui.draggable.text());
+          $( this ).data('rev',ui.draggable.data('rev'));
+	  remember_from_to();
+	  $('#go-to-compare').attr('href',current_compare_link());
+      }
+    });
+    recall_from_to();
+
     if ($('#benchChart').length) {
-    	setupChart();
+	setupChart();
     }
 }
 viewChanged.add(load_template);
 dataChanged.add(load_template);
 
 function setupChart () {
-    
+
     var commits = commitsFrom(data.revisions, data.latest, data.settings.limitRecent);
     var benchName = viewData.benchName;
 
@@ -355,7 +589,7 @@
 
                 if ($("#tooltip").data('rev') != rev) {
                     $("#tooltip")
-		    	.html(shortRev(rev))
+			.html(shortRev(rev))
 			.data('rev',rev)
 		        .html(templates.revTooltip(tooltipContext))
                         .fadeIn(200)
@@ -435,8 +669,15 @@
         $('tr.summary-row.summary-improvement')
             .removeClass('summary-row-collapsed');
     }
-    $('tr.summary-row').first()
+    // Always show first entry in the history
+    $('.summary-table tr.summary-row').first()
             .removeClass('summary-row-collapsed');
+
+    $('.graph-list-panel').show().each(function() {
+        if ($(this).has('tr.summary-row:visible').length == 0) {
+            $(this).hide();
+        }
+    });
 
     updateNothingToSee();
 };
diff --git a/src/BenchmarkSettings.hs b/src/BenchmarkSettings.hs
--- a/src/BenchmarkSettings.hs
+++ b/src/BenchmarkSettings.hs
@@ -27,12 +27,13 @@
     , numberType :: NumberType
     , group :: String
     , threshold :: Double
+    , important :: Bool
     }
     deriving (Show, Generic)
 instance ToJSON BenchSettings
 
 defaultBenchSettings :: BenchSettings
-defaultBenchSettings = BenchSettings True "" IntegralNT "" 3
+defaultBenchSettings = BenchSettings True "" IntegralNT "" 3 True
 
 newtype S = S { unS :: BenchName -> BenchSettings }
 newtype SM = SM (BenchName -> (BenchSettings -> BenchSettings))
@@ -62,13 +63,15 @@
         mg <- o .:? "group"
         ms <- o .:? "smallerIsBetter"
         mth <- o .:? "threshold"
-        return $ SM $ \n b -> 
+        mi <- o .:? "important"
+        return $ SM $ \n b ->
             if n `matches` m then
                b { numberType      = fromMaybe (numberType b) mt
                  , unit            = fromMaybe (unit b) mu
                  , group           = fromMaybe (group b) mg
                  , smallerIsBetter = fromMaybe (smallerIsBetter b) ms
                  , threshold       = fromMaybe (threshold b) mth
+                 , important       = fromMaybe (important b) mi
                  }
             else b
     parseJSON _ = mzero
@@ -81,20 +84,22 @@
 
 data Settings = Settings
    { title :: String
-   , cgitLink :: String
-   , logLink :: String
+   , diffLink :: String
+   , logLink :: Maybe String
    , limitRecent :: Integer
    , start :: String
+   , interestingTags :: Maybe String
    , benchSettings :: BenchName -> BenchSettings
    }
 
 instance FromJSON Settings where
     parseJSON (Object v) =
         Settings <$> v .: "title"
-                 <*> v .: "cgitLink"
-                 <*> v .: "logLink"
+                 <*> v .: "diffLink"
+                 <*> v .:? "logLink"
                  <*> v .: "limitRecent"
                  <*> v .: "start"
+                 <*> v .:? "interestingTags"
                  <*> (unS <$> v.: "benchmarks")
     parseJSON _ = mzero
 
diff --git a/src/Data/Text/Binary.hs b/src/Data/Text/Binary.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/Binary.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE CPP #-}
+module Data.Text.Binary where
+
+#if MIN_VERSION_text(1,2,1)
+#else
+
+import Development.Shake.Classes
+
+import Data.Text
+import Data.Text.Encoding
+import Data.Functor
+
+-- I do not want a new dependency just for these, so this is copied from text-binary
+instance Binary Text where
+    put = put . encodeUtf8
+    get = decodeUtf8 <$> get
+#endif
+
+
diff --git a/src/Development/Shake/Gitlib.hs b/src/Development/Shake/Gitlib.hs
new file mode 100644
--- /dev/null
+++ b/src/Development/Shake/Gitlib.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, GeneralizedNewtypeDeriving, MultiParamTypeClasses, FlexibleInstances #-}
+module Development.Shake.Gitlib
+    ( defaultRuleGitLib
+    , getGitReference
+    , getGitContents
+    , doesGitFileExist
+    , readGitFile
+    ) where
+
+import System.IO
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Data.Functor
+import Data.Maybe
+
+
+import Development.Shake
+import Development.Shake.Rule
+import Development.Shake.Classes
+
+import Data.Text.Binary
+
+import Git
+import Git.Libgit2
+import Data.Tagged
+
+type RepoPath = FilePath
+
+newtype GetGitReferenceQ = GetGitReferenceQ (RepoPath, RefName)
+    deriving (Typeable,Eq,Hashable,Binary,NFData,Show)
+
+newtype GitSHA = GitSHA T.Text
+    deriving (Typeable,Eq,Hashable,Binary,NFData,Show)
+
+newtype GetGitFileRefQ = GetGitFileRefQ (RepoPath, RefName, FilePath)
+    deriving (Typeable,Eq,Hashable,Binary,NFData,Show)
+
+instance Rule GetGitReferenceQ GitSHA where
+    storedValue _ (GetGitReferenceQ (repoPath, name)) = do
+        Just . GitSHA <$> getGitReference' repoPath name
+
+instance Rule GetGitFileRefQ (Maybe T.Text) where
+    storedValue _ (GetGitFileRefQ (repoPath, name, filename)) = do
+        ref' <- getGitReference' repoPath name
+        Just <$> getGitFileRef' repoPath ref' filename
+
+getGitReference :: RepoPath -> String -> Action String
+getGitReference repoPath refName = do
+    GitSHA ref' <- apply1 $ GetGitReferenceQ (repoPath, T.pack refName)
+    return $ T.unpack ref'
+
+getGitContents :: RepoPath -> Action [FilePath]
+getGitContents repoPath = do
+    GitSHA ref' <- apply1 $ GetGitReferenceQ (repoPath, "HEAD")
+    liftIO $ withRepository lgFactory repoPath $ do
+        ref <- parseOid ref'
+        commit <- lookupCommit (Tagged ref)
+        tree <- lookupTree (commitTree commit)
+        entries <- listTreeEntries tree
+        return $ map (BS.unpack . fst) entries
+
+-- Will also look through annotated tags
+getGitReference' :: RepoPath -> RefName -> IO T.Text
+{- This fails (https://github.com/jwiegley/gitlib/issues/49), so use command
+ - line git instead.
+getGitReference' repoPath refName = do
+    withRepository lgFactory repoPath $ do
+        Just ref <- resolveReference refName
+        o <- lookupObject ref
+        r <- case o of
+            TagObj t -> do
+                return $ renderObjOid $ tagCommit t
+            _ -> return $ renderOid ref
+        return $ renderOid ref
+-}
+getGitReference' repoPath refName = do
+    T.pack . concat . lines . fromStdout <$> cmd ["git", "-C", repoPath, "rev-parse", T.unpack refName++"^{commit}"]
+
+getGitFileRef' :: RepoPath -> T.Text -> FilePath -> IO (Maybe T.Text)
+getGitFileRef' repoPath ref' fn = do
+    withRepository lgFactory repoPath $ do
+        ref <- parseOid ref'
+        commit <- lookupCommit (Tagged ref)
+        tree <- lookupTree (commitTree commit)
+        entry <- treeEntry tree (BS.pack fn)
+        case entry of
+            Just (BlobEntry ref _) -> return $ Just $ renderObjOid ref
+            _ -> return Nothing
+
+doesGitFileExist :: RepoPath -> FilePath -> Action Bool
+doesGitFileExist repoPath fn = do
+    res <- apply1 $ GetGitFileRefQ (repoPath, "HEAD", fn)
+    return $ isJust (res :: Maybe T.Text)
+
+readGitFile :: FilePath -> FilePath -> Action BS.ByteString
+readGitFile repoPath fn = do
+    res <- apply1 $ GetGitFileRefQ (repoPath, "HEAD", fn)
+    case res of
+        Nothing -> fail "readGitFile: File does not exist"
+        Just ref' -> liftIO $ withRepository lgFactory repoPath $ do
+            ref <- parseOid ref'
+            catBlob (Tagged ref)
+
+defaultRuleGitLib :: Rules ()
+defaultRuleGitLib = do
+    rule $ \(GetGitReferenceQ (repoPath, refName)) -> Just $ liftIO $
+        GitSHA <$> getGitReference' repoPath refName
+    rule $ \(GetGitFileRefQ (repoPath, refName, fn)) -> Just $ do
+        GitSHA ref' <- apply1 $ GetGitReferenceQ (repoPath, "HEAD")
+        liftIO $ getGitFileRef' repoPath ref' fn
+
diff --git a/src/GraphReport.hs b/src/GraphReport.hs
--- a/src/GraphReport.hs
+++ b/src/GraphReport.hs
@@ -7,6 +7,8 @@
 import qualified Data.Text as T
 import Data.Aeson
 import GHC.Generics
+import Data.Maybe
+import Data.Functor
 
 import Paths
 import ReadResult
@@ -18,19 +20,22 @@
     settings <- S.readSettings "settings.yaml"
 
     g <- forM revs $ \rev -> do
-        m <- readCSV rev
-        let v = M.lookup bench m
-        return $ T.pack rev .= object
-            [ "benchResults" .= object
-                [ T.pack bench .= object
-                    [ "value" .= v ]
-                ]
-            ]
+        json <- BS.readFile (reportOf rev)
+        rep <- case eitherDecode json of
+            Left e -> fail e
+            Right rep -> return rep
+        case M.lookup bench (benchResults (rep !!! "revisions" !!! rev)) of
+            Nothing -> return Nothing
+            Just result -> return $ Just $ T.pack rev .= object
+                        [ "benchResults" .= object
+                            [ T.pack bench .= benchResultToGraphPoint result ]
+                        ]
     let doc = object
-                [ "revisions" .= object g
+                [ "revisions" .= object (catMaybes g)
                 , "benchmarkSettings" .= object
                     [ T.pack bench .= toJSON (S.benchSettings settings bench) ]
                 ]
 
     BS.putStr (encode doc)
-    
+  where
+   m !!! k =  m M.! T.pack k
diff --git a/src/GraphSummaries.hs b/src/GraphSummaries.hs
new file mode 100644
--- /dev/null
+++ b/src/GraphSummaries.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE OverloadedStrings #-}
+module GraphSummaries where
+
+import qualified Data.ByteString.Lazy as BS
+import Data.Aeson
+import Data.Aeson.Types
+import Control.Monad
+import qualified Data.Text as T
+import qualified Data.Map as M
+
+import Paths
+import JsonUtils
+import ReportTypes
+import ReadResult
+
+graphSummaries :: [BenchName] -> IO ()
+graphSummaries benchNames = do
+    g <- forM benchNames $ \bName -> do
+        json <- BS.readFile (graphFile bName)
+        graph <- case eitherDecode json of
+            Left e -> fail e
+            Right rep -> return rep
+        let gps = either (error.show) id . flip parseEither graph $ \obj -> do
+                revs <- obj .: "revisions"
+                forM (M.elems (revs :: M.Map T.Text Object)) $ \rev -> do
+                    results <- rev .: "benchResults"
+                    result <- results .: T.pack bName
+                    parseJSON result
+        return $ T.pack bName .= object
+                [ T.pack "improvements" .= length [ () | gp <- gps, gpChangeType gp == Improvement ]
+                , T.pack "regressions"  .= length [ () | gp <- gps, gpChangeType gp == Regression]
+                ]
+    let o = object [ "graphSummaries" .= object g ]
+    BS.putStr $ encode o
diff --git a/src/IndexReport.hs b/src/IndexReport.hs
deleted file mode 100644
--- a/src/IndexReport.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module IndexReport where
-
-import qualified Data.ByteString.Lazy as BS
-import Data.Aeson
-import Control.Monad
-
-import Paths
-import JsonUtils
-
-
-indexReportsMain :: [FilePath] -> IO ()
-indexReportsMain revs = do
-    g <- forM revs $ \rev -> do
-        json <- BS.readFile (summaryOf rev)
-        case eitherDecode json of
-            Left e -> fail e
-            Right rep -> return (rep :: Value)
-
-    BS.putStr (encode (merges g))
diff --git a/src/JsonSettings.hs b/src/JsonSettings.hs
--- a/src/JsonSettings.hs
+++ b/src/JsonSettings.hs
@@ -2,25 +2,13 @@
 
 module JsonSettings where
 
+import Data.Yaml
 import Data.Aeson
 import qualified Data.ByteString.Lazy as BS
-
-import BenchmarkSettings as S
+import Data.Functor
 
 jsonSettingsMain :: IO ()
 jsonSettingsMain = do
-    Settings {..} <- S.readSettings "settings.yaml"
-    let o = object
-            [ "settings" .= object
-                [ "title" .= title
-                , "cgitLink" .= cgitLink
-                , "logLink" .= logLink
-                , "limitRecent" .= limitRecent
-                ]
-            ]
-    BS.putStr (encode o)
-
-
-
-    
-
+    s <- either (error.show) id <$> decodeFileEither "settings.yaml"
+    let o = object [ "settings" .= (s :: Object) ]
+    BS.putStr (Data.Aeson.encode o)
diff --git a/src/ReadResult.hs b/src/ReadResult.hs
--- a/src/ReadResult.hs
+++ b/src/ReadResult.hs
@@ -12,6 +12,7 @@
 import Data.Aeson hiding (decode)
 import GHC.Generics
 import Data.Char
+import Data.Scientific
 
 import Paths
 
@@ -24,11 +25,11 @@
 
 type ResultMap = M.Map String BenchValue
 
-instance ToJSON BenchValue where 
+instance ToJSON BenchValue where
     toJSON (F d) = toJSON d
     toJSON (I i) = toJSON i
-instance FromJSON BenchValue where 
-    parseJSON o = (I <$> parseJSON o) <|> (F <$> parseJSON o)
+instance FromJSON BenchValue where
+    parseJSON o = either F I . floatingOrInteger <$> parseJSON o
 
 instance FromField BenchValue where
     parseField s = (I <$> parseField s) <|> (F <$> parseField s)
diff --git a/src/ReportTypes.hs b/src/ReportTypes.hs
--- a/src/ReportTypes.hs
+++ b/src/ReportTypes.hs
@@ -4,9 +4,11 @@
 
 import qualified Data.Map as M
 import Data.Aeson
+import Data.Aeson.Types
 import GHC.Generics
 import Text.Printf
 import Data.List
+import Data.Char
 
 import Paths
 import ReadResult
@@ -14,7 +16,7 @@
 
 data ClientSettings = ClientSettings
    { title :: String
-   , cgitLink :: String
+   , diffLink :: String
    , logLink :: String
    }
  deriving (Generic)
@@ -46,15 +48,18 @@
     { totalCount :: Int
     , improvementCount :: Int
     , regressionCount :: Int
+    , summaryDesc :: String
     }
  deriving (Show, Generic)
 instance ToJSON SummaryStats
 instance FromJSON SummaryStats
 
+{-
 sumStats :: [SummaryStats] -> SummaryStats
 sumStats = foldl' go (SummaryStats 0 0 0)
   where go (SummaryStats a b c) (SummaryStats a' b' c') =
             SummaryStats (a + a') (b + b') (c + c')
+-}
 
 data Summary = Summary
     { hash :: Hash
@@ -78,7 +83,7 @@
 instance FromJSON RevReport
 
 data ChangeType = Improvement | Boring | Regression
- deriving (Generic)
+ deriving (Eq, Generic)
 instance ToJSON ChangeType
 instance FromJSON ChangeType
 
@@ -97,11 +102,32 @@
     , change :: String
     , changeType :: ChangeType
     , unit :: String
+    , important :: Bool
     }
  deriving (Generic)
-instance ToJSON BenchResult
-instance FromJSON BenchResult
+instance ToJSON BenchResult where
+    toJSON = genericToJSON defaultOptions
+instance FromJSON BenchResult where
+    parseJSON = genericParseJSON defaultOptions
 
+-- A smaller BenchResult
+data GraphPoint = GraphPoint
+    { gpValue :: BenchValue
+    , gpChangeType :: ChangeType
+    }
+ deriving (Generic)
+instance ToJSON GraphPoint where
+    toJSON = genericToJSON (defaultOptions { fieldLabelModifier = fixup })
+instance FromJSON GraphPoint where
+    parseJSON = genericParseJSON (defaultOptions {fieldLabelModifier = fixup })
+
+fixup ('g':'p':c:cs) = toLower c : cs
+
+benchResultToGraphPoint (BenchResult {..}) = GraphPoint
+    { gpValue = value
+    , gpChangeType = changeType
+    }
+
 invertChangeType :: ChangeType -> ChangeType
 invertChangeType Improvement = Regression
 invertChangeType Boring = Boring
@@ -146,8 +172,9 @@
 explain :: S.BenchSettings -> BenchValue -> BenchValue -> (String, ChangeType)
 explain s@(S.numberType -> S.SmallIntegralNT) (I i1) (I i2) = explainSmallInt s i1 i2
 explain s@(S.numberType -> S.IntegralNT)      (I i1) (I i2) = explainInt s i1 i2
-explain s@(S.numberType -> S.FloatingNT)      v1     v2     = explainFloat s (toFloat v1) (toFloat v2)
-explain _ _ _ = noExplanation
+-- Treat everything else as Floats, so that we do something sensible
+-- even if the user did not set the numberType correctly:
+explain s                                     v1     v2     = explainFloat s (toFloat v1) (toFloat v2)
 
 toResult :: S.BenchSettings -> String -> BenchValue -> Maybe BenchValue -> BenchResult
 toResult s name value prev = BenchResult
@@ -157,6 +184,7 @@
     , change = change
     , changeType = changeType
     , unit = S.unit s
+    , important = S.important s
     }
   where 
     (change, changeType') =
@@ -169,10 +197,25 @@
 toSummaryStats :: [BenchResult] -> SummaryStats
 toSummaryStats res = SummaryStats
     { totalCount = length res
-    , improvementCount = length [ () | BenchResult { changeType = Improvement } <- res ]
-    , regressionCount =  length [ () | BenchResult { changeType = Regression } <- res ]
+    , improvementCount = length
+        [ ()
+        | BenchResult { changeType = Improvement, important = True } <- res
+        ]
+    , regressionCount =  length
+        [ ()
+        | BenchResult { changeType = Regression, important = True } <- res
+        ]
+    , summaryDesc = andMore 5
+        [ name r ++ ": " ++ change r
+        | r <- res, important r, changeType r `elem` [Improvement, Regression]
+        ]
     }
 
+andMore :: Int -> [String] -> String
+andMore _ [] = "–"
+andMore n xs = intercalate "\n" (take n xs) ++ rest
+  where rest | length xs > n = "\nand " ++ show (length xs - n) ++ " more"
+             | otherwise     = ""
 
 {-
 toGroup :: String -> [BenchResult] -> BenchGroup
diff --git a/src/RevReport.hs b/src/RevReport.hs
--- a/src/RevReport.hs
+++ b/src/RevReport.hs
@@ -20,10 +20,10 @@
 
     thisM <- readCSV this
     parentM <- case parents of
-        p:_ -> readCSV (head parents)
+        p:_ -> readCSV p
         _    -> return M.empty
 
-    log <- case parents of 
+    log <- case parents of
         p:_ -> fromStdout <$> git ["log", p ++ ".."++ this]
         _   -> fromStdout <$> git ["show", "-s", this]
 
diff --git a/src/Shake.hs b/src/Shake.hs
--- a/src/Shake.hs
+++ b/src/Shake.hs
@@ -1,20 +1,29 @@
-{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving   #-}
+{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, NondecreasingIndentation #-}
 module Shake where
 
 import Prelude hiding ((*>))
 
-import Development.Shake
+import Development.Shake hiding (withTempFile)
 import Development.Shake.FilePath
 import Development.Shake.Classes
 import Control.Monad
 import qualified Data.Map as M
 import Data.Functor
 import Data.List
+import System.IO.Extra (newTempFile)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+import qualified System.Directory
+import Data.Aeson
+import qualified Data.Text as T
 
+import Development.Shake.Gitlib
+
 import Paths hiding (Hash)
 import ParentMap
 import BenchmarksInCSV
 import qualified BenchmarkSettings as S
+import JsonUtils
 
 {- Global settings -}
 cGRAPH_HISTORY :: Integer
@@ -42,29 +51,52 @@
     need existing
     return existing
 
-findPred, findPredOrSelf :: ParentMap -> Hash -> Action (Maybe Hash)
-findPredOrSelf m h = do
-    ex <- doesFileExist (logsOf h)
+doesLogExist :: LogSource -> Hash -> Action Bool
+doesLogExist BareGit    hash = doesGitFileExist "logs" (hash <.> "log")
+doesLogExist FileSystem hash = doesFileExist (logsOf hash)
+doesLogExist NoLogs     hash = doesFileExist (resultsOf hash)
+
+findPred, findPredOrSelf :: LogSource -> ParentMap -> Hash -> Action (Maybe Hash)
+findPredOrSelf logSource m h = do
+    ex <- doesLogExist logSource h
     if ex then return (Just h)
-          else findPred m h
-findPred m h = case M.lookup h m of 
-    Just h' -> findPredOrSelf m h'
+          else findPred logSource m h
+findPred logSource m h = case M.lookup h m of
+    Just h' -> findPredOrSelf logSource m h'
     Nothing -> return Nothing
 
-findRecent :: ParentMap -> Integer -> FilePath -> Action [FilePath]
-findRecent _ 0 _ = return []
-findRecent m n h = do
-    pM <- findPred m h
+findRecent :: LogSource -> ParentMap -> Integer -> FilePath -> Action [FilePath]
+findRecent _ _ 0 _ = return []
+findRecent logSource m n h = do
+    pM <- findPred logSource m h
     (h:) <$> case pM of
         Nothing -> return []
-        Just p ->  findRecent m (n-1) p
+        Just p ->  findRecent logSource m (n-1) p
 
 newtype LimitRecent = LimitRecent ()
     deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
 
+data LogSource = FileSystem | BareGit | NoLogs deriving Show
+
+determineLogSource :: IO LogSource
+determineLogSource = do
+    haveLogs <- System.Directory.doesDirectoryExist "logs"
+    if haveLogs
+    then do
+        Stdout s <- cmd "git -C logs rev-parse --is-bare-repository"
+        if s == "true\n"
+        then return BareGit
+        else return FileSystem
+    else return NoLogs
+
 shakeMain :: IO ()
-shakeMain = shakeArgs shakeOptions $ do
+shakeMain = do
+    logSource <- determineLogSource
+    print logSource
 
+    shakeArgs shakeOptions $ do
+    defaultRuleGitLib
+
 {-
     "gipeda" *> \out ->  do
         sources <- getDirectoryFiles "src" ["*.hs"]
@@ -81,7 +113,7 @@
         range <- gitRange
         Stdout range <- git "log" ["--format=%H",range]
         let hashes = words range
-        withLogs <- filterM (doesFileExist . logsOf) hashes
+        withLogs <- filterM (doesLogExist logSource) hashes
         need $ map reportOf withLogs
     want ["reports"]
 
@@ -89,7 +121,7 @@
         range <- gitRange
         Stdout range <- git "log" ["--format=%H",range]
         let hashes = words range
-        withLogs <- filterM (doesFileExist . logsOf) hashes
+        withLogs <- filterM (doesLogExist logSource) hashes
         need $ map summaryOf withLogs
     want ["summaries"]
 
@@ -97,8 +129,8 @@
         alwaysRerun
         Stdout stdout <- git "rev-parse" ["master"]
         writeFileChanged out stdout
-     
 
+
     "site/out/history.csv" *> \out -> do
         range <- gitRange
         Stdout stdout <- git "log" ["--format=%H;%P",range]
@@ -109,19 +141,31 @@
          orderOnly ["site/out/history.csv"]
          liftIO $ ssvFileToMap "site/out/history.csv"
     let history = history' ()
-    let pred h = do { hist <- history; findPred hist h }
-    let predOrSelf h = do { hist <- history; findPredOrSelf hist h }
-    let recent n h = do { hist <- history; findRecent hist n h }
+    let pred h = do { hist <- history; findPred logSource hist h }
+    let predOrSelf h = do { hist <- history; findPredOrSelf logSource hist h }
+    let recent n h = do { hist <- history; findRecent logSource hist n h }
 
     "site/out/latest.txt" *> \ out -> do
         [head] <- readFileLines "site/out/head.txt"
         latestM <- predOrSelf head
         case latestM of
-           Just latest -> 
+           Just latest ->
                 writeFileChanged out latest
-           Nothing -> 
+           Nothing ->
                 fail "Head has no parent with logs?"
 
+    "site/out/tags.txt" *> \ out -> do
+        alwaysRerun
+
+        need ["settings.yaml"]
+        s <- liftIO $ S.readSettings "settings.yaml"
+        case S.interestingTags s of
+            Nothing ->
+                writeFileChanged out ""
+            Just pattern -> do
+                Stdout tags <- git "tag" ["-l", pattern]
+                writeFileChanged out tags
+
     "graphs" ~> do
         [latest] <- readFileLines "site/out/latest.txt"
         need [resultsOf latest]
@@ -129,11 +173,22 @@
         need (map graphFile b)
     want ["graphs"]
 
-    "site/out/results/*.csv" *> \out -> do
-        let hash = takeBaseName out
-        need [logsOf hash]
-        Stdout csv <- cmd "./log2csv" (logsOf hash)
-        writeFile' out csv
+    case logSource of
+        BareGit ->
+            "site/out/results/*.csv" *> \out -> do
+                let hash = takeBaseName out
+                withTempFile $ \fn -> do
+                    log <- readGitFile "logs" (hash <.> "log")
+                    liftIO $ BS.writeFile fn log
+                    Stdout csv <- cmd "./log2csv" fn
+                    writeFile' out csv
+        FileSystem ->
+            "site/out/results/*.csv" *> \out -> do
+                let hash = takeBaseName out
+                need [logsOf hash]
+                Stdout csv <- cmd "./log2csv" (logsOf hash)
+                writeFile' out csv
+        NoLogs -> return ()
 
     "site/out/graphs//*.json" *> \out -> do
         let bench = dropDirectory1 (dropDirectory1 (dropDirectory1 (dropExtension out)))
@@ -141,7 +196,7 @@
         [latest] <- readFileLines "site/out/latest.txt"
         limitRecent <- getLimitRecent (LimitRecent ())
         r <- recent limitRecent latest
-        need (map resultsOf r)
+        need (map reportOf r)
 
         Stdout json <- self "GraphReport" (bench : r)
         writeFile' out json
@@ -165,13 +220,38 @@
 
     "site/out/latest-summaries.json" *> \out -> do
         [latest] <- readFileLines "site/out/latest.txt"
-        r <- recent cGRAPH_HISTORY latest
-        need (map summaryOf r)
+        recentCommits <- recent cGRAPH_HISTORY latest
 
-        Stdout json <- self "IndexReport" r
-        writeFile' out json
+        tags <- readFileLines "site/out/tags.txt"
+        tagsAndHashes <- forM tags $ \t -> do
+            h <- getGitReference "repository" ("refs/tags/" ++ t)
+            return $ (t, h)
+        let o = object [ T.pack "tags" .= object [ (T.pack t .= h) | (t,h) <- tagsAndHashes ]]
+        liftIO $ LBS.writeFile out (encode o)
+        tagCommits <- filterM (doesLogExist logSource) (map snd tagsAndHashes)
+
+        let revs = nub $ recentCommits ++ tagCommits
+
+        need $ map summaryOf revs
+
+        g <- forM revs $ \rev -> do
+            json <- liftIO $ LBS.readFile (summaryOf rev)
+            case eitherDecode json of
+                Left e -> fail e
+                Right rep -> return (rep :: Value)
+        liftIO $ LBS.writeFile out (encode (merges (o:g)))
     want ["site/out/latest-summaries.json"]
 
+    "site/out/graph-summaries.json" *> \out -> do
+        [latest] <- readFileLines "site/out/latest.txt"
+        need [resultsOf latest]
+        b <- liftIO $ benchmarksInCSVFile (resultsOf latest)
+        need (map graphFile b)
+
+        Stdout json <- self "GraphSummaries" b
+        writeFile' out json
+    want ["site/out/graph-summaries.json"]
+
     "site/out/benchNames.json" *> \out -> do
         [latest] <- readFileLines "site/out/latest.txt"
         need [resultsOf latest]
@@ -182,18 +262,21 @@
         Stdout json <- self "BenchNames" (nub b)
         writeFile' out json
     want ["site/out/benchNames.json"]
-        
 
 
     "site/out/all-summaries.json" *> \out -> do
         range <- gitRange
         Stdout range <- git "log" ["--format=%H",range]
         let hashes = words range
-        withLogs <- filterM (doesFileExist . logsOf) hashes
-        need (map summaryOf withLogs)
+        revs <- filterM (doesLogExist logSource) hashes
+        need (map summaryOf revs)
 
-        Stdout json <- self "IndexReport" withLogs
-        writeFile' out json
+        g <- forM revs $ \rev -> do
+            json <- liftIO $ LBS.readFile (summaryOf rev)
+            case eitherDecode json of
+                Left e -> fail e
+                Right rep -> return (rep :: Value)
+        liftIO $ LBS.writeFile out (encode (merges g))
     want ["site/out/all-summaries.json"]
 
     "site/out/settings.json" *> \out -> do
@@ -206,3 +289,12 @@
     phony "clean" $ do
         removeFilesAfter "site/out" ["//*"]
 
+
+-- | Create a temporary file in the temporary directory. The file will be deleted
+--   after the action completes (provided the file is not still open).
+--   The 'FilePath' will not have any file extension, will exist, and will be zero bytes long.
+--   If you require a file with a specific name, use 'withTempDir'.
+withTempFile :: (FilePath -> Action a) -> Action a
+withTempFile act = do
+    (file, del) <- liftIO newTempFile
+    act file `actionFinally` del
diff --git a/src/gipeda.hs b/src/gipeda.hs
--- a/src/gipeda.hs
+++ b/src/gipeda.hs
@@ -8,10 +8,12 @@
 import RevReport
 import WithLatestLogs
 import Summary
-import IndexReport
 import GraphReport
 import BenchNames
+import GraphSummaries
 
+import GHC.IO.Encoding
+
 {-
 We want to build everything into one executable, bit still treat
 it as multiple tools. Hence the main function
@@ -20,19 +22,23 @@
 
 main :: IO ()
 main = do
+ 
+    -- Make us locale-independent 
+    setLocaleEncoding utf8
+
     args <- getArgs
 
     ex <- doesFileExist "settings.yaml"
     unless ex $ do
-        hPutStr stderr "Please run this from the same directory as the settings.yaml file."
+        hPutStr stderr "Please run this from the same directory as the settings.yaml file.\n"
 
 
     case args of 
-        "IndexReport":opts    -> indexReportsMain opts
         "JsonSettings":_      -> jsonSettingsMain
         "Summary":opts        -> summaryMain opts
         "RevReport":opts      -> revReportMain opts
         "GraphReport":opts    -> graphReportMain opts
         "WithLatestLogs":opts -> withLatestLogsMain opts
         "BenchNames":opts     -> benchNamesMain opts
+        "GraphSummaries":opts  -> graphSummaries opts
         _ -> shakeMain -- shake will take the arguments from getArgs
