22 Commits
0.01 ... 0.13

Author SHA1 Message Date
Alberto Bertogli
47d500715a views/tree.html: Fix lambda syntax
Some versions of bottle.py don't deal well with the "if" inside the lambda, so
work around it by just using comparison and simplifying the function.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2014-07-28 23:46:50 +01:00
Vanya Sergeev
eb7cadd64f Enable line number anchors when using pygments HtmlFormatter
Signed-off-by: Vanya Sergeev <vsergeev@gmail.com>
2014-07-03 00:56:19 +01:00
Vanya Sergeev
48a00cb460 Fix one-line 'if' termination in tree, blob templates
The missing '% end' template keyword to these one-line if statements was
causing bottle 0.12.7 to incorrectly indent the following line, leading to an
IndentationError at runtime when the blob and tree templates are compiled.

Signed-off-by: Vanya Sergeev <vsergeev@gmail.com>
2014-06-30 08:45:36 +01:00
Alberto Bertogli
2f65291ef1 Fix committer field in the commit view
The commit view shows the author information in the committer field; this
patch fixes it by showing the appropriate fields.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-03 10:57:54 +00:00
Alberto Bertogli
f6a75820e8 Work around HTTPError status code issues
It turned out that bottle.py is not backwards-compatible with the status code
change: older versions encode the status in e.status; newer ones use
e.status_code (and e.status became a string).

This patch works around that by trying to pick up which of the two variants we
have, and deciding accordingly.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-02 23:32:43 +00:00
Alberto Bertogli
e49c69da2e Show the age of a repository in the index, via javascript
This patch adds the age of the repository to the index view, using javascript
to give a nice human string for the age.

When javascript is not available, the element remains hidden.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-02 22:18:33 +00:00
Alberto Bertogli
6764bfcfd6 Use the status_code attribute to tell 404s appart
Newer versions of bottle have a string in the e.status attribute, and the
status code can be found in e.status_code, which should be backwards
compatible.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-02 21:15:23 +00:00
Alberto Bertogli
54026b7585 Make embedding markdown and images configurable per-repo
This patch introduces the embed_markdown and embed_images configuration
options, so users can enable and disable those features on a per-repository
basis.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-02 21:12:50 +00:00
Alberto Bertogli
a42d7da6a4 utils: Make the embedded image code use mimetypes
This patch makes minor changes to the code that handles embedded images,
mostly to make it use mimetypes, and to remove SVG support (at least for now)
due to security concerns.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-11-02 19:14:36 +00:00
Vanya Sergeev
21522f8a3a Add embed data URI image blob support 2013-11-02 19:07:59 +00:00
Vanya Sergeev
f62ca211eb Add markdown blob support 2013-11-02 19:03:59 +00:00
Vanya Sergeev
d3bf98ea00 Fix parsing of empty commit messages 2013-10-12 01:19:57 +01:00
Alberto Bertogli
6f5f3c4aa5 Add a post-receive hook
This patch adds a post-receive hook that can be used to trigger a git-arr
update when there is a push to a repository.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-03-12 22:09:25 +00:00
Alberto Bertogli
c72278c97c Allow calling the executable from any directory
When the tool is invoked like /path/to/git-arr, it currently fails because
both the serving of static files and bottle templates assume they're on the
current working directory.

This patch fixes that by computing the directories based on the executable
location.

Note this is assuming the static directory and the templates live next to the
executable, which will not always be the case, and eventually it should be
configurable; but it's ok for the time being.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-03-10 00:55:56 +00:00
Alberto Bertogli
18e8599bfa Use "cloneurl" as a default for git_url
This patch makes git_url have the same default as gitweb.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-03-09 23:23:52 +00:00
Alberto Bertogli
c303c30755 Fix the "--only" option
This patch fixes the --only option, and makes it avoid generating the
top-level index so we don't get a broken one with only the specified
repositories.

The intention is that this option is used in hooks to update the views after a
commit or push.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2013-03-09 23:23:52 +00:00
Alberto Bertogli
9ec2bde5c4 Only guess the lexer if the file starts with "#!"
The lexer guesser based on content is often wrong; to minimize the chances of
that happening, we only use it on files that start with "#!", for which it
usually has smarter rules.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-27 02:57:31 +00:00
Alberto Bertogli
36db9cc0ee Add a note about pygments in the README
Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-21 00:29:43 +00:00
Alberto Bertogli
bad8c52ef2 Fall back to guess the lexer by content
If we can't guess the lexer by the file name, try to guess based on the
content.

