 git-arr            |   23 ++++++++++++++++++-
 git.py             |   26 +++++++++++++++++++--
 static/git-arr.css |   20 ++++++++++++++++
 static/git-arr.js  |   63 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 utils.py           |   39 ++++++++++++++++++++++++++++++++
 views/blob.html    |    6 ++++-
 views/commit.html  |    8 +++---
 views/index.html   |    4 ++-
 8 files changed, 179 insertions(+), 10 deletions(-)

diff --git a/git-arr b/git-arr
index 173a6b2..418f883 100755
--- a/git-arr
+++ b/git-arr
@@ -55,6 +55,8 @@ def load_config(path):
         'web_url_file': 'web_url',
         'git_url': '',
         'git_url_file': 'cloneurl',
+        'embed_markdown': 'yes',
+        'embed_images': 'no',
     }
 
     config = configparser.SafeConfigParser(defaults)
@@ -115,6 +117,9 @@ def load_config(path):
         if not r.info.git_url and os.path.isfile(git_url_file):
             r.info.git_url = open(git_url_file).read()
 
+        r.info.embed_markdown = config.getboolean(s, 'embed_markdown')
+        r.info.embed_images = config.getboolean(s, 'embed_images')
+
         repos[r.name] = r
 
 def find_git_dir(path):
@@ -174,6 +179,10 @@ def with_utils(f):
         'can_colorize': utils.can_colorize,
         'colorize_diff': utils.colorize_diff,
         'colorize_blob': utils.colorize_blob,
+        'can_markdown': utils.can_markdown,
+        'markdown_blob': utils.markdown_blob,
+        'can_embed_image': utils.can_embed_image,
+        'embed_image_blob': utils.embed_image_blob,
         'abort': bottle.abort,
         'smstr': git.smstr,
     }
@@ -260,6 +269,16 @@ def static(path):
 # Static HTML generation
 #
 
+def is_404(e):
+    """True if e is an HTTPError with status 404, False otherwise."""
+    # We need this because older bottle.py versions put the status code in
+    # e.status as an integer, and newer versions make that a string, and using
+    # e.status_code for the code.
+    if isinstance(e.status, int):
+        return e.status == 404
+    else:
+        return e.status_code == 404
+
 def generate(output, skip_index = False):
     """Generate static html to the output directory."""
     def write_to(path, func_or_str, args = (), mtime = None):
@@ -343,6 +362,8 @@ def generate(output, skip_index = False):
     read_f = lambda f: open(f).read()
     write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
             os.stat(static_path + '/git-arr.css').st_mtime)
+    write_to('static/git-arr.js', read_f, [static_path + '/git-arr.js'],
+            os.stat(static_path + '/git-arr.js').st_mtime)
     write_to('static/syntax.css', read_f, [static_path + '/syntax.css'],
             os.stat(static_path + '/syntax.css').st_mtime)
 
@@ -385,7 +406,7 @@ def generate(output, skip_index = False):
                 # Some repos can have tags pointing to non-commits. This
                 # happens in the Linux Kernel's v2.6.11, which points directly
                 # to a tree. Ignore them.
-                if e.status == 404:
+                if is_404(e):
                     print('404 in tag %s (%s)' % (tag_name, obj_id))
                 else:
                     raise
diff --git a/git.py b/git.py
index d005b13..7e7ad74 100644
--- a/git.py
+++ b/git.py
@@ -207,11 +207,13 @@ class Repo:
         """Returns a GitCommand() on our path."""
         return GitCommand(self.path, cmd)
 
-    def for_each_ref(self, pattern = None, sort = None):
+    def for_each_ref(self, pattern = None, sort = None, count = None):
         """Returns a list of references."""
         cmd = self.cmd('for-each-ref')
         if sort:
             cmd.sort = sort
+        if count:
+            cmd.count = count
         if pattern:
             cmd.arg(pattern)
 
@@ -325,7 +327,7 @@ class Repo:
             ref = self.branch
         return Tree(self, ref)
 
-    def blob(self, path, ref = None):
+    def blob(self, path, ref = None, raw = False):
         """Returns the contents of the given path."""
         if not ref:
             ref = self.branch
