Initial commit
Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>
This commit is contained in:
commit
80ef0017d4
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
.*.swp
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
git-arr is under the MIT licence, which is reproduced below (taken from
|
||||
http://opensource.org/licenses/MIT).
|
||||
|
||||
-----
|
||||
|
||||
Copyright (c) 2012 Alberto Bertogli
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
49
README
Normal file
49
README
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
git-arr - A git repository browser
|
||||
----------------------------------
|
||||
|
||||
git-arr is a git repository browser that can generate static HTML instead of
|
||||
having to run dynamically.
|
||||
|
||||
It is smaller, with less features and a different set of tradeoffs than
|
||||
other similar software, so if you're looking for a robust and featureful git
|
||||
browser, please look at gitweb or cgit instead.
|
||||
|
||||
However, if you want to generate static HTML at the expense of features, then
|
||||
it's probably going to be useful.
|
||||
|
||||
It's open source under the MIT licence, please see the LICENSE file for more
|
||||
information.
|
||||
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
First, create a configuration file for your repositories. You can start by
|
||||
copying sample.conf, which has the list of the available options.
|
||||
|
||||
Then, to generate the output to "/var/www/git-arr/" directory, run:
|
||||
|
||||
$ ./git-arr --config config.conf generate --output /var/www/git-arr/
|
||||
|
||||
That's it!
|
||||
|
||||
The first time you generate, depending on the size of your repositories, it
|
||||
can take some time. Subsequent runs should take less time, as it is smart
|
||||
enough to only generate what has changed.
|
||||
|
||||
|
||||
You can also use git-arr dynamically, although it's not its intended mode of
|
||||
use, by running:
|
||||
|
||||
$ ./git-arr --config config.conf serve
|
||||
|
||||
That can be useful when making changes to the software itself.
|
||||
|
||||
|
||||
Where to report bugs
|
||||
--------------------
|
||||
|
||||
If you want to report bugs, or have any questions or comments, just let me
|
||||
know at albertito@blitiri.com.ar.
|
||||
|
13
TODO
Normal file
13
TODO
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
In no particular order.
|
||||
|
||||
- Atom/RSS.
|
||||
- Nicer diff:
|
||||
- Better stat section, with nicer handling of filenames. We should switch to
|
||||
--patch-with-raw and parse from that.
|
||||
- Nicer output, don't use pygments but do our own.
|
||||
- Anchors in diff sections so we can link to them.
|
||||
- Short symlinks to commits, with configurable length.
|
||||
- Handle symlinks properly.
|
||||
- "X hours ago" via javascript (only if it's not too ugly).
|
||||
|
390
git-arr
Executable file
390
git-arr
Executable file
@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
git-arr: A git web html generator.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import math
|
||||
import optparse
|
||||
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
import bottle
|
||||
|
||||
import git
|
||||
import utils
|
||||
|
||||
|
||||
# The list of repositories is a global variable for convenience. It will be
|
||||
# populated by load_config().
|
||||
repos = {}
|
||||
|
||||
|
||||
def load_config(path):
|
||||
"""Load the configuration from the given file.
|
||||
|
||||
The "repos" global variable will be filled with the repositories
|
||||
as configured.
|
||||
"""
|
||||
defaults = {
|
||||
'tree': 'yes',
|
||||
'desc': '',
|
||||
'recursive': 'no',
|
||||
'commits_in_summary': '10',
|
||||
'commits_per_page': '50',
|
||||
'max_pages': '5',
|
||||
'web_url': '',
|
||||
'web_url_file': 'web_url',
|
||||
'git_url': '',
|
||||
'git_url_file': 'git_url',
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
config.read(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'):
|
||||
continue
|
||||
|
||||
if os.path.exists(fullpath + '/disable_gitweb'):
|
||||
continue
|
||||
|
||||
if config.has_section(path):
|
||||
continue
|
||||
|
||||
config.add_section(path)
|
||||
for opt, value in config.items(s, raw = True):
|
||||
config.set(path, opt, value)
|
||||
|
||||
config.set(path, 'path', fullpath)
|
||||
config.set(path, 'recursive', 'no')
|
||||
|
||||
# This recursive section is no longer useful.
|
||||
config.remove_section(s)
|
||||
|
||||
for s in config.sections():
|
||||
fullpath = config.get(s, 'path')
|
||||
config.set(s, 'name', s)
|
||||
|
||||
desc = config.get(s, 'desc')
|
||||
if not desc and os.path.exists(fullpath + '/description'):
|
||||
desc = open(fullpath + '/description').read().strip()
|
||||
|
||||
r = git.Repo(fullpath, name = s)
|
||||
r.info.desc = desc
|
||||
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')
|
||||
r.info.generate_tree = config.getboolean(s, 'tree')
|
||||
|
||||
r.info.web_url = config.get(s, 'web_url')
|
||||
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
||||
if not r.info.web_url and os.path.isfile(web_url_file):
|
||||
r.info.web_url = open(web_url_file).read()
|
||||
|
||||
r.info.git_url = config.get(s, 'git_url')
|
||||
git_url_file = fullpath + '/' + config.get(s, 'git_url_file')
|
||||
if not r.info.git_url and os.path.isfile(git_url_file):
|
||||
r.info.git_url = open(git_url_file).read()
|
||||
|
||||
repos[r.name] = r
|
||||
|
||||
|
||||
def repo_filter(unused_conf):
|
||||
"""Bottle route filter for repos."""
|
||||
# TODO: consider allowing /, which is tricky.
|
||||
regexp = r'[\w\.~-]+'
|
||||
|
||||
def to_python(s):
|
||||
"""Return the corresponding Python object."""
|
||||
if s in repos:
|
||||
return repos[s]
|
||||
bottle.abort(404, "Unknown repository")
|
||||
|
||||
def to_url(r):
|
||||
"""Return the corresponding URL string."""
|
||||
return r.name
|
||||
|
||||
return regexp, to_python, to_url
|
||||
|
||||
app = bottle.Bottle()
|
||||
app.router.add_filter('repo', repo_filter)
|
||||
bottle.app.push(app)
|
||||
|
||||
|
||||
def with_utils(f):
|
||||
"""Decorator to add the utilities to the return value.
|
||||
|
||||
Used to wrap functions that return dictionaries which are then passed to
|
||||
templates.
|
||||
"""
|
||||
utilities = {
|
||||
'shorten': utils.shorten,
|
||||
'has_colorizer': utils.has_colorizer,
|
||||
'colorize_diff': utils.colorize_diff,
|
||||
'colorize_blob': utils.colorize_blob,
|
||||
'abort': bottle.abort,
|
||||
'smstr': git.smstr,
|
||||
}
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
"""Wrapped function we will return."""
|
||||
d = f(*args, **kwargs)
|
||||
d.update(utilities)
|
||||
return d
|
||||
|
||||
wrapped.__name__ = f.__name__
|
||||
wrapped.__doc__ = f.__doc__
|
||||
|
||||
return wrapped
|
||||
|
||||
@bottle.route('/')
|
||||
@bottle.view('index')
|
||||
@with_utils
|
||||
def index():
|
||||
return dict(repos = repos)
|
||||
|
||||
@bottle.route('/r/<repo:repo>/')
|
||||
@bottle.view('summary')
|
||||
@with_utils
|
||||
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.view('commit')
|
||||
@with_utils
|
||||
def commit(repo, cid):
|
||||
c = repo.commit(cid)
|
||||
if not c:
|
||||
bottle.abort(404, 'Commit not found')
|
||||
|
||||
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.view('tree')
|
||||
@with_utils
|
||||
def tree(repo, bname, dirname = ''):
|
||||
if dirname and not dirname.endswith('/'):
|
||||
dirname = dirname + '/'
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
|
||||
r = repo.new_in_branch(bname)
|
||||
return dict(repo = r, tree = r.tree(), 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')
|
||||
@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)
|
||||
|
||||
@bottle.route('/static/<path:path>')
|
||||
def static(path):
|
||||
return bottle.static_file(path, root = './static/')
|
||||
|
||||
|
||||
#
|
||||
# Static HTML generation
|
||||
#
|
||||
|
||||
def generate(output):
|
||||
"""Generate static html to the output directory."""
|
||||
def write_to(path, func_or_str, args = (), mtime = None):
|
||||
path = output + '/' + path
|
||||
dirname = os.path.dirname(path)
|
||||
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
if mtime:
|
||||
path_mtime = 0
|
||||
if os.path.exists(path):
|
||||
path_mtime = os.stat(path).st_mtime
|
||||
|
||||
# Make sure they're both float or int, to avoid failing
|
||||
# comparisons later on because of this.
|
||||
if isinstance(path_mtime, int):
|
||||
mtime = int(mtime)
|
||||
|
||||
# If we were given mtime, we compare against it to see if we
|
||||
# should write the file or not. Compare with almost-equality
|
||||
# because otherwise floating point equality gets in the way, and
|
||||
# we rather write a bit more, than generate the wrong output.
|
||||
if abs(path_mtime - mtime) < 0.000001:
|
||||
return
|
||||
print(path)
|
||||
s = func_or_str(*args)
|
||||
else:
|
||||
# Otherwise, be lazy if we were given a function to run, or write
|
||||
# always if they gave us a string.
|
||||
if isinstance(func_or_str, (str, unicode)):
|
||||
print(path)
|
||||
s = func_or_str
|
||||
else:
|
||||
if os.path.exists(path):
|
||||
return
|
||||
print(path)
|
||||
s = func_or_str(*args)
|
||||
|
||||
open(path, 'w').write(s.encode('utf8', errors = 'xmlcharrefreplace'))
|
||||
if mtime:
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
def link(from_path, to_path):
|
||||
from_path = output + '/' + from_path
|
||||
|
||||
if os.path.lexists(from_path):
|
||||
return
|
||||
print(from_path, '->', to_path)
|
||||
os.symlink(to_path, from_path)
|
||||
|
||||
def write_tree(r, bn, mtime):
|
||||
t = r.tree(bn)
|
||||
|
||||
write_to('r/%s/b/%s/t/index.html' % (r.name, bn),
|
||||
tree, (r, bn), mtime)
|
||||
|
||||
for otype, oname, _ in t.ls('', recursive = True):
|
||||
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
||||
# expected to fail for now; we skip them.
|
||||
if '\n' in oname.raw:
|
||||
print('skipping file with \\n: %r' % (oname.raw))
|
||||
continue
|
||||
|
||||
if otype == 'blob':
|
||||
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),
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
for r in sorted(repos.values(), key = lambda r: r.name):
|
||||
write_to('r/%s/index.html' % r.name, summary(r))
|
||||
for bn in r.branch_names():
|
||||
commit_count = 0
|
||||
commit_ids = r.commit_ids('refs/heads/' + bn,
|
||||
limit = r.info.commits_per_page * r.info.max_pages)
|
||||
for cid in commit_ids:
|
||||
write_to('r/%s/c/%s/index.html' % (r.name, cid),
|
||||
commit, (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
|
||||
# write.
|
||||
branch_mtime = r.commit(bn).committer_date.epoch
|
||||
|
||||
nr_pages = int(math.ceil(
|
||||
float(commit_count) / r.info.commits_per_page))
|
||||
nr_pages = min(nr_pages, r.info.max_pages)
|
||||
|
||||
for page in range(nr_pages):
|
||||
write_to('r/%s/b/%s/%d.html' % (r.name, bn, page),
|
||||
branch, (r, bn, page), branch_mtime)
|
||||
|
||||
link(from_path = 'r/%s/b/%s/index.html' % (r.name, bn),
|
||||
to_path = '0.html')
|
||||
|
||||
if r.info.generate_tree:
|
||||
write_tree(r, bn, branch_mtime)
|
||||
|
||||
for tag_name, obj_id in r.tags():
|
||||
try:
|
||||
write_to('r/%s/c/%s/index.html' % (r.name, obj_id),
|
||||
commit, (r, obj_id))
|
||||
except bottle.HTTPError as e:
|
||||
# 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:
|
||||
print('404 in tag %s (%s)' % (tag_name, obj_id))
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser('usage: %prog [options] serve|generate')
|
||||
parser.add_option('-c', '--config', metavar = 'FILE',
|
||||
help = 'configuration file')
|
||||
parser.add_option('-o', '--output', metavar = 'DIR',
|
||||
help = 'output directory (for generate)')
|
||||
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
|
||||
help = 'generate/serve only this repository')
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
if not opts.config:
|
||||
parser.error('--config is mandatory')
|
||||
|
||||
try:
|
||||
load_config(opts.config)
|
||||
except configparser.NoOptionError as e:
|
||||
print('Error parsing config:', e)
|
||||
|
||||
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)
|
||||
else:
|
||||
parser.error('Unknown action %s' % args[0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
522
git.py
Normal file
522
git.py
Normal file
@ -0,0 +1,522 @@
|
||||
"""
|
||||
Python wrapper for git.
|
||||
|
||||
This module is a light Python API for interfacing with it. It calls the git
|
||||
command line tool directly, so please be careful with using untrusted
|
||||
parameters.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
import email.utils
|
||||
import datetime
|
||||
import urllib
|
||||
from cgi import escape
|
||||
|
||||
|
||||
# Path to the git binary.
|
||||
GIT_BIN = "git"
|
||||
|
||||
class EncodeWrapper:
|
||||
"""File-like wrapper that returns data utf8 encoded."""
|
||||
def __init__(self, fd, encoding = 'utf8', errors = 'replace'):
|
||||
self.fd = fd
|
||||
self.encoding = encoding
|
||||
self.errors = errors
|
||||
|
||||
def __iter__(self):
|
||||
for line in self.fd:
|
||||
yield line.decode(self.encoding, errors = self.errors)
|
||||
|
||||
def read(self):
|
||||
"""Returns the whole content."""
|
||||
s = self.fd.read()
|
||||
return s.decode(self.encoding, errors = self.errors)
|
||||
|
||||
def readline(self):
|
||||
"""Returns a single line."""
|
||||
s = self.fd.readline()
|
||||
return s.decode(self.encoding, errors = self.errors)
|
||||
|
||||
|
||||
def run_git(repo_path, params, stdin = None):
|
||||
"""Invokes git with the given parameters.
|
||||
|
||||
This function invokes git with the given parameters, and returns a
|
||||
file-like object with the output (from a pipe).
|
||||
"""
|
||||
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
|
||||
|
||||
if not stdin:
|
||||
p = subprocess.Popen(params, stdin = None, stdout = subprocess.PIPE)
|
||||
else:
|
||||
p = subprocess.Popen(params,
|
||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE)
|
||||
p.stdin.write(stdin)
|
||||
p.stdin.close()
|
||||
|
||||
# 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:
|
||||
return io.TextIOWrapper(p.stdout, encoding = 'utf8',
|
||||
errors = 'replace')
|
||||
else:
|
||||
return EncodeWrapper(p.stdout)
|
||||
|
||||
|
||||
class GitCommand (object):
|
||||
"""Convenient way of invoking git."""
|
||||
def __init__(self, path, cmd, *args, **kwargs):
|
||||
self._override = True
|
||||
self._path = path
|
||||
self._cmd = cmd
|
||||
self._args = list(args)
|
||||
self._kwargs = {}
|
||||
self._stdin_buf = None
|
||||
self._override = False
|
||||
for k, v in kwargs:
|
||||
self.__setattr__(k, v)
|
||||
|
||||
def __setattr__(self, k, v):
|
||||
if k == '_override' or self._override:
|
||||
self.__dict__[k] = v
|
||||
return
|
||||
k = k.replace('_', '-')
|
||||
self._kwargs[k] = v
|
||||
|
||||
def arg(self, a):
|
||||
"""Adds an argument."""
|
||||
self._args.append(a)
|
||||
|
||||
def stdin(self, s):
|
||||
"""Sets the contents we will send in stdin."""
|
||||
self._override = True
|
||||
self._stdin_buf = s
|
||||
self._override = False
|
||||
|
||||
def run(self):
|
||||
"""Runs the git command."""
|
||||
params = [self._cmd]
|
||||
|
||||
for k, v in self._kwargs.items():
|
||||
dash = '--' if len(k) > 1 else '-'
|
||||
if v is None:
|
||||
params.append('%s%s' % (dash, k))
|
||||
else:
|
||||
params.append('%s%s=%s' % (dash, k, str(v)))
|
||||
|
||||
params.extend(self._args)
|
||||
|
||||
return run_git(self._path, params, self._stdin_buf)
|
||||
|
||||
|
||||
class SimpleNamespace (object):
|
||||
"""An entirely flexible object, which provides a convenient namespace."""
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
class smstr:
|
||||
"""A "smart" string, containing many representations for ease of use.
|
||||
|
||||
This is a string class that contains:
|
||||
.raw -> raw string, authoritative source.
|
||||
.unicode -> unicode representation, may not be perfect if .raw is not
|
||||
proper utf8 but should be good enough to show.
|
||||
.url -> escaped for safe embedding in URLs, can be not quite
|
||||
readable.
|
||||
.html -> an HTML-embeddable representation.
|
||||
"""
|
||||
def __init__(self, raw):
|
||||
if not isinstance(raw, str):
|
||||
raise TypeError("The raw string must be instance of 'str'")
|
||||
self.raw = raw
|
||||
self.unicode = raw.decode('utf8', errors = 'replace')
|
||||
self.url = urllib.pathname2url(raw)
|
||||
self.html = self._to_html()
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.raw, other.raw)
|
||||
|
||||
# Note we don't define __repr__() or __str__() to prevent accidental
|
||||
# misuse. It does mean that some uses become more annoying, so it's a
|
||||
# tradeoff that may change in the future.
|
||||
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
"""Returns an smstr() instance from an url-encoded string."""
|
||||
return smstr(urllib.url2pathname(url))
|
||||
|
||||
def split(self, sep):
|
||||
"""Like str.split()."""
|
||||
return [ smstr(s) for s in self.raw.split(sep) ]
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, smstr):
|
||||
other = other.raw
|
||||
return smstr(self.raw + other)
|
||||
|
||||
def _to_html(self):
|
||||
"""Returns an html representation of the unicode string."""
|
||||
html = u''
|
||||
for c in escape(self.unicode):
|
||||
if c in '\t\r\n\r\f\a\b\v\0':
|
||||
esc_c = c.encode('ascii').encode('string_escape')
|
||||
html += '<span class="ctrlchr">%s</span>' % esc_c
|
||||
else:
|
||||
html += c
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def unquote(s):
|
||||
"""Git can return quoted file names, unquote them. Always return a str."""
|
||||
if not (s[0] == '"' and s[-1] == '"'):
|
||||
# Unquoted strings are always safe, no need to mess with them; just
|
||||
# make sure we return str.
|
||||
s = s.encode('ascii')
|
||||
return s
|
||||
|
||||
# Get rid of the quotes, we never want them in the output, and convert to
|
||||
# a raw string, un-escaping the backslashes.
|
||||
s = s[1:-1].decode('string-escape')
|
||||
|
||||
return s
|
||||
|
||||
|
||||
class Repo:
|
||||
"""A git repository."""
|
||||
|
||||
def __init__(self, path, branch = None, 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()
|
||||
|
||||
def cmd(self, cmd):
|
||||
"""Returns a GitCommand() on our path."""
|
||||
return GitCommand(self.path, cmd)
|
||||
|
||||
def for_each_ref(self, pattern = None, sort = None):
|
||||
"""Returns a list of references."""
|
||||
cmd = self.cmd('for-each-ref')
|
||||
if sort:
|
||||
cmd.sort = sort
|
||||
if pattern:
|
||||
cmd.arg(pattern)
|
||||
|
||||
for l in cmd.run():
|
||||
obj_id, obj_type, ref = l.split()
|
||||
yield obj_id, obj_type, ref
|
||||
|
||||
def branches(self, sort = '-authordate'):
|
||||
"""Get the (name, obj_id) of the branches."""
|
||||
refs = self.for_each_ref(pattern = 'refs/heads/', sort = sort)
|
||||
for obj_id, _, ref in refs:
|
||||
yield ref[len('refs/heads/'):], obj_id
|
||||
|
||||
def branch_names(self):
|
||||
"""Get the names of the branches."""
|
||||
return ( name for name, _ in self.branches() )
|
||||
|
||||
def tags(self, sort = '-taggerdate'):
|
||||
"""Get the (name, obj_id) of the tags."""
|
||||
refs = self.for_each_ref(pattern = 'refs/tags/', sort = sort)
|
||||
for obj_id, _, ref in refs:
|
||||
yield ref[len('refs/tags/'):], obj_id
|
||||
|
||||
def tag_names(self):
|
||||
"""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')
|
||||
if limit:
|
||||
cmd.max_count = limit
|
||||
|
||||
cmd.arg(ref)
|
||||
|
||||
for l in cmd.run():
|
||||
yield l.rstrip('\n')
|
||||
|
||||
def commit(self, commit_id):
|
||||
"""Return a single commit."""
|
||||
cs = list(self.commits(commit_id, limit = 1))
|
||||
if len(cs) != 1:
|
||||
return None
|
||||
return cs[0]
|
||||
|
||||
def commits(self, ref, limit = None, offset = 0):
|
||||
"""Generate commit objects for the ref."""
|
||||
cmd = self.cmd('rev-list')
|
||||
if limit:
|
||||
cmd.max_count = limit + offset
|
||||
|
||||
cmd.header = None
|
||||
|
||||
cmd.arg(ref)
|
||||
|
||||
info_buffer = ''
|
||||
count = 0
|
||||
for l in cmd.run():
|
||||
if '\0' in l:
|
||||
pre, post = l.split('\0', 1)
|
||||
info_buffer += pre
|
||||
|
||||
count += 1
|
||||
if count > offset:
|
||||
yield Commit.from_str(self, info_buffer)
|
||||
|
||||
# Start over.
|
||||
info_buffer = post
|
||||
else:
|
||||
info_buffer += l
|
||||
|
||||
if info_buffer:
|
||||
count += 1
|
||||
if count > offset:
|
||||
yield Commit.from_str(self, info_buffer)
|
||||
|
||||
def diff(self, ref):
|
||||
"""Return a Diff object for the ref."""
|
||||
cmd = self.cmd('diff-tree')
|
||||
cmd.patch = None
|
||||
cmd.numstat = None
|
||||
cmd.find_renames = 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.
|
||||
|
||||
cmd.arg(ref)
|
||||
|
||||
return Diff.from_str(cmd.run())
|
||||
|
||||
def refs(self):
|
||||
"""Return a dict of obj_id -> ref."""
|
||||
cmd = self.cmd('show-ref')
|
||||
cmd.dereference = None
|
||||
|
||||
r = defaultdict(list)
|
||||
for l in cmd.run():
|
||||
l = l.strip()
|
||||
obj_id, ref = l.split(' ', 1)
|
||||
r[obj_id].append(ref)
|
||||
|
||||
return r
|
||||
|
||||
def tree(self, ref = None):
|
||||
"""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
|
||||
cmd = self.cmd('cat-file')
|
||||
cmd.batch = None
|
||||
|
||||
if isinstance(ref, unicode):
|
||||
ref = ref.encode('utf8')
|
||||
cmd.stdin('%s:%s' % (ref, path))
|
||||
|
||||
out = cmd.run()
|
||||
head = out.readline()
|
||||
if not head or head.strip().endswith('missing'):
|
||||
return None
|
||||
|
||||
return out.read()
|
||||
|
||||
|
||||
class Commit (object):
|
||||
"""A git commit."""
|
||||
|
||||
def __init__(self, repo,
|
||||
commit_id, parents, tree,
|
||||
author, author_epoch, author_tz,
|
||||
committer, committer_epoch, committer_tz,
|
||||
message):
|
||||
self._repo = repo
|
||||
self.id = commit_id
|
||||
self.parents = parents
|
||||
self.tree = tree
|
||||
self.author = author
|
||||
self.author_epoch = author_epoch
|
||||
self.author_tz = author_tz
|
||||
self.committer = committer
|
||||
self.committer_epoch = committer_epoch
|
||||
self.committer_tz = committer_tz
|
||||
self.message = message
|
||||
|
||||
self.author_name, self.author_email = \
|
||||
email.utils.parseaddr(self.author)
|
||||
|
||||
self.committer_name, self.committer_email = \
|
||||
email.utils.parseaddr(self.committer)
|
||||
|
||||
self.subject, self.body = self.message.split('\n', 1)
|
||||
|
||||
self.author_date = Date(self.author_epoch, self.author_tz)
|
||||
self.committer_date = Date(self.committer_epoch, self.committer_tz)
|
||||
|
||||
|
||||
# Only get this lazily when we need it; most of the time it's not
|
||||
# required by the caller.
|
||||
self._diff = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<C %s p:%s a:%s s:%r>' % (
|
||||
self.id[:7],
|
||||
','.join(p[:7] for p in self.parents),
|
||||
self.author_email,
|
||||
self.subject[:20])
|
||||
|
||||
@property
|
||||
def diff(self):
|
||||
"""Return the diff for this commit, in unified format."""
|
||||
if not self._diff:
|
||||
self._diff = self._repo.diff(self.id)
|
||||
return self._diff
|
||||
|
||||
@staticmethod
|
||||
def from_str(repo, buf):
|
||||
"""Parses git rev-list output, returns a commit object."""
|
||||
header, raw_message = buf.split('\n\n', 1)
|
||||
|
||||
header_lines = header.split('\n')
|
||||
commit_id = header_lines.pop(0)
|
||||
|
||||
header_dict = defaultdict(list)
|
||||
for line in header_lines:
|
||||
k, v = line.split(' ', 1)
|
||||
header_dict[k].append(v)
|
||||
|
||||
tree = header_dict['tree'][0]
|
||||
parents = set(header_dict['parent'])
|
||||
author, author_epoch, author_tz = \
|
||||
header_dict['author'][0].rsplit(' ', 2)
|
||||
committer, committer_epoch, committer_tz = \
|
||||
header_dict['committer'][0].rsplit(' ', 2)
|
||||
|
||||
# Remove the first four spaces from the message's lines.
|
||||
message = ''
|
||||
for line in raw_message.split('\n'):
|
||||
message += line[4:] + '\n'
|
||||
|
||||
return Commit(repo,
|
||||
commit_id = commit_id, tree = tree, parents = parents,
|
||||
author = author,
|
||||
author_epoch = author_epoch, author_tz = author_tz,
|
||||
committer = committer,
|
||||
committer_epoch = committer_epoch, committer_tz = committer_tz,
|
||||
message = message)
|
||||
|
||||
class Date:
|
||||
"""Handy representation for a datetime from git."""
|
||||
def __init__(self, epoch, tz):
|
||||
self.epoch = int(epoch)
|
||||
self.tz = tz
|
||||
self.utc = datetime.datetime.fromtimestamp(self.epoch)
|
||||
|
||||
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
||||
if tz[0] == '-':
|
||||
self.tz_sec_offset_min = -self.tz_sec_offset_min
|
||||
|
||||
self.local = self.utc + datetime.timedelta(
|
||||
minutes = self.tz_sec_offset_min)
|
||||
|
||||
self.str = self.utc.strftime('%a, %d %b %Y %H:%M:%S +0000 ')
|
||||
self.str += '(%s %s)' % (self.local.strftime('%H:%M'), self.tz)
|
||||
|
||||
def __str__(self):
|
||||
return self.str
|
||||
|
||||
|
||||
class Diff:
|
||||
"""A diff between two trees."""
|
||||
def __init__(self, ref, changes, body):
|
||||
"""Constructor.
|
||||
|
||||
- ref: reference id the diff refers to.
|
||||
- changes: [ (added, deleted, filename), ... ]
|
||||
- body: diff body, as text, verbatim.
|
||||
"""
|
||||
self.ref = ref
|
||||
self.changes = changes
|
||||
self.body = body
|
||||
|
||||
@staticmethod
|
||||
def from_str(buf):
|
||||
"""Parses git diff-tree output, returns a Diff object."""
|
||||
lines = iter(buf)
|
||||
try:
|
||||
ref_id = next(lines)
|
||||
except StopIteration:
|
||||
# No diff; this can happen in merges without conflicts.
|
||||
return Diff(None, [], '')
|
||||
|
||||
# First, --numstat information.
|
||||
changes = []
|
||||
l = next(lines)
|
||||
while l != '\n':
|
||||
l = l.rstrip('\n')
|
||||
added, deleted, fname = l.split('\t', 2)
|
||||
added = added.replace('-', '0')
|
||||
deleted = deleted.replace('-', '0')
|
||||
fname = smstr(unquote(fname))
|
||||
changes.append((int(added), int(deleted), fname))
|
||||
l = next(lines)
|
||||
|
||||
# And now the diff body. We just store as-is, we don't really care for
|
||||
# the contents.
|
||||
body = ''.join(lines)
|
||||
|
||||
return Diff(ref_id, changes, body)
|
||||
|
||||
|
||||
class Tree:
|
||||
""" A git tree."""
|
||||
|
||||
def __init__(self, repo, ref):
|
||||
self.repo = repo
|
||||
self.ref = ref
|
||||
|
||||
def ls(self, path, recursive = False):
|
||||
"""Generates (type, name, size) for each file in path."""
|
||||
cmd = self.repo.cmd('ls-tree')
|
||||
cmd.long = None
|
||||
if recursive:
|
||||
cmd.r = None
|
||||
cmd.t = None
|
||||
|
||||
cmd.arg(self.ref)
|
||||
cmd.arg(path)
|
||||
|
||||
for l in cmd.run():
|
||||
_mode, otype, _oid, size, name = l.split(None, 4)
|
||||
if size == '-':
|
||||
size = None
|
||||
else:
|
||||
size = int(size)
|
||||
|
||||
# Remove the quoting (if any); will always give us a str.
|
||||
name = unquote(name.strip('\n'))
|
||||
|
||||
# Strip the leading path, the caller knows it and it's often
|
||||
# easier to work with this way.
|
||||
name = name[len(path):]
|
||||
|
||||
# We use a smart string for the name, as it's often tricky to
|
||||
# manipulate otherwise.
|
||||
yield otype, smstr(name), size
|
||||
|
61
sample.conf
Normal file
61
sample.conf
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
# A single repository.
|
||||
[repo]
|
||||
path = /srv/git/repo/
|
||||
|
||||
# Description (optional).
|
||||
# Default: Read from <path>/description, or "" if there is no such file.
|
||||
#desc = My lovely repository
|
||||
|
||||
# Do we allow browsing the file tree for each branch? (optional).
|
||||
# Useful to disable an expensive operation in very large repositories.
|
||||
#tree = yes
|
||||
|
||||
# How many commits to show in the summary page (optional).
|
||||
#commits_in_summary = 10
|
||||
|
||||
# How many commits to show in each page when viewing a branch (optional).
|
||||
#commits_per_page = 50
|
||||
|
||||
# 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
|
||||
|
||||
# Project website (optional).
|
||||
# URL to the project's website. %(name)s will be replaced with the current
|
||||
# section name (here and everywhere).
|
||||
#web_url = http://example.org/%(name)s
|
||||
|
||||
# File name to get the project website from (optional).
|
||||
# If web_url is not set, attempt to get its value from this file.
|
||||
# Default: "web_url".
|
||||
#web_url_file = web_url
|
||||
|
||||
# Git repository URLs (optional).
|
||||
# URLs to the project's git repository.
|
||||
#git_url = git://example.org/%(name)s http://example.org/git/%(name)s
|
||||
|
||||
# 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
|
||||
|
||||
# Do we look for repositories within this path? (optional).
|
||||
# This option enables a recursive, 1 level search for repositories within the
|
||||
# given path. They will inherit their options from this section.
|
||||
# Note that repositories that contain a file named "disable_gitweb" will be
|
||||
# excluded.
|
||||
#recursive = no
|
||||
|
||||
|
||||
# Another repository, we don't generate a tree for it because it's too big.
|
||||
[linux]
|
||||
path = /srv/git/linux/
|
||||
desc = Linux kernel
|
||||
tree = no
|
||||
|
||||
# Look for repositories within this directory.
|
||||
[projects]
|
||||
path = /srv/projects/
|
||||
recursive = yes
|
168
static/git-arr.css
Normal file
168
static/git-arr.css
Normal file
@ -0,0 +1,168 @@
|
||||
|
||||
/*
|
||||
* git-arr style sheet
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: small;
|
||||
padding: 0 1em 1em 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: x-large;
|
||||
background: #ddd;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 0.3em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
background-color: #e3e3e3;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* By default, use implied links, more discrete for increased readability. */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
color: #800;
|
||||
}
|
||||
|
||||
/* Explicit links */
|
||||
a.explicit {
|
||||
color: #038;
|
||||
}
|
||||
a.explicit:hover, a.explicit:active {
|
||||
color: #880000;
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
table.nice tr:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* Table for commits. */
|
||||
table.commits td.date {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
table.commits td.subject {
|
||||
min-width: 32em;
|
||||
}
|
||||
table.commits td.author {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Table for commit information. */
|
||||
table.commit-info tr:hover {
|
||||
background: inherit;
|
||||
}
|
||||
table.commit-info td {
|
||||
vertical-align: top;
|
||||
}
|
||||
table.commit-info span.date, span.email {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Reference annotations. */
|
||||
span.refs {
|
||||
margin: 0px 0.5em;
|
||||
padding: 0px 0.25em;
|
||||
border: solid 1px gray;
|
||||
}
|
||||
span.head {
|
||||
background-color: #88ff88;
|
||||
}
|
||||
span.tag {
|
||||
background-color: #ffff88;
|
||||
}
|
||||
|
||||
/* Commit message and diff. */
|
||||
pre.commit-message {
|
||||
font-size: large;
|
||||
padding: 0.2em 2em;
|
||||
}
|
||||
pre.diff-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
font-size: medium;
|
||||
}
|
||||
table.changed-files span.lines-added {
|
||||
color: green;
|
||||
}
|
||||
table.changed-files span.lines-deleted {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Pagination. */
|
||||
div.paginate {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
div.paginate span.inactive {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Directory listing. */
|
||||
table.ls td.name {
|
||||
min-width: 20em;
|
||||
}
|
||||
table.ls tr.blob td.size {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Blob. */
|
||||
pre.blob-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
/* Pygments overrides. */
|
||||
div.linenodiv {
|
||||
padding-right: 0.5em;
|
||||
color: gray;
|
||||
font-size: medium;
|
||||
}
|
||||
div.source_code {
|
||||
background: inherit;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
/* Repository information table. */
|
||||
table.repo_info tr:hover {
|
||||
background: inherit;
|
||||
}
|
||||
table.repo_info td.category {
|
||||
font-weight: bold;
|
||||
}
|
||||
table.repo_info td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
span.ctrlchr {
|
||||
color: gray;
|
||||
padding: 0 0.2ex 0 0.1ex;
|
||||
margin: 0 0.2ex 0 0.1ex;
|
||||
}
|
70
static/syntax.css
Normal file
70
static/syntax.css
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
/* CSS for syntax highlighting.
|
||||
* Generated by pygments (what we use for syntax highlighting):
|
||||
*
|
||||
* $ pygmentize -S default -f html -a .source_code
|
||||
*/
|
||||
|
||||
.source_code .hll { background-color: #ffffcc }
|
||||
.source_code { background: #f8f8f8; }
|
||||
.source_code .c { color: #408080; font-style: italic } /* Comment */
|
||||
.source_code .err { border: 1px solid #FF0000 } /* Error */
|
||||
.source_code .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.source_code .o { color: #666666 } /* Operator */
|
||||
.source_code .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.source_code .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.source_code .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.source_code .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.source_code .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.source_code .ge { font-style: italic } /* Generic.Emph */
|
||||
.source_code .gr { color: #FF0000 } /* Generic.Error */
|
||||
.source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.source_code .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.source_code .go { color: #808080 } /* Generic.Output */
|
||||
.source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.source_code .gs { font-weight: bold } /* Generic.Strong */
|
||||
.source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.source_code .gt { color: #0040D0 } /* Generic.Traceback */
|
||||
.source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||
.source_code .kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.source_code .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.source_code .kt { color: #B00040 } /* Keyword.Type */
|
||||
.source_code .m { color: #666666 } /* Literal.Number */
|
||||
.source_code .s { color: #BA2121 } /* Literal.String */
|
||||
.source_code .na { color: #7D9029 } /* Name.Attribute */
|
||||
.source_code .nb { color: #008000 } /* Name.Builtin */
|
||||
.source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.source_code .no { color: #880000 } /* Name.Constant */
|
||||
.source_code .nd { color: #AA22FF } /* Name.Decorator */
|
||||
.source_code .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.source_code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.source_code .nf { color: #0000FF } /* Name.Function */
|
||||
.source_code .nl { color: #A0A000 } /* Name.Label */
|
||||
.source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.source_code .nv { color: #19177C } /* Name.Variable */
|
||||
.source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.source_code .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.source_code .mf { color: #666666 } /* Literal.Number.Float */
|
||||
.source_code .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.source_code .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.source_code .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.source_code .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.source_code .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.source_code .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.source_code .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.source_code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.source_code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
||||
.source_code .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.source_code .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.source_code .ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.source_code .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.source_code .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.source_code .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.source_code .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.source_code .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
|
41
utils.py
Normal file
41
utils.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""
|
||||
Miscellaneous utilities.
|
||||
|
||||
These are mostly used in templates, for presentation purposes.
|
||||
"""
|
||||
|
||||
try:
|
||||
import pygments
|
||||
from pygments import highlight
|
||||
from pygments import lexers
|
||||
from pygments.formatters import HtmlFormatter
|
||||
except ImportError:
|
||||
pygments = None
|
||||
|
||||
|
||||
def shorten(s, width = 60):
|
||||
if len(s) < 60:
|
||||
return s
|
||||
return s[:57] + "..."
|
||||
|
||||
def has_colorizer():
|
||||
return pygments is not None
|
||||
|
||||
def colorize_diff(s):
|
||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
cssclass = 'source_code')
|
||||
|
||||
return highlight(s, lexer, formatter)
|
||||
|
||||
def colorize_blob(fname, s):
|
||||
try:
|
||||
lexer = lexers.guess_lexer_for_filename(fname, s)
|
||||
except lexers.ClassNotFound:
|
||||
lexer = lexers.TextLexer(encoding = 'utf-8')
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
cssclass = 'source_code',
|
||||
linenos = 'table')
|
||||
|
||||
return highlight(s, lexer, formatter)
|
||||
|
50
views/blob.html
Normal file
50
views/blob.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!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">
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.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"/>
|
||||
</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>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
% for c in dirname.split('/'):
|
||||
% if not c.raw: continue
|
||||
<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)}}
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
{{blob}}
|
||||
</pre>
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
|
||||
</body>
|
||||
</html>
|
42
views/branch.html
Normal file
42
views/branch.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!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">
|
||||
<head>
|
||||
<title>git » {{repo.name}} » {{repo.branch}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../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>
|
||||
|
||||
<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,
|
||||
% offset = repo.info.commits_per_page * offset)
|
||||
% commits = list(commits)
|
||||
|
||||
% if len(commits) == 0:
|
||||
% abort(404, "No more commits")
|
||||
% end
|
||||
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
|
||||
% kwargs = dict(repo=repo, commits=commits,
|
||||
% shorten=shorten, repo_root="../..")
|
||||
% include commit-list **kwargs
|
||||
|
||||
<p/>
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
47
views/commit-list.html
Normal file
47
views/commit-list.html
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
% def refs_to_html(refs):
|
||||
% for ref in refs:
|
||||
% c = ref.split('/', 2)
|
||||
% if len(c) != 3:
|
||||
% return
|
||||
% end
|
||||
% if c[1] == 'heads':
|
||||
<span class="refs head">{{c[2]}}</span>
|
||||
% elif c[1] == 'tags':
|
||||
% if c[2].endswith('^{}'):
|
||||
% c[2] = c[2][:-3]
|
||||
% end
|
||||
<span class="refs tag">{{c[2]}}</span>
|
||||
% end
|
||||
% end
|
||||
% end
|
||||
|
||||
<table class="nice commits">
|
||||
|
||||
% refs = repo.refs()
|
||||
% if not defined("commits"):
|
||||
% commits = repo.commits(start_ref, limit = limit, offset = offset)
|
||||
% end
|
||||
|
||||
% for c in commits:
|
||||
<tr>
|
||||
<td class="date">
|
||||
<span title="{{c.author_date.str}}">{{c.author_date.utc.date()}}</span>
|
||||
</td>
|
||||
<td class="subject">
|
||||
<a href="{{repo_root}}/c/{{c.id}}/"
|
||||
title="{{c.subject}}">
|
||||
{{shorten(c.subject)}}</a>
|
||||
</td>
|
||||
<td class="author">
|
||||
<span title="{{c.author_name}}">{{shorten(c.author_name, 26)}}</span>
|
||||
</td>
|
||||
% if c.id in refs:
|
||||
<td>
|
||||
% refs_to_html(refs[c.id])
|
||||
</td>
|
||||
% end
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
|
72
views/commit.html
Normal file
72
views/commit.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!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">
|
||||
<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"/>
|
||||
</head>
|
||||
|
||||
<body class="commit">
|
||||
<h1><a href="../../../../">git</a> »
|
||||
<a href="../../">{{repo.name}}</a> » commit {{c.id[:7]}}
|
||||
</h1>
|
||||
|
||||
<h2>{{c.subject}}</h2>
|
||||
|
||||
<table class="nice commit-info">
|
||||
<tr><td>author</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>
|
||||
<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>
|
||||
|
||||
% for p in c.parents:
|
||||
<tr><td>parent</td>
|
||||
<td><a href="../{{p}}/">{{p}}</a></td></tr>
|
||||
% end
|
||||
</table>
|
||||
|
||||
<hr/>
|
||||
|
||||
<pre class="commit-message">
|
||||
{{c.message.strip()}}
|
||||
</pre>
|
||||
|
||||
<hr/>
|
||||
|
||||
% if c.diff.changes:
|
||||
|
||||
<table class="nice changed-files">
|
||||
% for added, deleted, fname in c.diff.changes:
|
||||
<tr>
|
||||
<td class="main">{{!fname.html}}</td>
|
||||
<td><span class="lines-added">+{{added}}</span></td>
|
||||
<td><span class="lines-deleted">-{{deleted}}</span></td>
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
|
||||
<hr/>
|
||||
|
||||
% if has_colorizer():
|
||||
{{!colorize_diff(c.diff.body)}}
|
||||
% else:
|
||||
<pre class="diff-body">
|
||||
{{c.diff.body}}
|
||||
</pre>
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
|
||||
% end
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
29
views/index.html
Normal file
29
views/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!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">
|
||||
<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"/>
|
||||
</head>
|
||||
|
||||
<body class="index">
|
||||
<h1>git</h1>
|
||||
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>project</th>
|
||||
<th>description</th>
|
||||
</tr>
|
||||
|
||||
% 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>
|
||||
</tr>
|
||||
%end
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
15
views/paginate.html
Normal file
15
views/paginate.html
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
<div class="paginate">
|
||||
% if offset > 0:
|
||||
<a href="{{offset - 1}}.html">← prev</a>
|
||||
% else:
|
||||
<span class="inactive">← prev</span>
|
||||
% end
|
||||
<span class="sep">|</span>
|
||||
% if nelem >= max_per_page:
|
||||
<a href="{{offset + 1}}.html">next →</a>
|
||||
% else:
|
||||
<span class="inactive">next →</span>
|
||||
% end
|
||||
</div>
|
||||
|
81
views/summary.html
Normal file
81
views/summary.html
Normal file
@ -0,0 +1,81 @@
|
||||
<!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">
|
||||
<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"/>
|
||||
</head>
|
||||
|
||||
<body class="summary">
|
||||
<h1><a href="../../">git</a> » <a href="./">{{repo.name}}</a></h1>
|
||||
|
||||
<h2>{{repo.info.desc}}</h2>
|
||||
|
||||
|
||||
% if repo.info.web_url or repo.info.git_url:
|
||||
<table class="nice repo_info">
|
||||
|
||||
% if repo.info.web_url:
|
||||
<tr>
|
||||
<td class="category">website</td>
|
||||
<td><a class="explicit" href="{{repo.info.web_url}}">
|
||||
{{repo.info.web_url}}</a></td>
|
||||
</tr>
|
||||
% end
|
||||
% if repo.info.git_url:
|
||||
<tr>
|
||||
<td class="category">repository</td>
|
||||
<td>{{! '<br/>'.join(repo.info.git_url.split())}}</td>
|
||||
</tr>
|
||||
% end
|
||||
|
||||
</table>
|
||||
<hr/>
|
||||
% end
|
||||
|
||||
% if "master" in repo.branch_names():
|
||||
% 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
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>branches</th>
|
||||
</tr>
|
||||
|
||||
% for b in repo.branch_names():
|
||||
<tr>
|
||||
<td class="main"><a href="b/{{b}}/">{{b}}</a></td>
|
||||
<td class="links">
|
||||
<a class="explicit" href="b/{{b}}/">commits</a></td>
|
||||
<td class="links">
|
||||
<a class="explicit" href="b/{{b}}/t/">tree</a></td>
|
||||
</tr>
|
||||
%end
|
||||
</table>
|
||||
|
||||
<hr/>
|
||||
|
||||
% tags = list(repo.tags())
|
||||
% if tags:
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>tags</th>
|
||||
</tr>
|
||||
|
||||
% for name, obj_id in tags:
|
||||
<tr>
|
||||
<td><a href="c/{{obj_id}}/">{{name}}</a></td>
|
||||
</tr>
|
||||
%end
|
||||
</table>
|
||||
% end
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
54
views/tree.html
Normal file
54
views/tree.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!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">
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.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"/>
|
||||
</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>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
% for c in dirname.split('/'):
|
||||
% if not c.raw: continue
|
||||
<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>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user