This allows pygments to colorize extension-less files, usually scripts.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-18 14:55:27 +00:00
Alberto Bertogli
62da3ebc08 Use heuristics to decide what to colorize
In practise pygments seems to have a very hard time processing large files and
files with long lines, so try to avoid using it in those cases.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-18 14:55:22 +00:00
Alberto Bertogli
ba3b2132f5 Improve the way we find repo paths
This patch improves the way we find the path to the repositories, both in the
recursive and in the non-recursive cases.

We now support specifying non-bare repositories directly, and also recursing
on them.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-11 13:43:02 +00:00
Alberto Bertogli
1c729578b2 Add dependencies and improve contact information to README
Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
2012-11-11 12:53:39 +00:00
13 changed files with 379 additions and 47 deletions

15
README
View File

@@ -19,6 +19,13 @@ information.
Getting started
---------------
You will need Python, and the bottle.py framework (the package is usually
called python-bottle in most distributions).
If pygments is available, it will be used for syntax highlighting, otherwise
everything will work fine, just in black and white.
First, create a configuration file for your repositories. You can start by
copying sample.conf, which has the list of the available options.
@@ -41,9 +48,9 @@ use, by running:
That can be useful when making changes to the software itself.
Where to report bugs
--------------------
Contact
-------
If you want to report bugs, or have any questions or comments, just let me
know at albertito@blitiri.com.ar.
If you want to report bugs, send patches, or have any questions or comments,
just let me know at albertito@blitiri.com.ar.

106
git-arr
View File