@@ -341,8 +343,21 @@ class Repo:
         if not head or head.strip().endswith('missing'):
             return None
 
+        # Raw option in case we need a binary blob and not a utf-8 encoded one.
+        if raw:
+            return out.fd.read()
+
         return out.read()
 
+    def last_commit_timestamp(self):
+        """Return the timestamp of the last commit."""
+        refs = self.for_each_ref(pattern = 'refs/heads/',
+                sort = '-committerdate', count = 1)
+        for obj_id, _, _ in refs:
+            commit = self.commit(obj_id)
+            return commit.committer_epoch
+        return -1
+
 
 class Commit (object):
     """A git commit."""
@@ -397,7 +412,12 @@ class Commit (object):
     @staticmethod
     def from_str(repo, buf):
         """Parses git rev-list output, returns a commit object."""
-        header, raw_message = buf.split('\n\n', 1)
+        if '\n\n' in buf:
+            # Header, commit message
+            header, raw_message = buf.split('\n\n', 1)
+        else:
+            # Header only, no commit message
+            header, raw_message = buf.rstrip(), '    '
 
         header_lines = header.split('\n')
         commit_id = header_lines.pop(0)
diff --git a/static/git-arr.css b/static/git-arr.css
index 2e28c69..8da2b67 100644
--- a/static/git-arr.css
+++ b/static/git-arr.css
@@ -100,6 +100,26 @@ span.tag {
     background-color: #ffff88;
 }
 
