Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5568fd50c2 | ||
|
|
89a637660f | ||
|
|
37e731fc2e | ||
|
|
e6099cf272 | ||
|
|
46640c68b9 | ||
|
|
c91beccdb0 | ||
|
|
6f3942ce38 | ||
|
|
09c2f33f5a | ||
|
|
58037e57c5 | ||
|
|
50c004f8a5 | ||
|
|
1d79988228 | ||
|
|
0ba89d75e6 | ||
|
|
6b83e32bc1 | ||
|
|
43f4132bf1 | ||
|
|
66afd72d6d | ||
|
|
bb9bad89d1 | ||
|
|
56fcfd0278 | ||
|
|
e930f9e4f7 | ||
|
|
93b161c23e | ||
|
|
7f2f67629f | ||
|
|
ac105c8383 | ||
|
|
bebc7fa3f0 | ||
|
|
9ef78aaffd | ||
|
|
d7604dab4d | ||
|
|
aaf2968538 | ||
|
|
420afd3206 | ||
|
|
605421f2d6 | ||
|
|
df00293a7c | ||
|
|
7898b2becd | ||
|
|
47d500715a | ||
|
|
eb7cadd64f | ||
|
|
48a00cb460 | ||
|
|
2f65291ef1 | ||
|
|
f6a75820e8 | ||
|
|
e49c69da2e | ||
|
|
6764bfcfd6 | ||
|
|
54026b7585 | ||
|
|
a42d7da6a4 | ||
|
|
21522f8a3a | ||
|
|
f62ca211eb | ||
|
|
d3bf98ea00 |
97
git-arr
97
git-arr
@@ -46,15 +46,18 @@ def load_config(path):
|
||||
"""
|
||||
defaults = {
|
||||
'tree': 'yes',
|
||||
'rootdiff': 'yes',
|
||||
'desc': '',
|
||||
'recursive': 'no',
|
||||
'commits_in_summary': '10',
|
||||
'commits_per_page': '50',
|
||||
'max_pages': '5',
|
||||
'max_pages': '250',
|
||||
'web_url': '',
|
||||
'web_url_file': 'web_url',
|
||||
'git_url': '',
|
||||
'git_url_file': 'cloneurl',
|
||||
'embed_markdown': 'yes',
|
||||
'embed_images': 'no',
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
@@ -103,7 +106,10 @@ def load_config(path):
|
||||
r.info.commits_in_summary = config.getint(s, 'commits_in_summary')
|
||||
r.info.commits_per_page = config.getint(s, 'commits_per_page')
|
||||
r.info.max_pages = config.getint(s, 'max_pages')
|
||||
if r.info.max_pages <= 0:
|
||||
r.info.max_pages = sys.maxint
|
||||
r.info.generate_tree = config.getboolean(s, 'tree')
|
||||
r.info.root_diff = config.getboolean(s, 'rootdiff')
|
||||
|
||||
r.info.web_url = config.get(s, 'web_url')
|
||||
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
||||
@@ -115,6 +121,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 +183,12 @@ 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,
|
||||
'is_binary': utils.is_binary,
|
||||
'hexdump': utils.hexdump,
|
||||
'abort': bottle.abort,
|
||||
'smstr': git.smstr,
|
||||
}
|
||||
@@ -201,14 +216,7 @@ def index():
|
||||
def summary(repo):
|
||||
return dict(repo = repo)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/<offset:int>.html')
|
||||
@bottle.view('branch')
|
||||
@with_utils
|
||||
def branch(repo, bname, offset = 0):
|
||||
return dict(repo = repo.new_in_branch(bname), offset = offset)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-z]{5,40}>/')
|
||||
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/')
|
||||
@bottle.view('commit')
|
||||
@with_utils
|
||||
def commit(repo, cid):
|
||||
@@ -218,8 +226,27 @@ def commit(repo, cid):
|
||||
|
||||
return dict(repo = repo, c=c)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/<dirname:path>/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html')
|
||||
@bottle.view('blob')
|
||||
@with_utils
|
||||
def blob(repo, bname, fname, dirname = ''):
|
||||
if dirname and not dirname.endswith('/'):
|
||||
dirname = dirname + '/'
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
fname = git.smstr.from_url(fname)
|
||||
path = dirname.raw + fname.raw
|
||||
|
||||
content = repo.blob(path, bname)
|
||||
if content is None:
|
||||
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
||||
|
||||
return dict(repo = repo, branch = bname, dirname = dirname, fname = fname,
|
||||
blob = content)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/')
|
||||
@bottle.view('tree')
|
||||
@with_utils
|
||||
def tree(repo, bname, dirname = ''):
|
||||
@@ -228,28 +255,15 @@ def tree(repo, bname, dirname = ''):
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
|
||||
r = repo.new_in_branch(bname)
|
||||
return dict(repo = r, tree = r.tree(), dirname = dirname)
|
||||
return dict(repo = repo, branch = bname, tree = repo.tree(bname),
|
||||
dirname = dirname)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/f=<fname:path>.html')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/<dirname:path>/f=<fname:path>.html')
|
||||
@bottle.view('blob')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/<offset:int>.html')
|
||||
@bottle.view('branch')
|
||||
@with_utils
|
||||
def blob(repo, bname, fname, dirname = ''):
|
||||
r = repo.new_in_branch(bname)
|
||||
|
||||
if dirname and not dirname.endswith('/'):
|
||||
dirname = dirname + '/'
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
fname = git.smstr.from_url(fname)
|
||||
path = dirname.raw + fname.raw
|
||||
|
||||
content = r.blob(path)
|
||||
if content is None:
|
||||
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
||||
|
||||
return dict(repo = r, dirname = dirname, fname = fname, blob = content)
|
||||
def branch(repo, bname, offset = 0):
|
||||
return dict(repo = repo, branch = bname, offset = offset)
|
||||
|
||||
@bottle.route('/static/<path:path>')
|
||||
def static(path):
|
||||
@@ -260,6 +274,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):
|
||||
@@ -328,8 +352,9 @@ def generate(output, skip_index = False):
|
||||
dirname = git.smstr(os.path.dirname(oname.raw))
|
||||
fname = git.smstr(os.path.basename(oname.raw))
|
||||
write_to(
|
||||
'r/%s/b/%s/t/%s/f=%s.html' %
|
||||
(str(r.name), str(bn), dirname.raw, fname.raw),
|
||||
'r/%s/b/%s/t/%s%sf=%s.html' %
|
||||
(str(r.name), str(bn),
|
||||
dirname.raw, '/' if dirname.raw else '', fname.raw),
|
||||
blob, (r, bn, fname.url, dirname.url), mtime)
|
||||
else:
|
||||
write_to('r/%s/b/%s/t/%s/index.html' %
|
||||
@@ -343,6 +368,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)
|
||||
|
||||
@@ -359,7 +386,7 @@ def generate(output, skip_index = False):
|
||||
|
||||
# To avoid regenerating files that have not changed, we will
|
||||
# instruct write_to() to set their mtime to the branch's committer
|
||||
# date, and then compare against it to decide wether or not to
|
||||
# date, and then compare against it to decide whether or not to
|
||||
# write.
|
||||
branch_mtime = r.commit(bn).committer_date.epoch
|
||||
|
||||
@@ -385,7 +412,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
|
||||
|
||||
78
git.py
78
git.py
@@ -41,7 +41,7 @@ class EncodeWrapper:
|
||||
return s.decode(self.encoding, errors = self.errors)
|
||||
|
||||
|
||||
def run_git(repo_path, params, stdin = None, silent_stderr = False):
|
||||
def run_git(repo_path, params, stdin = None, silent_stderr = False, raw = False):
|
||||
"""Invokes git with the given parameters.
|
||||
|
||||
This function invokes git with the given parameters, and returns a
|
||||
@@ -63,6 +63,9 @@ def run_git(repo_path, params, stdin = None, silent_stderr = False):
|
||||
p.stdin.write(stdin)
|
||||
p.stdin.close()
|
||||
|
||||
if raw:
|
||||
return p.stdout
|
||||
|
||||
# We need to wrap stdout if we want to decode it as utf8, subprocess
|
||||
# doesn't support us telling it the encoding.
|
||||
if sys.version_info.major == 3:
|
||||
@@ -81,6 +84,7 @@ class GitCommand (object):
|
||||
self._args = list(args)
|
||||
self._kwargs = {}
|
||||
self._stdin_buf = None
|
||||
self._raw = False
|
||||
self._override = False
|
||||
for k, v in kwargs:
|
||||
self.__setattr__(k, v)
|
||||
@@ -96,6 +100,12 @@ class GitCommand (object):
|
||||
"""Adds an argument."""
|
||||
self._args.append(a)
|
||||
|
||||
def raw(self, b):
|
||||
"""Request raw rather than utf8-encoded command output."""
|
||||
self._override = True
|
||||
self._raw = b
|
||||
self._override = False
|
||||
|
||||
def stdin(self, s):
|
||||
"""Sets the contents we will send in stdin."""
|
||||
self._override = True
|
||||
@@ -115,7 +125,7 @@ class GitCommand (object):
|
||||
|
||||
params.extend(self._args)
|
||||
|
||||
return run_git(self._path, params, self._stdin_buf)
|
||||
return run_git(self._path, params, self._stdin_buf, raw = self._raw)
|
||||
|
||||
|
||||
class SimpleNamespace (object):
|
||||
@@ -195,11 +205,8 @@ def unquote(s):
|
||||
class Repo:
|
||||
"""A git repository."""
|
||||
|
||||
def __init__(self, path, branch = None, name = None, info = None):
|
||||
def __init__(self, path, name = None, info = None):
|
||||
self.path = path
|
||||
self.branch = branch
|
||||
|
||||
# We don't need these, but provide them for the users' convenience.
|
||||
self.name = name
|
||||
self.info = info or SimpleNamespace()
|
||||
|
||||
@@ -207,11 +214,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)
|
||||
|
||||
@@ -239,11 +248,6 @@ class Repo:
|
||||
"""Get the names of the tags."""
|
||||
return ( name for name, _ in self.tags() )
|
||||
|
||||
def new_in_branch(self, branch):
|
||||
"""Returns a new Repo, but on the specific branch."""
|
||||
return Repo(self.path, branch = branch, name = self.name,
|
||||
info = self.info)
|
||||
|
||||
def commit_ids(self, ref, limit = None):
|
||||
"""Generate commit ids."""
|
||||
cmd = self.cmd('rev-list')
|
||||
@@ -251,6 +255,7 @@ class Repo:
|
||||
cmd.max_count = limit
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg('--')
|
||||
|
||||
for l in cmd.run():
|
||||
yield l.rstrip('\n')
|
||||
@@ -271,6 +276,7 @@ class Repo:
|
||||
cmd.header = None
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg('--')
|
||||
|
||||
info_buffer = ''
|
||||
count = 0
|
||||
@@ -299,6 +305,8 @@ class Repo:
|
||||
cmd.patch = None
|
||||
cmd.numstat = None
|
||||
cmd.find_renames = None
|
||||
if (self.info.root_diff):
|
||||
cmd.root = None
|
||||
# Note we intentionally do not use -z, as the filename is just for
|
||||
# reference, and it is safer to let git do the escaping.
|
||||
|
||||
@@ -319,18 +327,15 @@ class Repo:
|
||||
|
||||
return r
|
||||
|
||||
def tree(self, ref = None):
|
||||
def tree(self, ref):
|
||||
"""Returns a Tree instance for the given ref."""
|
||||
if not ref:
|
||||
ref = self.branch
|
||||
return Tree(self, ref)
|
||||
|
||||
def blob(self, path, ref = None):
|
||||
"""Returns the contents of the given path."""
|
||||
if not ref:
|
||||
ref = self.branch
|
||||
def blob(self, path, ref):
|
||||
"""Returns a Blob instance for the given path."""
|
||||
cmd = self.cmd('cat-file')
|
||||
cmd.batch = None
|
||||
cmd.raw(True)
|
||||
cmd.batch = '%(objectsize)'
|
||||
|
||||
if isinstance(ref, unicode):
|
||||
ref = ref.encode('utf8')
|
||||
@@ -341,7 +346,16 @@ class Repo:
|
||||
if not head or head.strip().endswith('missing'):
|
||||
return None
|
||||
|
||||
return out.read()
|
||||
return Blob(out.read()[:int(head)])
|
||||
|
||||
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):
|
||||
@@ -397,7 +411,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)
|
||||
@@ -432,7 +451,7 @@ class Date:
|
||||
def __init__(self, epoch, tz):
|
||||
self.epoch = int(epoch)
|
||||
self.tz = tz
|
||||
self.utc = datetime.datetime.fromtimestamp(self.epoch)
|
||||
self.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
||||
|
||||
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
||||
if tz[0] == '-':
|
||||
@@ -526,3 +545,16 @@ class Tree:
|
||||
# manipulate otherwise.
|
||||
yield otype, smstr(name), size
|
||||
|
||||
|
||||
class Blob:
|
||||
"""A git blob."""
|
||||
|
||||
def __init__(self, raw_content):
|
||||
self.raw_content = raw_content
|
||||
self._utf8_content = None
|
||||
|
||||
@property
|
||||
def utf8_content(self):
|
||||
if not self._utf8_content:
|
||||
self._utf8_content = self.raw_content.decode('utf8', 'replace')
|
||||
return self._utf8_content
|
||||
|
||||
23
sample.conf
23
sample.conf
@@ -11,6 +11,15 @@ path = /srv/git/repo/
|
||||
# Useful to disable an expensive operation in very large repositories.
|
||||
#tree = yes
|
||||
|
||||
# Show a "creation event" diff for a root commit? (optional)
|
||||
# For projects placed under revision control from inception, the root commit
|
||||
# diff is often meaningful. However, in cases when a well established, large
|
||||
# project is placed under revision control belatedly, the root commit may
|
||||
# represent a lump import of the entire project, in which case such a
|
||||
# "creation event" diff would likely be considered meaningless noise.
|
||||
# Default: yes
|
||||
#rootdiff = yes
|
||||
|
||||
# How many commits to show in the summary page (optional).
|
||||
#commits_in_summary = 10
|
||||
|
||||
@@ -19,8 +28,9 @@ path = /srv/git/repo/
|
||||
|
||||
# Maximum number of per-branch pages for static generation (optional).
|
||||
# When generating static html, this is the maximum number of pages we will
|
||||
# generate for each branch's commit listings.
|
||||
#max_pages = 5
|
||||
# generate for each branch's commit listings. Zero (0) means unlimited.
|
||||
# Default: 250
|
||||
#max_pages = 250
|
||||
|
||||
# Project website (optional).
|
||||
# URL to the project's website. %(name)s will be replaced with the current
|
||||
@@ -48,6 +58,15 @@ path = /srv/git/repo/
|
||||
# excluded.
|
||||
#recursive = no
|
||||
|
||||
# Render Markdown blobs (*.md) formatted rather than as raw text? (optional)
|
||||
# Requires 'markdown' module.
|
||||
# Default: yes
|
||||
#embed_markdown = yes
|
||||
|
||||
# Render image blobs graphically rather than as raw binary data? (optional)
|
||||
# Default: no
|
||||
#embed_images = no
|
||||
|
||||
|
||||
# Another repository, we don't generate a tree for it because it's too big.
|
||||
[linux]
|
||||
|
||||
@@ -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;
|
||||
@@ -139,6 +159,22 @@ pre.blob-body {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
table.blob-binary pre {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.blob-binary .offset {
|
||||
text-align: right;
|
||||
font-size: x-small;
|
||||
color: darkgray;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
table.blob-binary tr.etc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Pygments overrides. */
|
||||
div.linenodiv {
|
||||
padding-right: 0.5em;
|
||||
|
||||
63
static/git-arr.js
Normal file
63
static/git-arr.js
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
52
utils.py
52
utils.py
@@ -12,6 +12,14 @@ try:
|
||||
except ImportError:
|
||||
pygments = None
|
||||
|
||||
try:
|
||||
import markdown
|
||||
except ImportError:
|
||||
markdown = None
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import string
|
||||
|
||||
def shorten(s, width = 60):
|
||||
if len(s) < 60:
|
||||
@@ -41,6 +49,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',
|
||||
@@ -64,7 +90,31 @@ def colorize_blob(fname, s):
|
||||
|
||||
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(fname, image_data):
|
||||
mimetype = mimetypes.guess_type(fname)[0]
|
||||
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format( \
|
||||
mimetype, base64.b64encode(image_data))
|
||||
|
||||
def is_binary(s):
|
||||
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
||||
return '\0' in s[:8192]
|
||||
|
||||
def hexdump(s):
|
||||
graph = string.ascii_letters + string.digits + string.punctuation + ' '
|
||||
offset = 0
|
||||
while s:
|
||||
t = s[:16]
|
||||
hexvals = ['%.2x' % ord(c) for c in t]
|
||||
text = ''.join(c if c in graph else '.' for c in t)
|
||||
yield offset, ' '.join(hexvals[:8]), ' '.join(hexvals[8:]), text
|
||||
offset += 16
|
||||
s = s[16:]
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% reltree = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.branch}} » {{dirname.unicode}}/{{fname.unicode}}</title>
|
||||
{{branch}} » {{dirname.unicode}}/{{fname.unicode}}</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
@@ -21,26 +22,63 @@
|
||||
<body class="tree">
|
||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
||||
<a href="{{relroot}}">tree</a>
|
||||
<a href="{{reltree}}../">{{branch}}</a> »
|
||||
<a href="{{reltree}}">tree</a>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||
% base = smstr(reltree)
|
||||
% 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 can_colorize(blob):
|
||||
{{!colorize_blob(fname.unicode, blob)}}
|
||||
% if len(blob.raw_content) == 0:
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<td>empty — 0 bytes</td>
|
||||
</tr>
|
||||
</table>
|
||||
% elif can_embed_image(repo, fname.unicode):
|
||||
{{!embed_image_blob(fname.raw, blob.raw_content)}}
|
||||
% elif is_binary(blob.raw_content):
|
||||
<table class="nice blob-binary">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
binary — {{'{:,}'.format(len(blob.raw_content))}} bytes
|
||||
</td>
|
||||
</tr>
|
||||
% lim = 256
|
||||
% for offset, hex1, hex2, text in hexdump(blob.raw_content[:lim]):
|
||||
<tr>
|
||||
<td class="offset">{{offset}}</td>
|
||||
<td><pre>{{hex1}}</pre></td>
|
||||
<td><pre>{{hex2}}</pre></td>
|
||||
<td><pre>{{text}}</pre></td>
|
||||
</tr>
|
||||
% end
|
||||
% if lim < len(blob.raw_content):
|
||||
<tr class="etc">
|
||||
<td></td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
% elif can_markdown(repo, fname.unicode):
|
||||
{{!markdown_blob(blob.utf8_content)}}
|
||||
% elif can_colorize(blob.utf8_content):
|
||||
{{!colorize_blob(fname.unicode, blob.utf8_content)}}
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
{{blob}}
|
||||
{{blob.utf8_content}}
|
||||
</pre>
|
||||
% end
|
||||
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>git » {{repo.name}} » {{repo.branch}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
||||
|
||||
% relroot = '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} » {{branch}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{relroot}}../../../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
</head>
|
||||
|
||||
<body class="branch">
|
||||
<h1><a href="../../../../">git</a> »
|
||||
<a href="../../">{{repo.name}}</a> »
|
||||
<a href="./">{{repo.branch}}</a>
|
||||
<h1><a href="{{relroot}}../../../../">git</a> »
|
||||
<a href="{{relroot}}../../">{{repo.name}}</a> »
|
||||
<a href="./">{{branch}}</a>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
<a class="explicit" href="t/">Browse current source tree</a>
|
||||
</p>
|
||||
|
||||
% commits = repo.commits("refs/heads/" + repo.branch,
|
||||
% limit = repo.info.commits_per_page,
|
||||
% commits = repo.commits("refs/heads/" + branch,
|
||||
% limit = repo.info.commits_per_page + 1,
|
||||
% offset = repo.info.commits_per_page * offset)
|
||||
% commits = list(commits)
|
||||
|
||||
@@ -26,16 +29,21 @@
|
||||
% abort(404, "No more commits")
|
||||
% end
|
||||
|
||||
% more = len(commits) > repo.info.commits_per_page
|
||||
% if more:
|
||||
% commits = commits[:-1]
|
||||
% end
|
||||
% more = more and offset + 1 < repo.info.max_pages
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
% include paginate more = more, offset = offset
|
||||
|
||||
% kwargs = dict(repo=repo, commits=commits,
|
||||
% shorten=shorten, repo_root="../..")
|
||||
% shorten=shorten, repo_root=relroot + "../..")
|
||||
% include commit-list **kwargs
|
||||
|
||||
<p/>
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
% include paginate more = more, offset = offset
|
||||
|
||||
</body>
|
||||
</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"><{{c.author_email}}></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"><{{c.committer_email}}></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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<span class="inactive">← prev</span>
|
||||
% end
|
||||
<span class="sep">|</span>
|
||||
% if nelem >= max_per_page:
|
||||
% if more:
|
||||
<a href="{{offset + 1}}.html">next →</a>
|
||||
% else:
|
||||
<span class="inactive">next →</span>
|
||||
|
||||
@@ -39,9 +39,8 @@
|
||||
% limit = repo.info.commits_in_summary,
|
||||
% shorten = shorten, repo_root = ".", offset = 0)
|
||||
% include commit-list **kwargs
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
% end
|
||||
|
||||
<table class="nice">
|
||||
<tr>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% reltree = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.branch}} » {{dirname.unicode}}</title>
|
||||
{{branch}} » {{dirname.unicode}}</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
@@ -19,22 +20,24 @@
|
||||
<body class="tree">
|
||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
||||
<a href="{{relroot}}">tree</a>
|
||||
<a href="{{reltree}}../">{{branch}}</a> »
|
||||
<a href="{{reltree}}">tree</a>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||
% base = smstr(reltree)
|
||||
% 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":
|
||||
|
||||
Reference in New Issue
Block a user