@@ -5,6 +5,7 @@ git-arr: A git web html generator.
from __future__ import print_function
import sys
import os
import math
import optparse
@@ -20,6 +21,18 @@ import git
import utils
# Tell bottle where to find the views.
# Note this assumes they live next to the executable, and that is not a good
# assumption; but it's good enough for now.
bottle.TEMPLATE_PATH.insert(
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
# The path to our static files.
# Note this assumes they live next to the executable, and that is not a good
# assumption; but it's good enough for now.
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
# The list of repositories is a global variable for convenience. It will be
# populated by load_config().
repos = {}
@@ -41,7 +54,9 @@ def load_config(path):
'web_url': '',
'web_url_file': 'web_url',
'git_url': '',
'git_url_file': 'git_url',
'git_url_file': 'cloneurl',
'embed_markdown': 'yes',
'embed_images': 'no',
}
config = configparser.SafeConfigParser(defaults)
@@ -49,14 +64,10 @@ def load_config(path):
# Do a first pass for general sanity checking and recursive expansion.
for s in config.sections():
if not config.has_option(s, 'path'):
raise configparser.NoOptionError(
'%s is missing the mandatory path' % s)
if config.getboolean(s, 'recursive'):
for path in os.listdir(config.get(s, 'path')):
fullpath = config.get(s, 'path') + '/' + path
if not os.path.exists(fullpath + '/HEAD'):
fullpath = find_git_dir(config.get(s, 'path') + '/' + path)
if not fullpath:
continue
if os.path.exists(fullpath + '/disable_gitweb'):
@@ -76,7 +87,13 @@ def load_config(path):
config.remove_section(s)
for s in config.sections():
fullpath = config.get(s, 'path')
fullpath = find_git_dir(config.get(s, 'path'))
if not fullpath:
raise ValueError(
'%s: path %s is not a valid git repository' % (
s, config.get(s, 'path')))
config.set(s, 'path', fullpath)
config.set(s, 'name', s)
desc = config.get(s, 'desc')
@@ -100,8 +117,34 @@ 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):
"""Returns the path to the git directory for the given repository.
This function takes a path to a git repository, and returns the path to
its git directory. If the repo is bare, it will be the same path;
otherwise it will be path + '.git/'.
An empty string is returned if the given path is not a valid repository.
"""
def check(p):
"""A dirty check for whether this is a git dir or not."""
# Note silent stderr because we expect this to fail and don't want the
# noise; and also we strip the final \n from the output.
return git.run_git(p,
['rev-parse', '--git-dir'],
silent_stderr = True).read()[:-1]
for p in [ path, path + '/.git' ]:
if check(p):
return p
return ''
def repo_filter(unused_conf):
"""Bottle route filter for repos."""
@@ -133,9 +176,13 @@ def with_utils(f):
"""
utilities = {
'shorten': utils.shorten,
'has_colorizer': utils.has_colorizer,
'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,
}
@@ -215,14 +262,24 @@ def blob(repo, bname, fname, dirname = ''):
@bottle.route('/static/<path:path>')
def static(path):
return bottle.static_file(path, root = './static/')
return bottle.static_file(path, root = static_path)
#
# Static HTML generation
#
def generate(output):
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):
path = output + '/' + path
@@ -298,14 +355,17 @@ def generate(output):
(str(r.name), str(bn), oname.raw),
tree, (r, bn, oname.url), mtime)
write_to('index.html', index())
if not skip_index:
write_to('index.html', index())
# We can't call static() because it relies on HTTP headers.
read_f = lambda f: open(f).read()
write_to('static/git-arr.css', read_f, ['static/git-arr.css'],
os.stat('static/git-arr.css').st_mtime)
write_to('static/syntax.css', read_f, ['static/syntax.css'],
os.stat('static/syntax.css').st_mtime)
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)
for r in sorted(repos.values(), key = lambda r: r.name):
write_to('r/%s/index.html' % r.name, summary(r))
@@ -346,7 +406,7 @@ def generate(output):
# 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
@@ -359,6 +419,7 @@ def main():
parser.add_option('-o', '--output', metavar = 'DIR',
help = 'output directory (for generate)')
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
default = [],
help = 'generate/serve only this repository')
opts, args = parser.parse_args()
@@ -367,22 +428,25 @@ def main():
try:
load_config(opts.config)
except configparser.NoOptionError as e:
except (configparser.NoOptionError, ValueError) as e:
print('Error parsing config:', e)
return
if not args:
parser.error('Must specify an action (serve|generate)')
if opts.only:
global repos
repos = [ r for r in repos if r.name in opts.only ]
for rname in list(repos.keys()):
if rname not in opts.only:
del repos[rname]
if args[0] == 'serve':
bottle.run(host = 'localhost', port = 8008, reloader = True)
elif args[0] == 'generate':
if not opts.output:
parser.error('Must specify --output')
generate(output = opts.output)
generate(output = opts.output,
skip_index = len(opts.only) > 0)
else:
parser.error('Unknown action %s' % args[0])

38
git.py
View File

@@ -41,7 +41,7 @@ class EncodeWrapper:
return s.decode(self.encoding, errors = self.errors)
def run_git(repo_path, params, stdin = None):
def run_git(repo_path, params, stdin = None, silent_stderr = False):
"""Invokes git with the given parameters.
This function invokes git with the given parameters, and returns a
@@ -49,11 +49,17 @@ def run_git(repo_path, params, stdin = None):
"""
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
stderr = None
if silent_stderr:
stderr = subprocess.PIPE
if not stdin:
p = subprocess.Popen(params, stdin = None, stdout = subprocess.PIPE)
p = subprocess.Popen(params,
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
else:
p = subprocess.Popen(params,
stdin = subprocess.PIPE, stdout = subprocess.PIPE)
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
stderr = stderr)
p.stdin.write(stdin)
p.stdin.close()
@@ -201,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)
@@ -319,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
@@ -335,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."""
@@ -391,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)

14
hooks/README Normal file
View File

@@ -0,0 +1,14 @@
You can use the post-receive hook to automatically generate the repository
view after a push.
To do so, configure in your target repository the following options:
$ git config hooks.git-arr-config /path/to/site.conf
$ git config hooks.git-arr-output /var/www/git/
# Only if the git-arr executable is not on your $PATH.
$ git config hooks.git-arr-path /path/to/git-arr
Then copy the post-receive file to the "hooks" directory in your repository.