+/* Age of an object.
+ * Note this is hidden by default as we rely on javascript to show it. */
+span.age {
+    display: none;
+    color: gray;
+    font-size: x-small;
+}
+
+span.age-band0 {
+    color: darkgreen;
+}
+
+span.age-band1 {
+    color: green;
+}
+
+span.age-band2 {
+    color: seagreen;
+}
+
 /* Commit message and diff. */
 pre.commit-message {
     font-size: large;
diff --git a/static/git-arr.js b/static/git-arr.js
new file mode 100644
index 0000000..d1e3b81
--- /dev/null
+++ b/static/git-arr.js
@@ -0,0 +1,63 @@
+/* Miscellaneous javascript functions for git-arr. */
+
+/* Return the current timestamp. */
+function now() {
+    return (new Date().getTime() / 1000);
+}
+
+/* Return a human readable string telling "how long ago" for a timestamp. */
+function how_long_ago(timestamp) {
+    if (timestamp < 0)
+        return "never";
+
+	var seconds = Math.floor(now() - timestamp);
+
+	var interval = Math.floor(seconds / (365 * 24 * 60 * 60));
+	if (interval > 1)
+		return interval + " years ago";
+
+	interval = Math.floor(seconds / (30 * 24 * 60 * 60));
+	if (interval > 1)
+		return interval + " months ago";
+
+	interval = Math.floor(seconds / (24 * 60 * 60));
+
+	if (interval > 1)
+		return interval + " days ago";
+	interval = Math.floor(seconds / (60 * 60));
+
+	if (interval > 1)
+		return interval + " hours ago";
+
+	interval = Math.floor(seconds / 60);
+	if (interval > 1)
+		return interval + " minutes ago";
+
+    if (seconds > 1)
+        return Math.floor(seconds) + " seconds ago";
+
+    return "about now";
+}
+
+/* Go through the document and replace the contents of the span.age elements
+ * with a human-friendly variant, and then show them. */
+function replace_timestamps() {
+    var elements = document.getElementsByClassName("age");
+    for (var i = 0; i < elements.length; i++) {
+        var e = elements[i];
+
+        var timestamp = e.innerHTML;
+        e.innerHTML = how_long_ago(timestamp);
+        e.style.display = "inline";
+
+        if (timestamp > 0) {
+            var age = now() - timestamp;
+            if (age < (2 * 60 * 60))
+                e.className = e.className + " age-band0";
+            else if (age < (3 * 24 * 60 * 60))
+                e.className = e.className + " age-band1";
+            else if (age < (30 * 24 * 60 * 60))
+                e.className = e.className + " age-band2";
+        }
+    }
+}
diff --git a/utils.py b/utils.py
index 801580e..51141e4 100644
--- a/utils.py
+++ b/utils.py
@@ -12,6 +12,13 @@ try:
 except ImportError:
     pygments = None
 
+try:
+    import markdown
+except ImportError:
+    markdown = None
+
+import base64
+import mimetypes
 
 def shorten(s, width = 60):
     if len(s) < 60:
@@ -41,6 +48,24 @@ def can_colorize(s):
 
     return True
 
+def can_markdown(repo, fname):
+    """True if we can process file through markdown, False otherwise."""
+    if markdown is None:
+        return False
+
+    if not repo.info.embed_markdown:
+        return False
+
+    return fname.endswith(".md")
+
+def can_embed_image(repo, fname):
+    """True if we can embed image file in HTML, False otherwise."""
+    if not repo.info.embed_images:
+        return False
+
+    return (('.' in fname) and
+            (fname.split('.')[-1].lower() in [ 'jpg', 'jpeg', 'png', 'gif' ]))
+
 def colorize_diff(s):
     lexer = lexers.DiffLexer(encoding = 'utf-8')
     formatter = HtmlFormatter(encoding = 'utf-8',
@@ -68,3 +93,17 @@ def colorize_blob(fname, s):
 
     return highlight(s, lexer, formatter)
 
+def markdown_blob(s):
+    return markdown.markdown(s)
+
+def embed_image_blob(repo, dirname, fname):
+    mimetype = mimetypes.guess_type(fname)[0]
+
+    # Unfortunately, bottle seems to require utf-8 encoded data.
+    # We have to refetch the blob with raw=True, because the utf-8 encoded
+    # version of the blob available in the bottle template discards binary data.
+    raw_blob = repo.blob(dirname + fname, raw = True)
+
+    return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format( \
+                                    mimetype, base64.b64encode(raw_blob))
+
diff --git a/views/blob.html b/views/blob.html
index 283bbcd..9d0e9ff 100644
--- a/views/blob.html
+++ b/views/blob.html
@@ -36,7 +36,11 @@
     <a href="">{{!fname.html}}</a>
 </h3>
 
-% if can_colorize(blob):
+% if can_embed_image(repo, fname.unicode):
+{{!embed_image_blob(repo, dirname.raw, fname.raw)}}
+% elif can_markdown(repo, fname.unicode):
+{{!markdown_blob(blob)}}
+% elif can_colorize(blob):
 {{!colorize_blob(fname.unicode, blob)}}
 % else:
 <pre class="blob-body">
diff --git a/views/commit.html b/views/commit.html
index bc3119c..1ddc4d8 100644
--- a/views/commit.html
+++ b/views/commit.html
@@ -22,10 +22,10 @@
             <span class="date" title="{{c.author_date}}">
                 {{c.author_date.utc}} UTC</span></td></tr>
     <tr><td>committer</td>
-        <td><span class="name">{{c.author_name}}</span>
-            <span class="email">&lt;{{c.author_email}}&gt;</span><br/>
-            <span class="date" title="{{c.author_date}}">
-                {{c.author_date.utc}} UTC</span></td></tr>
+        <td><span class="name">{{c.committer_name}}</span>
+            <span class="email">&lt;{{c.committer_email}}&gt;</span><br/>
+            <span class="date" title="{{c.committer_date}}">
+                {{c.committer_date.utc}} UTC</span></td></tr>
 
 % for p in c.parents:
     <tr><td>parent</td>
diff --git a/views/index.html b/views/index.html
index b218b8b..7967a7c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -5,9 +5,10 @@
 <title>git</title>
 <link rel="stylesheet" type="text/css" href="static/git-arr.css"/>
 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+<script src="static/git-arr.js"></script>
 </head>
 
-<body class="index">
+<body class="index" onload="replace_timestamps()">
 <h1>git</h1>
 
 <table class="nice">
@@ -20,6 +21,7 @@
     <tr>
         <td><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
         <td><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
+        <td><span class="age">{{repo.last_commit_timestamp()}}</span></td>
     </tr>
     %end
 </table>
