Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb36e087c | ||
|
|
722d765973 | ||
|
|
5e75a1e7a1 | ||
|
|
e1349d418c | ||
|
|
5def4c9e01 | ||
|
|
891a944381 | ||
|
|
d7f0e4a265 | ||
|
|
56b0b34930 | ||
|
|
9b21bd6f19 | ||
|
|
c96d0dbea6 | ||
|
|
9c8a6d2408 | ||
|
|
53155e566a | ||
|
|
c648cfb593 | ||
|
|
cacf2ee2cc | ||
|
|
c4e6484bb0 | ||
|
|
88dd6fab76 | ||
|
|
84d628c690 | ||
|
|
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 | ||
|
|
6f5f3c4aa5 | ||
|
|
c72278c97c | ||
|
|
18e8599bfa | ||
|
|
c303c30755 | ||
|
|
9ec2bde5c4 | ||
|
|
36db9cc0ee | ||
|
|
bad8c52ef2 | ||
|
|
62da3ebc08 | ||
|
|
ba3b2132f5 | ||
|
|
1c729578b2 |
15
README
15
README
@@ -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.
|
||||
|
||||
|
||||
223
git-arr
223
git-arr
@@ -5,9 +5,11 @@ git-arr: A git web html generator.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import math
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
import configparser
|
||||
@@ -20,6 +22,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 = {}
|
||||
@@ -33,15 +47,21 @@ def load_config(path):
|
||||
"""
|
||||
defaults = {
|
||||
'tree': 'yes',
|
||||
'rootdiff': 'yes',
|
||||
'desc': '',
|
||||
'recursive': 'no',
|
||||
'prefix': '',
|
||||
'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': 'git_url',
|
||||
'git_url_file': 'cloneurl',
|
||||
'embed_markdown': 'yes',
|
||||
'embed_images': 'no',
|
||||
'ignore': '',
|
||||
'generate_patch': 'yes',
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
@@ -49,34 +69,43 @@ 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'):
|
||||
root = config.get(s, 'path')
|
||||
prefix = config.get(s, 'prefix')
|
||||
|
||||
for path in os.listdir(root):
|
||||
fullpath = find_git_dir(root + '/' + path)
|
||||
if not fullpath:
|
||||
continue
|
||||
|
||||
if os.path.exists(fullpath + '/disable_gitweb'):
|
||||
continue
|
||||
|
||||
if config.has_section(path):
|
||||
section = prefix + path
|
||||
if config.has_section(section):
|
||||
continue
|
||||
|
||||
config.add_section(path)
|
||||
config.add_section(section)
|
||||
for opt, value in config.items(s, raw = True):
|
||||
config.set(path, opt, value)
|
||||
config.set(section, opt, value)
|
||||
|
||||
config.set(path, 'path', fullpath)
|
||||
config.set(path, 'recursive', 'no')
|
||||
config.set(section, 'path', fullpath)
|
||||
config.set(section, 'recursive', 'no')
|
||||
|
||||
# This recursive section is no longer useful.
|
||||
config.remove_section(s)
|
||||
|
||||
for s in config.sections():
|
||||
fullpath = config.get(s, 'path')
|
||||
if config.get(s, 'ignore') and re.search(config.get(s, 'ignore'), s):
|
||||
continue
|
||||
|
||||
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')
|
||||
@@ -88,7 +117,11 @@ 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.generate_patch = config.getboolean(s, 'generate_patch')
|
||||
|
||||
r.info.web_url = config.get(s, 'web_url')
|
||||
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
||||
@@ -100,8 +133,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 +192,15 @@ 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,
|
||||
'is_binary': utils.is_binary,
|
||||
'hexdump': utils.hexdump,
|
||||
'abort': bottle.abort,
|
||||
'smstr': git.smstr,
|
||||
}
|
||||
@@ -163,14 +228,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):
|
||||
@@ -180,8 +238,40 @@ 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>/c/<cid:re:[0-9a-f]{5,40}>.patch')
|
||||
@bottle.view('patch',
|
||||
# Output is text/plain, don't do HTML escaping.
|
||||
template_settings={"noescape": True})
|
||||
def patch(repo, cid):
|
||||
c = repo.commit(cid)
|
||||
if not c:
|
||||
bottle.abort(404, 'Commit not found')
|
||||
|
||||
bottle.response.content_type = 'text/plain; charset=utf8'
|
||||
|
||||
return dict(repo = repo, c=c)
|
||||
|
||||
@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 = ''):
|
||||
@@ -190,39 +280,36 @@ 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):
|
||||
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, only = None):
|
||||
"""Generate static html to the output directory."""
|
||||
def write_to(path, func_or_str, args = (), mtime = None):
|
||||
path = output + '/' + path
|
||||
@@ -290,24 +377,32 @@ def generate(output):
|
||||
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' %
|
||||
(str(r.name), str(bn), oname.raw),
|
||||
tree, (r, bn, oname.url), mtime)
|
||||
|
||||
# Always generate the index, to keep the "last updated" time fresh.
|
||||
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):
|
||||
rs = sorted(repos.values(), key = lambda r: r.name)
|
||||
if only:
|
||||
rs = [r for r in rs if r.name in only]
|
||||
|
||||
for r in rs:
|
||||
write_to('r/%s/index.html' % r.name, summary(r))
|
||||
for bn in r.branch_names():
|
||||
commit_count = 0
|
||||
@@ -316,11 +411,13 @@ def generate(output):
|
||||
for cid in commit_ids:
|
||||
write_to('r/%s/c/%s/index.html' % (r.name, cid),
|
||||
commit, (r, cid))
|
||||
if r.info.generate_patch:
|
||||
write_to('r/%s/c/%s.patch' % (r.name, cid), patch, (r, cid))
|
||||
commit_count += 1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -346,7 +443,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 +456,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 +465,19 @@ 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 ]
|
||||
|
||||
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, only = opts.only)
|
||||
else:
|
||||
parser.error('Unknown action %s' % args[0])
|
||||
|
||||
|
||||
93
git.py
93
git.py
@@ -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, raw = False):
|
||||
"""Invokes git with the given parameters.
|
||||
|
||||
This function invokes git with the given parameters, and returns a
|
||||
@@ -49,14 +49,23 @@ 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()
|
||||
|
||||
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:
|
||||
@@ -75,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)
|
||||
@@ -90,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
|
||||
@@ -109,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):
|
||||
@@ -189,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()
|
||||
|
||||
@@ -201,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)
|
||||
|
||||
@@ -233,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')
|
||||
@@ -245,6 +255,7 @@ class Repo:
|
||||
cmd.max_count = limit
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg('--')
|
||||
|
||||
for l in cmd.run():
|
||||
yield l.rstrip('\n')
|
||||
@@ -265,6 +276,7 @@ class Repo:
|
||||
cmd.header = None
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg('--')
|
||||
|
||||
info_buffer = ''
|
||||
count = 0
|
||||
@@ -293,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.
|
||||
|
||||
@@ -313,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')
|
||||
@@ -335,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):
|
||||
@@ -391,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)
|
||||
@@ -426,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] == '-':
|
||||
@@ -500,7 +525,10 @@ class Tree:
|
||||
cmd.t = None
|
||||
|
||||
cmd.arg(self.ref)
|
||||
cmd.arg(path)
|
||||
if not path:
|
||||
cmd.arg(".")
|
||||
else:
|
||||
cmd.arg(path)
|
||||
|
||||
for l in cmd.run():
|
||||
_mode, otype, _oid, size, name = l.split(None, 4)
|
||||
@@ -520,3 +548,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
|
||||
|
||||
14
hooks/README
Normal file
14
hooks/README
Normal 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
58
hooks/post-receive
Executable 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
|
||||
|
||||
34
sample.conf
34
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
|
||||
@@ -38,8 +48,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
|
||||
@@ -48,6 +58,22 @@ 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
|
||||
|
||||
# Ignore repositories that match this regular expression.
|
||||
# Generally used with recursive = yes, to ignore repeated repositories (for
|
||||
# example, if using symlinks).
|
||||
# For ignoring specific repositories, putting a "disable_gitweb" is a much
|
||||
# better alternative.
|
||||
# Default: empty (don't ignore)
|
||||
#ignore = \.git$
|
||||
|
||||
# Another repository, we don't generate a tree for it because it's too big.
|
||||
[linux]
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: small;
|
||||
padding: 0 1em 1em 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: x-large;
|
||||
background: #ddd;
|
||||
padding: 0.3em;
|
||||
}
|
||||
@@ -49,13 +47,11 @@ a.explicit:hover, a.explicit:active {
|
||||
/* Normal table, for listing things like repositories, branches, etc. */
|
||||
table.nice {
|
||||
text-align: left;
|
||||
font-size: small;
|
||||
}
|
||||
table.nice td {
|
||||
padding: 0.15em 0.5em;
|
||||
}
|
||||
table.nice td.links {
|
||||
font-size: smaller;
|
||||
}
|
||||
table.nice td.main {
|
||||
min-width: 10em;
|
||||
@@ -69,8 +65,11 @@ table.commits td.date {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
table.commits td.subject {
|
||||
min-width: 32em;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
table.commits td.subject {
|
||||
min-width: 32em;
|
||||
}
|
||||
}
|
||||
table.commits td.author {
|
||||
color: gray;
|
||||
@@ -100,6 +99,37 @@ span.tag {
|
||||
background-color: #ffff88;
|
||||
}
|
||||
|
||||
/* Projects table */
|
||||
table.projects td.name a {
|
||||
color: #038;
|
||||
}
|
||||
|
||||
/* 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: smaller;
|
||||
}
|
||||
|
||||
span.age-band0 {
|
||||
color: darkgreen;
|
||||
}
|
||||
|
||||
span.age-band1 {
|
||||
color: green;
|
||||
}
|
||||
|
||||
span.age-band2 {
|
||||
color: seagreen;
|
||||
}
|
||||
|
||||
/* Toggable titles */
|
||||
div.toggable-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
/* Commit message and diff. */
|
||||
pre.commit-message {
|
||||
font-size: large;
|
||||
@@ -107,7 +137,9 @@ pre.commit-message {
|
||||
}
|
||||
pre.diff-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
font-size: medium;
|
||||
}
|
||||
table.changed-files {
|
||||
font-family: monospace;
|
||||
}
|
||||
table.changed-files span.lines-added {
|
||||
color: green;
|
||||
@@ -126,8 +158,14 @@ div.paginate span.inactive {
|
||||
}
|
||||
|
||||
/* Directory listing. */
|
||||
table.ls td.name {
|
||||
min-width: 20em;
|
||||
@media (min-width: 600px) {
|
||||
table.ls td.name {
|
||||
min-width: 20em;
|
||||
}
|
||||
}
|
||||
table.ls {
|
||||
font-family: monospace;
|
||||
font-size: larger;
|
||||
}
|
||||
table.ls tr.blob td.size {
|
||||
color: gray;
|
||||
@@ -136,18 +174,35 @@ table.ls tr.blob td.size {
|
||||
/* Blob. */
|
||||
pre.blob-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
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;
|
||||
font-size: larger; /* must match div.source_code */
|
||||
}
|
||||
div.linenodiv a {
|
||||
color: gray;
|
||||
font-size: medium;
|
||||
}
|
||||
div.source_code {
|
||||
background: inherit;
|
||||
font-size: medium;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
/* Repository information table. */
|
||||
@@ -156,6 +211,10 @@ table.repo_info tr:hover {
|
||||
}
|
||||
table.repo_info td.category {
|
||||
font-weight: bold;
|
||||
/* So we can copy-paste rows and preserve spaces, useful for the row:
|
||||
* git clone | url
|
||||
*/
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
table.repo_info td {
|
||||
vertical-align: top;
|
||||
@@ -166,3 +225,20 @@ span.ctrlchr {
|
||||
padding: 0 0.2ex 0 0.1ex;
|
||||
margin: 0 0.2ex 0 0.1ex;
|
||||
}
|
||||
|
||||
/*
|
||||
* Markdown overrides
|
||||
*/
|
||||
|
||||
/* Colored links (same as explicit links above) */
|
||||
div.markdown a {
|
||||
color: #038;
|
||||
}
|
||||
div.markdown a:hover, div.markdown a:active {
|
||||
color: #880000;
|
||||
}
|
||||
|
||||
/* Restrict max width for readability */
|
||||
div.markdown {
|
||||
max-width: 55em;
|
||||
}
|
||||
|
||||
73
static/git-arr.js
Normal file
73
static/git-arr.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/* 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
var e = document.getElementById(id);
|
||||
|
||||
if (e.style.display == "") {
|
||||
e.style.display = "none"
|
||||
} else if (e.style.display == "none") {
|
||||
e.style.display = ""
|
||||
}
|
||||
}
|
||||
132
utils.py
132
utils.py
@@ -12,14 +12,62 @@ try:
|
||||
except ImportError:
|
||||
pygments = None
|
||||
|
||||
try:
|
||||
import markdown
|
||||
import markdown.treeprocessors
|
||||
except ImportError:
|
||||
markdown = None
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import string
|
||||
import os.path
|
||||
|
||||
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 +78,88 @@ 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):
|
||||
extensions = [
|
||||
"markdown.extensions.fenced_code",
|
||||
"markdown.extensions.tables",
|
||||
RewriteLocalLinksExtension(),
|
||||
]
|
||||
return markdown.markdown(s, extensions = extensions)
|
||||
|
||||
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:]
|
||||
|
||||
|
||||
if markdown:
|
||||
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
||||
"""Rewrites relative links to files, to match git-arr's links.
|
||||
|
||||
A link of "[example](a/file.md)" will be rewritten such that it links to
|
||||
"a/f=file.md.html".
|
||||
|
||||
Note that we're already assuming a degree of sanity in the HTML, so we
|
||||
don't re-check that the path is reasonable.
|
||||
"""
|
||||
def run(self, root):
|
||||
for child in root:
|
||||
if child.tag == "a":
|
||||
self.rewrite_href(child)
|
||||
|
||||
# Continue recursively.
|
||||
self.run(child)
|
||||
|
||||
def rewrite_href(self, tag):
|
||||
"""Rewrite an <a>'s href."""
|
||||
target = tag.get("href")
|
||||
if not target:
|
||||
return
|
||||
if "://" in target or target.startswith("/"):
|
||||
return
|
||||
|
||||
head, tail = os.path.split(target)
|
||||
new_target = os.path.join(head, "f=" + tail + ".html")
|
||||
tag.set("href", new_target)
|
||||
|
||||
|
||||
class RewriteLocalLinksExtension(markdown.Extension):
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
md.treeprocessors.add(
|
||||
"RewriteLocalLinks", RewriteLocalLinks(), "_end")
|
||||
|
||||
|
||||
@@ -1,46 +1,86 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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"
|
||||
href="{{relroot}}../../../../../static/syntax.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<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 has_colorizer():
|
||||
{{!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):
|
||||
<div class="markdown">
|
||||
{{!markdown_blob(blob.utf8_content)}}
|
||||
</div>
|
||||
% elif can_colorize(blob.utf8_content):
|
||||
{{!colorize_blob(fname.unicode, blob.utf8_content)}}
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
{{blob}}
|
||||
{{blob.utf8_content}}
|
||||
</pre>
|
||||
% end
|
||||
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</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>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
% end
|
||||
% end
|
||||
|
||||
<table class="nice commits">
|
||||
<table class="nice commits" id="commits">
|
||||
|
||||
% refs = repo.refs()
|
||||
% if not defined("commits"):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git » {{repo.name}} » commit {{c.id[:7]}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/syntax.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body class="commit">
|
||||
@@ -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>
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<hr/>
|
||||
|
||||
% if has_colorizer():
|
||||
% if can_colorize(c.diff.body):
|
||||
{{!colorize_diff(c.diff.body)}}
|
||||
% else:
|
||||
<pre class="diff-body">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<script async src="static/git-arr.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="index">
|
||||
<body class="index" onload="replace_timestamps()">
|
||||
<h1>git</h1>
|
||||
|
||||
<table class="nice">
|
||||
<table class="nice projects">
|
||||
<tr>
|
||||
<th>project</th>
|
||||
<th>description</th>
|
||||
@@ -18,8 +19,9 @@
|
||||
|
||||
% for repo in sorted(repos.values(), key = lambda r: r.name):
|
||||
<tr>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
||||
<td class="name"><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||
<td class="desc"><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>
|
||||
|
||||
8
views/patch.tpl
Normal file
8
views/patch.tpl
Normal file
@@ -0,0 +1,8 @@
|
||||
From: {{c.author_name}} <{{c.author_email}}>
|
||||
Date: {{c.author_date}}
|
||||
Subject: {{c.subject}}
|
||||
|
||||
{{c.body.strip()}}
|
||||
---
|
||||
|
||||
{{c.diff.body}}
|
||||
@@ -1,10 +1,11 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git » {{repo.name}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<script async src="../../static/git-arr.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="summary">
|
||||
@@ -25,7 +26,7 @@
|
||||
% end
|
||||
% if repo.info.git_url:
|
||||
<tr>
|
||||
<td class="category">repository</td>
|
||||
<td class="category">git clone </td>
|
||||
<td>{{! '<br/>'.join(repo.info.git_url.split())}}</td>
|
||||
</tr>
|
||||
% end
|
||||
@@ -35,19 +36,25 @@
|
||||
% end
|
||||
|
||||
% if "master" in repo.branch_names():
|
||||
<div class="toggable-title" onclick="toggle('commits')">
|
||||
<a href="b/master/">commits (master)</a>
|
||||
</div>
|
||||
% kwargs = dict(repo = repo, start_ref = "refs/heads/master",
|
||||
% limit = repo.info.commits_in_summary,
|
||||
% shorten = shorten, repo_root = ".", offset = 0)
|
||||
% include commit-list **kwargs
|
||||
<hr/>
|
||||
<div class="toggable-title" onclick="toggle('ls')">
|
||||
<a href="b/master/t/">tree (master)</a>
|
||||
</div>
|
||||
% kwargs = dict(repo = repo, tree=repo.tree("master"),
|
||||
% treeroot="b/master/t", dirname=smstr.from_url(""))
|
||||
% include tree-list **kwargs
|
||||
<hr/>
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>branches</th>
|
||||
</tr>
|
||||
|
||||
<div class="toggable-title" onclick="toggle('branches')">branches</div>
|
||||
<table class="nice toggable" id="branches">
|
||||
% for b in repo.branch_names():
|
||||
<tr>
|
||||
<td class="main"><a href="b/{{b}}/">{{b}}</a></td>
|
||||
@@ -63,11 +70,8 @@
|
||||
|
||||
% tags = list(repo.tags())
|
||||
% if tags:
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>tags</th>
|
||||
</tr>
|
||||
|
||||
<div class="toggable-title" onclick="toggle('tags')">tags</div>
|
||||
<table class="nice toggable" id="tags">
|
||||
% for name, obj_id in tags:
|
||||
<tr>
|
||||
<td><a href="c/{{obj_id}}/">{{name}}</a></td>
|
||||
|
||||
16
views/tree-list.html
Normal file
16
views/tree-list.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<table class="nice toggable ls" id="ls">
|
||||
% 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":
|
||||
<td class="name"><a href="{{treeroot}}/f={{name.url}}.html">
|
||||
{{!name.html}}</a></td>
|
||||
<td class="size">{{size}}</td>
|
||||
% elif type == "tree":
|
||||
<td class="name">
|
||||
<a class="explicit" href="{{treeroot}}/{{name.url}}/">
|
||||
{{!name.html}}/</a></td>
|
||||
% end
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
@@ -1,54 +1,43 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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 if dirname.unicode else '/'}}</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<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)
|
||||
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
||||
<tr class="{{type}}">
|
||||
% if type == "blob":
|
||||
<td class="name"><a href="./f={{name.url}}.html">
|
||||
{{!name.html}}</a></td>
|
||||
<td class="size">{{size}}</td>
|
||||
% elif type == "tree":
|
||||
<td class="name">
|
||||
<a class="explicit" href="./{{name.url}}/">
|
||||
{{!name.html}}/</a></td>
|
||||
% end
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
% kwargs = dict(repo = repo, tree=tree, treeroot=".")
|
||||
% include tree-list **kwargs
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user