58
hooks/post-receive Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/sh
#
# git-arr post-receive hook
#
# This is a script intended to be used as a post-receive hook, which updates
# its git-arr view.
#
# You should place it /path/to/your/repository.git/hooks/.
# Config
# --------
#
# hooks.git-arr-config
# The git-arr configuration file to use. Mandatory.
# Example: /srv/git-arr/site.conf
#
# hooks.git-arr-output
# Directory for the generated output. Mandatory.
# Example: /srv/www/git/
#
# hooks.git-arr-path
# The path to the git-arr executable. Optional, defaults to "git-arr".
#
# hooks.git-arr-repo-name
# The git-arr repository name. Optional, defaults to the path name.
git_arr_config="$(git config --path hooks.git-arr-config)"
git_arr_output="$(git config --path hooks.git-arr-output)"
git_arr_path="$(git config --path hooks.git-arr-path 2> /dev/null)"
git_arr_repo_name="$(git config hooks.git-arr-repo-name 2> /dev/null)"
if [ -z "$git_arr_config" -o -z "$git_arr_output" ]; then
echo "Error: missing config options."
echo "Both hooks.git-arr-config and hooks.git-arr-output must be set."
exit 1
fi
if [ -z "$git_arr_path" ]; then
git_arr_path=git-arr
fi
if [ -z "$git_arr_repo_name" ]; then
PARENT_DIR=$(cd $(dirname "$0")/..; echo "$PWD")
git_arr_repo_name=$(basename "$PARENT_DIR")
fi
echo "Running git-arr"
$git_arr_path --config "$git_arr_config" generate \
--output "$git_arr_output" \
--only "$git_arr_repo_name" > /dev/null
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "Error running git-arr"
exit $RESULT
fi

View File

@@ -38,8 +38,8 @@ path = /srv/git/repo/
# File name to get the git URLs from (optional).
# If git_url is not set, attempt to get its value from this file.
# Default: "git_url"
#git_url_file = git_url
# Default: "cloneurl" (same as gitweb).
#git_url_file = cloneurl
# Do we look for repositories within this path? (optional).
# This option enables a recursive, 1 level search for repositories within the

View File

@@ -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;

63
static/git-arr.js Normal file
View File

@@ -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";
}
}
}

View File

@@ -12,14 +12,59 @@ 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:
return s
return s[:57] + "..."
def has_colorizer():
return pygments is not None
def can_colorize(s):
"""True if we can colorize the string, False otherwise."""
if pygments is None:
return False
# Pygments can take a huge amount of time with long files, or with very
# long lines; these are heuristics to try to avoid those situations.
if len(s) > (512 * 1024):
return False
# If any of the first 5 lines is over 300 characters long, don't colorize.
start = 0
for i in range(5):
pos = s.find('\n', start)
if pos == -1:
break
if pos - start > 300:
return False
start = pos + 1
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')
@@ -30,12 +75,37 @@ def colorize_diff(s):
def colorize_blob(fname, s):
try:
lexer = lexers.guess_lexer_for_filename(fname, s)
lexer = lexers.guess_lexer_for_filename(fname, s, encoding = 'utf-8')
except lexers.ClassNotFound:
# Only try to guess lexers if the file starts with a shebang,
# otherwise it's likely a text file and guess_lexer() is prone to
# make mistakes with those.
lexer = lexers.TextLexer(encoding = 'utf-8')
if s.startswith('#!'):
try:
lexer = lexers.guess_lexer(s[:80], encoding = 'utf-8')
except lexers.ClassNotFound:
pass
formatter = HtmlFormatter(encoding = 'utf-8',
cssclass = 'source_code',
linenos = 'table')
linenos = 'table',
anchorlinenos = True,
lineanchors = 'line')
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))

View File

@@ -29,14 +29,20 @@
<a href="{{relroot}}">[{{repo.branch}}]</a> /
% base = smstr(relroot)
% for c in dirname.split('/'):
% if not c.raw: continue
% if not c.raw:
% continue
% end
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
% base += c + '/'
% end
<a href="">{{!fname.html}}</a>
</h3>
% if has_colorizer():
% 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">

View File

@@ -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>
@@ -55,7 +55,7 @@
<hr/>
% if has_colorizer():
% if can_colorize(c.diff.body):
{{!colorize_diff(c.diff.body)}}
% else:
<pre class="diff-body">

View File

@@ -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>

View File

@@ -27,14 +27,16 @@
<a href="{{relroot}}">[{{repo.branch}}]</a> /
% base = smstr(relroot)
% for c in dirname.split('/'):
% if not c.raw: continue
% if not c.raw:
% continue
% end
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
% base += c + '/'
% end
</h3>
<table class="nice ls">
% key_func = lambda (t, n, s): (0 if t == 'tree' else 1, n.raw)
% key_func = lambda (t, n, s): (t != 'tree', n.raw)
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
<tr class="{{type}}">
% if type == "blob":