Auto-format the code with black
This patch applies auto-formatting of the source code using black (https://github.com/psf/black). This makes the code style more uniform and simplifies editing. Note I also tried yapf, and IMO produced nicer output and handled some corner cases much better, but unfortunately it doesn't yet support type annotations, which will be introduced in later commits. So in the future we might switch to yapf instead.
This commit is contained in:
parent
1183d6f817
commit
ad950208bf
372
git-arr
372
git-arr
@ -20,12 +20,13 @@ import utils
|
|||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
bottle.TEMPLATE_PATH.insert(
|
bottle.TEMPLATE_PATH.insert(
|
||||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
|
0, os.path.abspath(os.path.dirname(sys.argv[0])) + "/views/"
|
||||||
|
)
|
||||||
|
|
||||||
# The path to our static files.
|
# The path to our static files.
|
||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
|
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
|
# The list of repositories is a global variable for convenience. It will be
|
||||||
@ -40,22 +41,22 @@ def load_config(path):
|
|||||||
as configured.
|
as configured.
|
||||||
"""
|
"""
|
||||||
defaults = {
|
defaults = {
|
||||||
'tree': 'yes',
|
"tree": "yes",
|
||||||
'rootdiff': 'yes',
|
"rootdiff": "yes",
|
||||||
'desc': '',
|
"desc": "",
|
||||||
'recursive': 'no',
|
"recursive": "no",
|
||||||
'prefix': '',
|
"prefix": "",
|
||||||
'commits_in_summary': '10',
|
"commits_in_summary": "10",
|
||||||
'commits_per_page': '50',
|
"commits_per_page": "50",
|
||||||
'max_pages': '250',
|
"max_pages": "250",
|
||||||
'web_url': '',
|
"web_url": "",
|
||||||
'web_url_file': 'web_url',
|
"web_url_file": "web_url",
|
||||||
'git_url': '',
|
"git_url": "",
|
||||||
'git_url_file': 'cloneurl',
|
"git_url_file": "cloneurl",
|
||||||
'embed_markdown': 'yes',
|
"embed_markdown": "yes",
|
||||||
'embed_images': 'no',
|
"embed_images": "no",
|
||||||
'ignore': '',
|
"ignore": "",
|
||||||
'generate_patch': 'yes',
|
"generate_patch": "yes",
|
||||||
}
|
}
|
||||||
|
|
||||||
config = configparser.ConfigParser(defaults)
|
config = configparser.ConfigParser(defaults)
|
||||||
@ -63,16 +64,16 @@ def load_config(path):
|
|||||||
|
|
||||||
# Do a first pass for general sanity checking and recursive expansion.
|
# Do a first pass for general sanity checking and recursive expansion.
|
||||||
for s in config.sections():
|
for s in config.sections():
|
||||||
if config.getboolean(s, 'recursive'):
|
if config.getboolean(s, "recursive"):
|
||||||
root = config.get(s, 'path')
|
root = config.get(s, "path")
|
||||||
prefix = config.get(s, 'prefix')
|
prefix = config.get(s, "prefix")
|
||||||
|
|
||||||
for path in os.listdir(root):
|
for path in os.listdir(root):
|
||||||
fullpath = find_git_dir(root + '/' + path)
|
fullpath = find_git_dir(root + "/" + path)
|
||||||
if not fullpath:
|
if not fullpath:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if os.path.exists(fullpath + '/disable_gitweb'):
|
if os.path.exists(fullpath + "/disable_gitweb"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
section = prefix + path
|
section = prefix + path
|
||||||
@ -83,55 +84,57 @@ def load_config(path):
|
|||||||
for opt, value in config.items(s, raw=True):
|
for opt, value in config.items(s, raw=True):
|
||||||
config.set(section, opt, value)
|
config.set(section, opt, value)
|
||||||
|
|
||||||
config.set(section, 'path', fullpath)
|
config.set(section, "path", fullpath)
|
||||||
config.set(section, 'recursive', 'no')
|
config.set(section, "recursive", "no")
|
||||||
|
|
||||||
# This recursive section is no longer useful.
|
# This recursive section is no longer useful.
|
||||||
config.remove_section(s)
|
config.remove_section(s)
|
||||||
|
|
||||||
for s in config.sections():
|
for s in config.sections():
|
||||||
if config.get(s, 'ignore') and re.search(config.get(s, 'ignore'), s):
|
if config.get(s, "ignore") and re.search(config.get(s, "ignore"), s):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fullpath = find_git_dir(config.get(s, 'path'))
|
fullpath = find_git_dir(config.get(s, "path"))
|
||||||
if not fullpath:
|
if not fullpath:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'%s: path %s is not a valid git repository' % (
|
"%s: path %s is not a valid git repository"
|
||||||
s, config.get(s, 'path')))
|
% (s, config.get(s, "path"))
|
||||||
|
)
|
||||||
|
|
||||||
config.set(s, 'path', fullpath)
|
config.set(s, "path", fullpath)
|
||||||
config.set(s, 'name', s)
|
config.set(s, "name", s)
|
||||||
|
|
||||||
desc = config.get(s, 'desc')
|
desc = config.get(s, "desc")
|
||||||
if not desc and os.path.exists(fullpath + '/description'):
|
if not desc and os.path.exists(fullpath + "/description"):
|
||||||
desc = open(fullpath + '/description').read().strip()
|
desc = open(fullpath + "/description").read().strip()
|
||||||
|
|
||||||
r = git.Repo(fullpath, name=s)
|
r = git.Repo(fullpath, name=s)
|
||||||
r.info.desc = desc
|
r.info.desc = desc
|
||||||
r.info.commits_in_summary = config.getint(s, 'commits_in_summary')
|
r.info.commits_in_summary = config.getint(s, "commits_in_summary")
|
||||||
r.info.commits_per_page = config.getint(s, 'commits_per_page')
|
r.info.commits_per_page = config.getint(s, "commits_per_page")
|
||||||
r.info.max_pages = config.getint(s, 'max_pages')
|
r.info.max_pages = config.getint(s, "max_pages")
|
||||||
if r.info.max_pages <= 0:
|
if r.info.max_pages <= 0:
|
||||||
r.info.max_pages = sys.maxsize
|
r.info.max_pages = sys.maxsize
|
||||||
r.info.generate_tree = config.getboolean(s, 'tree')
|
r.info.generate_tree = config.getboolean(s, "tree")
|
||||||
r.info.root_diff = config.getboolean(s, 'rootdiff')
|
r.info.root_diff = config.getboolean(s, "rootdiff")
|
||||||
r.info.generate_patch = config.getboolean(s, 'generate_patch')
|
r.info.generate_patch = config.getboolean(s, "generate_patch")
|
||||||
|
|
||||||
r.info.web_url = config.get(s, 'web_url')
|
r.info.web_url = config.get(s, "web_url")
|
||||||
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
web_url_file = fullpath + "/" + config.get(s, "web_url_file")
|
||||||
if not r.info.web_url and os.path.isfile(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.web_url = open(web_url_file).read()
|
||||||
|
|
||||||
r.info.git_url = config.get(s, 'git_url')
|
r.info.git_url = config.get(s, "git_url")
|
||||||
git_url_file = fullpath + '/' + config.get(s, 'git_url_file')
|
git_url_file = fullpath + "/" + config.get(s, "git_url_file")
|
||||||
if not r.info.git_url and os.path.isfile(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()
|
r.info.git_url = open(git_url_file).read()
|
||||||
|
|
||||||
r.info.embed_markdown = config.getboolean(s, 'embed_markdown')
|
r.info.embed_markdown = config.getboolean(s, "embed_markdown")
|
||||||
r.info.embed_images = config.getboolean(s, 'embed_images')
|
r.info.embed_images = config.getboolean(s, "embed_images")
|
||||||
|
|
||||||
repos[r.name] = r
|
repos[r.name] = r
|
||||||
|
|
||||||
|
|
||||||
def find_git_dir(path):
|
def find_git_dir(path):
|
||||||
"""Returns the path to the git directory for the given repository.
|
"""Returns the path to the git directory for the given repository.
|
||||||
|
|
||||||
@ -141,25 +144,26 @@ def find_git_dir(path):
|
|||||||
|
|
||||||
An empty string is returned if the given path is not a valid repository.
|
An empty string is returned if the given path is not a valid repository.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(p):
|
def check(p):
|
||||||
"""A dirty check for whether this is a git dir or not."""
|
"""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
|
# 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.
|
# noise; and also we strip the final \n from the output.
|
||||||
return git.run_git(p,
|
return git.run_git(
|
||||||
['rev-parse', '--git-dir'],
|
p, ["rev-parse", "--git-dir"], silent_stderr=True
|
||||||
silent_stderr = True).read()[:-1]
|
).read()[:-1]
|
||||||
|
|
||||||
for p in [ path, path + '/.git' ]:
|
for p in [path, path + "/.git"]:
|
||||||
if check(p):
|
if check(p):
|
||||||
return p
|
return p
|
||||||
|
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def repo_filter(unused_conf):
|
def repo_filter(unused_conf):
|
||||||
"""Bottle route filter for repos."""
|
"""Bottle route filter for repos."""
|
||||||
# TODO: consider allowing /, which is tricky.
|
# TODO: consider allowing /, which is tricky.
|
||||||
regexp = r'[\w\.~-]+'
|
regexp = r"[\w\.~-]+"
|
||||||
|
|
||||||
def to_python(s):
|
def to_python(s):
|
||||||
"""Return the corresponding Python object."""
|
"""Return the corresponding Python object."""
|
||||||
@ -173,8 +177,9 @@ def repo_filter(unused_conf):
|
|||||||
|
|
||||||
return regexp, to_python, to_url
|
return regexp, to_python, to_url
|
||||||
|
|
||||||
|
|
||||||
app = bottle.Bottle()
|
app = bottle.Bottle()
|
||||||
app.router.add_filter('repo', repo_filter)
|
app.router.add_filter("repo", repo_filter)
|
||||||
bottle.app.push(app)
|
bottle.app.push(app)
|
||||||
|
|
||||||
|
|
||||||
@ -185,18 +190,18 @@ def with_utils(f):
|
|||||||
templates.
|
templates.
|
||||||
"""
|
"""
|
||||||
utilities = {
|
utilities = {
|
||||||
'shorten': utils.shorten,
|
"shorten": utils.shorten,
|
||||||
'can_colorize': utils.can_colorize,
|
"can_colorize": utils.can_colorize,
|
||||||
'colorize_diff': utils.colorize_diff,
|
"colorize_diff": utils.colorize_diff,
|
||||||
'colorize_blob': utils.colorize_blob,
|
"colorize_blob": utils.colorize_blob,
|
||||||
'can_markdown': utils.can_markdown,
|
"can_markdown": utils.can_markdown,
|
||||||
'markdown_blob': utils.markdown_blob,
|
"markdown_blob": utils.markdown_blob,
|
||||||
'can_embed_image': utils.can_embed_image,
|
"can_embed_image": utils.can_embed_image,
|
||||||
'embed_image_blob': utils.embed_image_blob,
|
"embed_image_blob": utils.embed_image_blob,
|
||||||
'is_binary': utils.is_binary,
|
"is_binary": utils.is_binary,
|
||||||
'hexdump': utils.hexdump,
|
"hexdump": utils.hexdump,
|
||||||
'abort': bottle.abort,
|
"abort": bottle.abort,
|
||||||
'smstr': git.smstr,
|
"smstr": git.smstr,
|
||||||
}
|
}
|
||||||
|
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
@ -210,48 +215,57 @@ def with_utils(f):
|
|||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
@bottle.route('/')
|
|
||||||
@bottle.view('index')
|
@bottle.route("/")
|
||||||
|
@bottle.view("index")
|
||||||
@with_utils
|
@with_utils
|
||||||
def index():
|
def index():
|
||||||
return dict(repos=repos)
|
return dict(repos=repos)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/')
|
|
||||||
@bottle.view('summary')
|
@bottle.route("/r/<repo:repo>/")
|
||||||
|
@bottle.view("summary")
|
||||||
@with_utils
|
@with_utils
|
||||||
def summary(repo):
|
def summary(repo):
|
||||||
return dict(repo=repo)
|
return dict(repo=repo)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/')
|
|
||||||
@bottle.view('commit')
|
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/")
|
||||||
|
@bottle.view("commit")
|
||||||
@with_utils
|
@with_utils
|
||||||
def commit(repo, cid):
|
def commit(repo, cid):
|
||||||
c = repo.commit(cid)
|
c = repo.commit(cid)
|
||||||
if not c:
|
if not c:
|
||||||
bottle.abort(404, 'Commit not found')
|
bottle.abort(404, "Commit not found")
|
||||||
|
|
||||||
return dict(repo=repo, c=c)
|
return dict(repo=repo, c=c)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>.patch')
|
|
||||||
@bottle.view('patch',
|
@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.
|
# Output is text/plain, don't do HTML escaping.
|
||||||
template_settings={"noescape": True})
|
template_settings={"noescape": True},
|
||||||
|
)
|
||||||
def patch(repo, cid):
|
def patch(repo, cid):
|
||||||
c = repo.commit(cid)
|
c = repo.commit(cid)
|
||||||
if not c:
|
if not c:
|
||||||
bottle.abort(404, 'Commit not found')
|
bottle.abort(404, "Commit not found")
|
||||||
|
|
||||||
bottle.response.content_type = 'text/plain; charset=utf8'
|
bottle.response.content_type = "text/plain; charset=utf8"
|
||||||
|
|
||||||
return dict(repo=repo, c=c)
|
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.route("/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html")
|
||||||
@bottle.view('blob')
|
@bottle.route(
|
||||||
|
"/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html"
|
||||||
|
)
|
||||||
|
@bottle.view("blob")
|
||||||
@with_utils
|
@with_utils
|
||||||
def blob(repo, bname, fname, dirname = ''):
|
def blob(repo, bname, fname, dirname=""):
|
||||||
if dirname and not dirname.endswith('/'):
|
if dirname and not dirname.endswith("/"):
|
||||||
dirname = dirname + '/'
|
dirname = dirname + "/"
|
||||||
|
|
||||||
dirname = git.smstr.from_url(dirname)
|
dirname = git.smstr.from_url(dirname)
|
||||||
fname = git.smstr.from_url(fname)
|
fname = git.smstr.from_url(fname)
|
||||||
@ -265,30 +279,35 @@ def blob(repo, bname, fname, dirname = ''):
|
|||||||
if content is None:
|
if content is None:
|
||||||
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
||||||
|
|
||||||
return dict(repo = repo, branch = bname, dirname = dirname, fname = fname,
|
return dict(
|
||||||
blob = content)
|
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.route("/r/<repo:repo>/b/<bname:path>/t/")
|
||||||
@bottle.view('tree')
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/")
|
||||||
|
@bottle.view("tree")
|
||||||
@with_utils
|
@with_utils
|
||||||
def tree(repo, bname, dirname = ''):
|
def tree(repo, bname, dirname=""):
|
||||||
if dirname and not dirname.endswith('/'):
|
if dirname and not dirname.endswith("/"):
|
||||||
dirname = dirname + '/'
|
dirname = dirname + "/"
|
||||||
|
|
||||||
dirname = git.smstr.from_url(dirname)
|
dirname = git.smstr.from_url(dirname)
|
||||||
|
|
||||||
return dict(repo = repo, branch = bname, tree = repo.tree(bname),
|
return dict(
|
||||||
dirname = dirname)
|
repo=repo, branch=bname, tree=repo.tree(bname), dirname=dirname
|
||||||
|
)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/')
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname:path>/<offset:int>.html')
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/")
|
||||||
@bottle.view('branch')
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/<offset:int>.html")
|
||||||
|
@bottle.view("branch")
|
||||||
@with_utils
|
@with_utils
|
||||||
def branch(repo, bname, offset=0):
|
def branch(repo, bname, offset=0):
|
||||||
return dict(repo=repo, branch=bname, offset=offset)
|
return dict(repo=repo, branch=bname, offset=offset)
|
||||||
|
|
||||||
@bottle.route('/static/<path:path>')
|
|
||||||
|
@bottle.route("/static/<path:path>")
|
||||||
def static(path):
|
def static(path):
|
||||||
return bottle.static_file(path, root=static_path)
|
return bottle.static_file(path, root=static_path)
|
||||||
|
|
||||||
@ -297,6 +316,7 @@ def static(path):
|
|||||||
# Static HTML generation
|
# Static HTML generation
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def is_404(e):
|
def is_404(e):
|
||||||
"""True if e is an HTTPError with status 404, False otherwise."""
|
"""True if e is an HTTPError with status 404, False otherwise."""
|
||||||
# We need this because older bottle.py versions put the status code in
|
# We need this because older bottle.py versions put the status code in
|
||||||
@ -307,10 +327,12 @@ def is_404(e):
|
|||||||
else:
|
else:
|
||||||
return e.status_code == 404
|
return e.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def generate(output, only=None):
|
def generate(output, only=None):
|
||||||
"""Generate static html to the output directory."""
|
"""Generate static html to the output directory."""
|
||||||
|
|
||||||
def write_to(path, func_or_str, args=(), mtime=None):
|
def write_to(path, func_or_str, args=(), mtime=None):
|
||||||
path = output + '/' + path
|
path = output + "/" + path
|
||||||
dirname = os.path.dirname(path)
|
dirname = os.path.dirname(path)
|
||||||
|
|
||||||
if not os.path.exists(dirname):
|
if not os.path.exists(dirname):
|
||||||
@ -346,71 +368,99 @@ def generate(output, only = None):
|
|||||||
print(path)
|
print(path)
|
||||||
s = func_or_str(*args)
|
s = func_or_str(*args)
|
||||||
|
|
||||||
open(path, 'w').write(s)
|
open(path, "w").write(s)
|
||||||
if mtime:
|
if mtime:
|
||||||
os.utime(path, (mtime, mtime))
|
os.utime(path, (mtime, mtime))
|
||||||
|
|
||||||
def link(from_path, to_path):
|
def link(from_path, to_path):
|
||||||
from_path = output + '/' + from_path
|
from_path = output + "/" + from_path
|
||||||
|
|
||||||
if os.path.lexists(from_path):
|
if os.path.lexists(from_path):
|
||||||
return
|
return
|
||||||
print(from_path, '->', to_path)
|
print(from_path, "->", to_path)
|
||||||
os.symlink(to_path, from_path)
|
os.symlink(to_path, from_path)
|
||||||
|
|
||||||
def write_tree(r, bn, mtime):
|
def write_tree(r, bn, mtime):
|
||||||
t = r.tree(bn)
|
t = r.tree(bn)
|
||||||
|
|
||||||
write_to('r/%s/b/%s/t/index.html' % (r.name, bn),
|
write_to("r/%s/b/%s/t/index.html" % (r.name, bn), tree, (r, bn), mtime)
|
||||||
tree, (r, bn), mtime)
|
|
||||||
|
|
||||||
for otype, oname, _ in t.ls('', recursive = True):
|
for otype, oname, _ in t.ls("", recursive=True):
|
||||||
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
||||||
# expected to fail for now; we skip them.
|
# expected to fail for now; we skip them.
|
||||||
if '\n' in oname.raw:
|
if "\n" in oname.raw:
|
||||||
print('skipping file with \\n: %r' % (oname.raw))
|
print("skipping file with \\n: %r" % (oname.raw))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if otype == 'blob':
|
if otype == "blob":
|
||||||
dirname = git.smstr(os.path.dirname(oname.raw))
|
dirname = git.smstr(os.path.dirname(oname.raw))
|
||||||
fname = git.smstr(os.path.basename(oname.raw))
|
fname = git.smstr(os.path.basename(oname.raw))
|
||||||
write_to(
|
write_to(
|
||||||
'r/%s/b/%s/t/%s%sf=%s.html' %
|
"r/%s/b/%s/t/%s%sf=%s.html"
|
||||||
(str(r.name), str(bn),
|
% (
|
||||||
dirname.raw, '/' if dirname.raw else '', fname.raw),
|
str(r.name),
|
||||||
blob, (r, bn, fname.url, dirname.url), mtime)
|
str(bn),
|
||||||
|
dirname.raw,
|
||||||
|
"/" if dirname.raw else "",
|
||||||
|
fname.raw,
|
||||||
|
),
|
||||||
|
blob,
|
||||||
|
(r, bn, fname.url, dirname.url),
|
||||||
|
mtime,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
write_to('r/%s/b/%s/t/%s/index.html' %
|
write_to(
|
||||||
(str(r.name), str(bn), oname.raw),
|
"r/%s/b/%s/t/%s/index.html"
|
||||||
tree, (r, bn, oname.url), mtime)
|
% (str(r.name), str(bn), oname.raw),
|
||||||
|
tree,
|
||||||
|
(r, bn, oname.url),
|
||||||
|
mtime,
|
||||||
|
)
|
||||||
|
|
||||||
# Always generate the index, to keep the "last updated" time fresh.
|
# Always generate the index, to keep the "last updated" time fresh.
|
||||||
write_to('index.html', index())
|
write_to("index.html", index())
|
||||||
|
|
||||||
# We can't call static() because it relies on HTTP headers.
|
# We can't call static() because it relies on HTTP headers.
|
||||||
read_f = lambda f: open(f).read()
|
read_f = lambda f: open(f).read()
|
||||||
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
write_to(
|
||||||
os.stat(static_path + '/git-arr.css').st_mtime)
|
"static/git-arr.css",
|
||||||
write_to('static/git-arr.js', read_f, [static_path + '/git-arr.js'],
|
read_f,
|
||||||
os.stat(static_path + '/git-arr.js').st_mtime)
|
[static_path + "/git-arr.css"],
|
||||||
write_to('static/syntax.css', read_f, [static_path + '/syntax.css'],
|
os.stat(static_path + "/git-arr.css").st_mtime,
|
||||||
os.stat(static_path + '/syntax.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,
|
||||||
|
)
|
||||||
|
|
||||||
rs = sorted(list(repos.values()), key=lambda r: r.name)
|
rs = sorted(list(repos.values()), key=lambda r: r.name)
|
||||||
if only:
|
if only:
|
||||||
rs = [r for r in rs if r.name in only]
|
rs = [r for r in rs if r.name in only]
|
||||||
|
|
||||||
for r in rs:
|
for r in rs:
|
||||||
write_to('r/%s/index.html' % r.name, summary(r))
|
write_to("r/%s/index.html" % r.name, summary(r))
|
||||||
for bn in r.branch_names():
|
for bn in r.branch_names():
|
||||||
commit_count = 0
|
commit_count = 0
|
||||||
commit_ids = r.commit_ids('refs/heads/' + bn,
|
commit_ids = r.commit_ids(
|
||||||
limit = r.info.commits_per_page * r.info.max_pages)
|
"refs/heads/" + bn,
|
||||||
|
limit=r.info.commits_per_page * r.info.max_pages,
|
||||||
|
)
|
||||||
for cid in commit_ids:
|
for cid in commit_ids:
|
||||||
write_to('r/%s/c/%s/index.html' % (r.name, cid),
|
write_to(
|
||||||
commit, (r, cid))
|
"r/%s/c/%s/index.html" % (r.name, cid), commit, (r, cid)
|
||||||
|
)
|
||||||
if r.info.generate_patch:
|
if r.info.generate_patch:
|
||||||
write_to('r/%s/c/%s.patch' % (r.name, cid), patch, (r, cid))
|
write_to(
|
||||||
|
"r/%s/c/%s.patch" % (r.name, cid), patch, (r, cid)
|
||||||
|
)
|
||||||
commit_count += 1
|
commit_count += 1
|
||||||
|
|
||||||
# To avoid regenerating files that have not changed, we will
|
# To avoid regenerating files that have not changed, we will
|
||||||
@ -419,65 +469,83 @@ def generate(output, only = None):
|
|||||||
# write.
|
# write.
|
||||||
branch_mtime = r.commit(bn).committer_date.epoch
|
branch_mtime = r.commit(bn).committer_date.epoch
|
||||||
|
|
||||||
nr_pages = int(math.ceil(
|
nr_pages = int(
|
||||||
float(commit_count) / r.info.commits_per_page))
|
math.ceil(float(commit_count) / r.info.commits_per_page)
|
||||||
|
)
|
||||||
nr_pages = min(nr_pages, r.info.max_pages)
|
nr_pages = min(nr_pages, r.info.max_pages)
|
||||||
|
|
||||||
for page in range(nr_pages):
|
for page in range(nr_pages):
|
||||||
write_to('r/%s/b/%s/%d.html' % (r.name, bn, page),
|
write_to(
|
||||||
branch, (r, bn, page), branch_mtime)
|
"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),
|
link(
|
||||||
to_path = '0.html')
|
from_path="r/%s/b/%s/index.html" % (r.name, bn),
|
||||||
|
to_path="0.html",
|
||||||
|
)
|
||||||
|
|
||||||
if r.info.generate_tree:
|
if r.info.generate_tree:
|
||||||
write_tree(r, bn, branch_mtime)
|
write_tree(r, bn, branch_mtime)
|
||||||
|
|
||||||
for tag_name, obj_id in r.tags():
|
for tag_name, obj_id in r.tags():
|
||||||
try:
|
try:
|
||||||
write_to('r/%s/c/%s/index.html' % (r.name, obj_id),
|
write_to(
|
||||||
commit, (r, obj_id))
|
"r/%s/c/%s/index.html" % (r.name, obj_id),
|
||||||
|
commit,
|
||||||
|
(r, obj_id),
|
||||||
|
)
|
||||||
except bottle.HTTPError as e:
|
except bottle.HTTPError as e:
|
||||||
# Some repos can have tags pointing to non-commits. This
|
# Some repos can have tags pointing to non-commits. This
|
||||||
# happens in the Linux Kernel's v2.6.11, which points directly
|
# happens in the Linux Kernel's v2.6.11, which points directly
|
||||||
# to a tree. Ignore them.
|
# to a tree. Ignore them.
|
||||||
if is_404(e):
|
if is_404(e):
|
||||||
print('404 in tag %s (%s)' % (tag_name, obj_id))
|
print("404 in tag %s (%s)" % (tag_name, obj_id))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = optparse.OptionParser('usage: %prog [options] serve|generate')
|
parser = optparse.OptionParser("usage: %prog [options] serve|generate")
|
||||||
parser.add_option('-c', '--config', metavar = 'FILE',
|
parser.add_option(
|
||||||
help = 'configuration file')
|
"-c", "--config", metavar="FILE", help="configuration file"
|
||||||
parser.add_option('-o', '--output', metavar = 'DIR',
|
)
|
||||||
help = 'output directory (for generate)')
|
parser.add_option(
|
||||||
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
|
"-o", "--output", metavar="DIR", help="output directory (for generate)"
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
"",
|
||||||
|
"--only",
|
||||||
|
metavar="REPO",
|
||||||
|
action="append",
|
||||||
default=[],
|
default=[],
|
||||||
help = 'generate/serve only this repository')
|
help="generate/serve only this repository",
|
||||||
|
)
|
||||||
opts, args = parser.parse_args()
|
opts, args = parser.parse_args()
|
||||||
|
|
||||||
if not opts.config:
|
if not opts.config:
|
||||||
parser.error('--config is mandatory')
|
parser.error("--config is mandatory")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
load_config(opts.config)
|
load_config(opts.config)
|
||||||
except (configparser.NoOptionError, ValueError) as e:
|
except (configparser.NoOptionError, ValueError) as e:
|
||||||
print('Error parsing config:', e)
|
print("Error parsing config:", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
parser.error('Must specify an action (serve|generate)')
|
parser.error("Must specify an action (serve|generate)")
|
||||||
|
|
||||||
if args[0] == 'serve':
|
if args[0] == "serve":
|
||||||
bottle.run(host = 'localhost', port = 8008, reloader = True)
|
bottle.run(host="localhost", port=8008, reloader=True)
|
||||||
elif args[0] == 'generate':
|
elif args[0] == "generate":
|
||||||
if not opts.output:
|
if not opts.output:
|
||||||
parser.error('Must specify --output')
|
parser.error("Must specify --output")
|
||||||
generate(output=opts.output, only=opts.only)
|
generate(output=opts.output, only=opts.only)
|
||||||
else:
|
else:
|
||||||
parser.error('Unknown action %s' % args[0])
|
parser.error("Unknown action %s" % args[0])
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
216
git.py
216
git.py
@ -19,37 +19,45 @@ from html import escape
|
|||||||
# Path to the git binary.
|
# Path to the git binary.
|
||||||
GIT_BIN = "git"
|
GIT_BIN = "git"
|
||||||
|
|
||||||
|
|
||||||
def run_git(repo_path, params, stdin=None, silent_stderr=False, raw=False):
|
def run_git(repo_path, params, stdin=None, silent_stderr=False, raw=False):
|
||||||
"""Invokes git with the given parameters.
|
"""Invokes git with the given parameters.
|
||||||
|
|
||||||
This function invokes git with the given parameters, and returns a
|
This function invokes git with the given parameters, and returns a
|
||||||
file-like object with the output (from a pipe).
|
file-like object with the output (from a pipe).
|
||||||
"""
|
"""
|
||||||
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
|
params = [GIT_BIN, "--git-dir=%s" % repo_path] + list(params)
|
||||||
|
|
||||||
stderr = None
|
stderr = None
|
||||||
if silent_stderr:
|
if silent_stderr:
|
||||||
stderr = subprocess.PIPE
|
stderr = subprocess.PIPE
|
||||||
|
|
||||||
if not stdin:
|
if not stdin:
|
||||||
p = subprocess.Popen(params,
|
p = subprocess.Popen(
|
||||||
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
|
params, stdin=None, stdout=subprocess.PIPE, stderr=stderr
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
p = subprocess.Popen(params,
|
p = subprocess.Popen(
|
||||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
|
params,
|
||||||
stderr = stderr)
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=stderr,
|
||||||
|
)
|
||||||
|
|
||||||
p.stdin.write(stdin)
|
p.stdin.write(stdin)
|
||||||
p.stdin.close()
|
p.stdin.close()
|
||||||
|
|
||||||
if raw:
|
if raw:
|
||||||
return p.stdout
|
return p.stdout
|
||||||
|
|
||||||
return io.TextIOWrapper(p.stdout, encoding = 'utf8',
|
return io.TextIOWrapper(
|
||||||
errors = 'backslashreplace')
|
p.stdout, encoding="utf8", errors="backslashreplace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GitCommand(object):
|
class GitCommand(object):
|
||||||
"""Convenient way of invoking git."""
|
"""Convenient way of invoking git."""
|
||||||
|
|
||||||
def __init__(self, path, cmd, *args, **kwargs):
|
def __init__(self, path, cmd, *args, **kwargs):
|
||||||
self._override = True
|
self._override = True
|
||||||
self._path = path
|
self._path = path
|
||||||
@ -63,10 +71,10 @@ class GitCommand (object):
|
|||||||
self.__setattr__(k, v)
|
self.__setattr__(k, v)
|
||||||
|
|
||||||
def __setattr__(self, k, v):
|
def __setattr__(self, k, v):
|
||||||
if k == '_override' or self._override:
|
if k == "_override" or self._override:
|
||||||
self.__dict__[k] = v
|
self.__dict__[k] = v
|
||||||
return
|
return
|
||||||
k = k.replace('_', '-')
|
k = k.replace("_", "-")
|
||||||
self._kwargs[k] = v
|
self._kwargs[k] = v
|
||||||
|
|
||||||
def arg(self, a):
|
def arg(self, a):
|
||||||
@ -92,11 +100,11 @@ class GitCommand (object):
|
|||||||
params = [self._cmd]
|
params = [self._cmd]
|
||||||
|
|
||||||
for k, v in list(self._kwargs.items()):
|
for k, v in list(self._kwargs.items()):
|
||||||
dash = '--' if len(k) > 1 else '-'
|
dash = "--" if len(k) > 1 else "-"
|
||||||
if v is None:
|
if v is None:
|
||||||
params.append('%s%s' % (dash, k))
|
params.append("%s%s" % (dash, k))
|
||||||
else:
|
else:
|
||||||
params.append('%s%s=%s' % (dash, k, str(v)))
|
params.append("%s%s=%s" % (dash, k, str(v)))
|
||||||
|
|
||||||
params.extend(self._args)
|
params.extend(self._args)
|
||||||
|
|
||||||
@ -105,6 +113,7 @@ class GitCommand (object):
|
|||||||
|
|
||||||
class SimpleNamespace(object):
|
class SimpleNamespace(object):
|
||||||
"""An entirely flexible object, which provides a convenient namespace."""
|
"""An entirely flexible object, which provides a convenient namespace."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
@ -120,14 +129,15 @@ class smstr:
|
|||||||
readable.
|
readable.
|
||||||
.html -> an HTML-embeddable representation.
|
.html -> an HTML-embeddable representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
if not isinstance(raw, (str, bytes)):
|
if not isinstance(raw, (str, bytes)):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"The raw string must be instance of 'str', not %s" %
|
"The raw string must be instance of 'str', not %s" % type(raw)
|
||||||
type(raw))
|
)
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
if isinstance(raw, bytes):
|
if isinstance(raw, bytes):
|
||||||
self.unicode = raw.decode('utf8', errors = 'backslashreplace')
|
self.unicode = raw.decode("utf8", errors="backslashreplace")
|
||||||
else:
|
else:
|
||||||
self.unicode = raw
|
self.unicode = raw
|
||||||
self.url = urllib.request.pathname2url(raw)
|
self.url = urllib.request.pathname2url(raw)
|
||||||
@ -156,9 +166,9 @@ class smstr:
|
|||||||
|
|
||||||
def _to_html(self):
|
def _to_html(self):
|
||||||
"""Returns an html representation of the unicode string."""
|
"""Returns an html representation of the unicode string."""
|
||||||
html = ''
|
html = ""
|
||||||
for c in escape(self.unicode):
|
for c in escape(self.unicode):
|
||||||
if c in '\t\r\n\r\f\a\b\v\0':
|
if c in "\t\r\n\r\f\a\b\v\0":
|
||||||
esc_c = c.encode("unicode-escape").decode("utf8")
|
esc_c = c.encode("unicode-escape").decode("utf8")
|
||||||
html += '<span class="ctrlchr">%s</span>' % esc_c
|
html += '<span class="ctrlchr">%s</span>' % esc_c
|
||||||
else:
|
else:
|
||||||
@ -186,7 +196,7 @@ def unquote(s):
|
|||||||
s = s.encode("latin1").decode("unicode-escape")
|
s = s.encode("latin1").decode("unicode-escape")
|
||||||
|
|
||||||
# Convert to utf8.
|
# Convert to utf8.
|
||||||
s = s.encode("latin1").decode("utf8", errors='backslashreplace')
|
s = s.encode("latin1").decode("utf8", errors="backslashreplace")
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@ -205,7 +215,7 @@ class Repo:
|
|||||||
|
|
||||||
def for_each_ref(self, pattern=None, sort=None, count=None):
|
def for_each_ref(self, pattern=None, sort=None, count=None):
|
||||||
"""Returns a list of references."""
|
"""Returns a list of references."""
|
||||||
cmd = self.cmd('for-each-ref')
|
cmd = self.cmd("for-each-ref")
|
||||||
if sort:
|
if sort:
|
||||||
cmd.sort = sort
|
cmd.sort = sort
|
||||||
if count:
|
if count:
|
||||||
@ -217,21 +227,21 @@ class Repo:
|
|||||||
obj_id, obj_type, ref = l.split()
|
obj_id, obj_type, ref = l.split()
|
||||||
yield obj_id, obj_type, ref
|
yield obj_id, obj_type, ref
|
||||||
|
|
||||||
def branches(self, sort = '-authordate'):
|
def branches(self, sort="-authordate"):
|
||||||
"""Get the (name, obj_id) of the branches."""
|
"""Get the (name, obj_id) of the branches."""
|
||||||
refs = self.for_each_ref(pattern = 'refs/heads/', sort = sort)
|
refs = self.for_each_ref(pattern="refs/heads/", sort=sort)
|
||||||
for obj_id, _, ref in refs:
|
for obj_id, _, ref in refs:
|
||||||
yield ref[len('refs/heads/'):], obj_id
|
yield ref[len("refs/heads/") :], obj_id
|
||||||
|
|
||||||
def branch_names(self):
|
def branch_names(self):
|
||||||
"""Get the names of the branches."""
|
"""Get the names of the branches."""
|
||||||
return (name for name, _ in self.branches())
|
return (name for name, _ in self.branches())
|
||||||
|
|
||||||
def tags(self, sort = '-taggerdate'):
|
def tags(self, sort="-taggerdate"):
|
||||||
"""Get the (name, obj_id) of the tags."""
|
"""Get the (name, obj_id) of the tags."""
|
||||||
refs = self.for_each_ref(pattern = 'refs/tags/', sort = sort)
|
refs = self.for_each_ref(pattern="refs/tags/", sort=sort)
|
||||||
for obj_id, _, ref in refs:
|
for obj_id, _, ref in refs:
|
||||||
yield ref[len('refs/tags/'):], obj_id
|
yield ref[len("refs/tags/") :], obj_id
|
||||||
|
|
||||||
def tag_names(self):
|
def tag_names(self):
|
||||||
"""Get the names of the tags."""
|
"""Get the names of the tags."""
|
||||||
@ -239,15 +249,15 @@ class Repo:
|
|||||||
|
|
||||||
def commit_ids(self, ref, limit=None):
|
def commit_ids(self, ref, limit=None):
|
||||||
"""Generate commit ids."""
|
"""Generate commit ids."""
|
||||||
cmd = self.cmd('rev-list')
|
cmd = self.cmd("rev-list")
|
||||||
if limit:
|
if limit:
|
||||||
cmd.max_count = limit
|
cmd.max_count = limit
|
||||||
|
|
||||||
cmd.arg(ref)
|
cmd.arg(ref)
|
||||||
cmd.arg('--')
|
cmd.arg("--")
|
||||||
|
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
yield l.rstrip('\n')
|
yield l.rstrip("\n")
|
||||||
|
|
||||||
def commit(self, commit_id):
|
def commit(self, commit_id):
|
||||||
"""Return a single commit."""
|
"""Return a single commit."""
|
||||||
@ -258,20 +268,20 @@ class Repo:
|
|||||||
|
|
||||||
def commits(self, ref, limit=None, offset=0):
|
def commits(self, ref, limit=None, offset=0):
|
||||||
"""Generate commit objects for the ref."""
|
"""Generate commit objects for the ref."""
|
||||||
cmd = self.cmd('rev-list')
|
cmd = self.cmd("rev-list")
|
||||||
if limit:
|
if limit:
|
||||||
cmd.max_count = limit + offset
|
cmd.max_count = limit + offset
|
||||||
|
|
||||||
cmd.header = None
|
cmd.header = None
|
||||||
|
|
||||||
cmd.arg(ref)
|
cmd.arg(ref)
|
||||||
cmd.arg('--')
|
cmd.arg("--")
|
||||||
|
|
||||||
info_buffer = ''
|
info_buffer = ""
|
||||||
count = 0
|
count = 0
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
if '\0' in l:
|
if "\0" in l:
|
||||||
pre, post = l.split('\0', 1)
|
pre, post = l.split("\0", 1)
|
||||||
info_buffer += pre
|
info_buffer += pre
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
@ -290,11 +300,11 @@ class Repo:
|
|||||||
|
|
||||||
def diff(self, ref):
|
def diff(self, ref):
|
||||||
"""Return a Diff object for the ref."""
|
"""Return a Diff object for the ref."""
|
||||||
cmd = self.cmd('diff-tree')
|
cmd = self.cmd("diff-tree")
|
||||||
cmd.patch = None
|
cmd.patch = None
|
||||||
cmd.numstat = None
|
cmd.numstat = None
|
||||||
cmd.find_renames = None
|
cmd.find_renames = None
|
||||||
if (self.info.root_diff):
|
if self.info.root_diff:
|
||||||
cmd.root = None
|
cmd.root = None
|
||||||
# Note we intentionally do not use -z, as the filename is just for
|
# Note we intentionally do not use -z, as the filename is just for
|
||||||
# reference, and it is safer to let git do the escaping.
|
# reference, and it is safer to let git do the escaping.
|
||||||
@ -305,13 +315,13 @@ class Repo:
|
|||||||
|
|
||||||
def refs(self):
|
def refs(self):
|
||||||
"""Return a dict of obj_id -> ref."""
|
"""Return a dict of obj_id -> ref."""
|
||||||
cmd = self.cmd('show-ref')
|
cmd = self.cmd("show-ref")
|
||||||
cmd.dereference = None
|
cmd.dereference = None
|
||||||
|
|
||||||
r = defaultdict(list)
|
r = defaultdict(list)
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
l = l.strip()
|
l = l.strip()
|
||||||
obj_id, ref = l.split(' ', 1)
|
obj_id, ref = l.split(" ", 1)
|
||||||
r[obj_id].append(ref)
|
r[obj_id].append(ref)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
@ -322,9 +332,9 @@ class Repo:
|
|||||||
|
|
||||||
def blob(self, path, ref):
|
def blob(self, path, ref):
|
||||||
"""Returns a Blob instance for the given path."""
|
"""Returns a Blob instance for the given path."""
|
||||||
cmd = self.cmd('cat-file')
|
cmd = self.cmd("cat-file")
|
||||||
cmd.raw(True)
|
cmd.raw(True)
|
||||||
cmd.batch = '%(objectsize)'
|
cmd.batch = "%(objectsize)"
|
||||||
|
|
||||||
# Format: <ref>:<path>
|
# Format: <ref>:<path>
|
||||||
# Construct it in binary since the path might not be utf8.
|
# Construct it in binary since the path might not be utf8.
|
||||||
@ -332,15 +342,16 @@ class Repo:
|
|||||||
|
|
||||||
out = cmd.run()
|
out = cmd.run()
|
||||||
head = out.readline()
|
head = out.readline()
|
||||||
if not head or head.strip().endswith(b'missing'):
|
if not head or head.strip().endswith(b"missing"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Blob(out.read()[: int(head)])
|
return Blob(out.read()[: int(head)])
|
||||||
|
|
||||||
def last_commit_timestamp(self):
|
def last_commit_timestamp(self):
|
||||||
"""Return the timestamp of the last commit."""
|
"""Return the timestamp of the last commit."""
|
||||||
refs = self.for_each_ref(pattern = 'refs/heads/',
|
refs = self.for_each_ref(
|
||||||
sort = '-committerdate', count = 1)
|
pattern="refs/heads/", sort="-committerdate", count=1
|
||||||
|
)
|
||||||
for obj_id, _, _ in refs:
|
for obj_id, _, _ in refs:
|
||||||
commit = self.commit(obj_id)
|
commit = self.commit(obj_id)
|
||||||
return commit.committer_epoch
|
return commit.committer_epoch
|
||||||
@ -350,11 +361,20 @@ class Repo:
|
|||||||
class Commit(object):
|
class Commit(object):
|
||||||
"""A git commit."""
|
"""A git commit."""
|
||||||
|
|
||||||
def __init__(self, repo,
|
def __init__(
|
||||||
commit_id, parents, tree,
|
self,
|
||||||
author, author_epoch, author_tz,
|
repo,
|
||||||
committer, committer_epoch, committer_tz,
|
commit_id,
|
||||||
message):
|
parents,
|
||||||
|
tree,
|
||||||
|
author,
|
||||||
|
author_epoch,
|
||||||
|
author_tz,
|
||||||
|
committer,
|
||||||
|
committer_epoch,
|
||||||
|
committer_tz,
|
||||||
|
message,
|
||||||
|
):
|
||||||
self._repo = repo
|
self._repo = repo
|
||||||
self.id = commit_id
|
self.id = commit_id
|
||||||
self.parents = parents
|
self.parents = parents
|
||||||
@ -367,28 +387,30 @@ class Commit (object):
|
|||||||
self.committer_tz = committer_tz
|
self.committer_tz = committer_tz
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
self.author_name, self.author_email = \
|
self.author_name, self.author_email = email.utils.parseaddr(
|
||||||
email.utils.parseaddr(self.author)
|
self.author
|
||||||
|
)
|
||||||
|
|
||||||
self.committer_name, self.committer_email = \
|
self.committer_name, self.committer_email = email.utils.parseaddr(
|
||||||
email.utils.parseaddr(self.committer)
|
self.committer
|
||||||
|
)
|
||||||
|
|
||||||
self.subject, self.body = self.message.split('\n', 1)
|
self.subject, self.body = self.message.split("\n", 1)
|
||||||
|
|
||||||
self.author_date = Date(self.author_epoch, self.author_tz)
|
self.author_date = Date(self.author_epoch, self.author_tz)
|
||||||
self.committer_date = Date(self.committer_epoch, self.committer_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
|
# Only get this lazily when we need it; most of the time it's not
|
||||||
# required by the caller.
|
# required by the caller.
|
||||||
self._diff = None
|
self._diff = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<C %s p:%s a:%s s:%r>' % (
|
return "<C %s p:%s a:%s s:%r>" % (
|
||||||
self.id[:7],
|
self.id[:7],
|
||||||
','.join(p[:7] for p in self.parents),
|
",".join(p[:7] for p in self.parents),
|
||||||
self.author_email,
|
self.author_email,
|
||||||
self.subject[:20])
|
self.subject[:20],
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diff(self):
|
def diff(self):
|
||||||
@ -400,57 +422,68 @@ class Commit (object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_str(repo, buf):
|
def from_str(repo, buf):
|
||||||
"""Parses git rev-list output, returns a commit object."""
|
"""Parses git rev-list output, returns a commit object."""
|
||||||
if '\n\n' in buf:
|
if "\n\n" in buf:
|
||||||
# Header, commit message
|
# Header, commit message
|
||||||
header, raw_message = buf.split('\n\n', 1)
|
header, raw_message = buf.split("\n\n", 1)
|
||||||
else:
|
else:
|
||||||
# Header only, no commit message
|
# Header only, no commit message
|
||||||
header, raw_message = buf.rstrip(), ' '
|
header, raw_message = buf.rstrip(), " "
|
||||||
|
|
||||||
header_lines = header.split('\n')
|
header_lines = header.split("\n")
|
||||||
commit_id = header_lines.pop(0)
|
commit_id = header_lines.pop(0)
|
||||||
|
|
||||||
header_dict = defaultdict(list)
|
header_dict = defaultdict(list)
|
||||||
for line in header_lines:
|
for line in header_lines:
|
||||||
k, v = line.split(' ', 1)
|
k, v = line.split(" ", 1)
|
||||||
header_dict[k].append(v)
|
header_dict[k].append(v)
|
||||||
|
|
||||||
tree = header_dict['tree'][0]
|
tree = header_dict["tree"][0]
|
||||||
parents = set(header_dict['parent'])
|
parents = set(header_dict["parent"])
|
||||||
author, author_epoch, author_tz = \
|
|
||||||
header_dict['author'][0].rsplit(' ', 2)
|
authorhdr = header_dict["author"][0]
|
||||||
committer, committer_epoch, committer_tz = \
|
author, author_epoch, author_tz = authorhdr.rsplit(" ", 2)
|
||||||
header_dict['committer'][0].rsplit(' ', 2)
|
|
||||||
|
committerhdr = header_dict["committer"][0]
|
||||||
|
committer, committer_epoch, committer_tz = committerhdr.rsplit(" ", 2)
|
||||||
|
|
||||||
# Remove the first four spaces from the message's lines.
|
# Remove the first four spaces from the message's lines.
|
||||||
message = ''
|
message = ""
|
||||||
for line in raw_message.split('\n'):
|
for line in raw_message.split("\n"):
|
||||||
message += line[4:] + '\n'
|
message += line[4:] + "\n"
|
||||||
|
|
||||||
return Commit(repo,
|
return Commit(
|
||||||
commit_id = commit_id, tree = tree, parents = parents,
|
repo,
|
||||||
|
commit_id=commit_id,
|
||||||
|
tree=tree,
|
||||||
|
parents=parents,
|
||||||
author=author,
|
author=author,
|
||||||
author_epoch = author_epoch, author_tz = author_tz,
|
author_epoch=author_epoch,
|
||||||
|
author_tz=author_tz,
|
||||||
committer=committer,
|
committer=committer,
|
||||||
committer_epoch = committer_epoch, committer_tz = committer_tz,
|
committer_epoch=committer_epoch,
|
||||||
message = message)
|
committer_tz=committer_tz,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Date:
|
class Date:
|
||||||
"""Handy representation for a datetime from git."""
|
"""Handy representation for a datetime from git."""
|
||||||
|
|
||||||
def __init__(self, epoch, tz):
|
def __init__(self, epoch, tz):
|
||||||
self.epoch = int(epoch)
|
self.epoch = int(epoch)
|
||||||
self.tz = tz
|
self.tz = tz
|
||||||
self.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
self.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
||||||
|
|
||||||
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
||||||
if tz[0] == '-':
|
if tz[0] == "-":
|
||||||
self.tz_sec_offset_min = -self.tz_sec_offset_min
|
self.tz_sec_offset_min = -self.tz_sec_offset_min
|
||||||
|
|
||||||
self.local = self.utc + datetime.timedelta(
|
self.local = self.utc + datetime.timedelta(
|
||||||
minutes = self.tz_sec_offset_min)
|
minutes=self.tz_sec_offset_min
|
||||||
|
)
|
||||||
|
|
||||||
self.str = self.utc.strftime('%a, %d %b %Y %H:%M:%S +0000 ')
|
self.str = self.utc.strftime("%a, %d %b %Y %H:%M:%S +0000 ")
|
||||||
self.str += '(%s %s)' % (self.local.strftime('%H:%M'), self.tz)
|
self.str += "(%s %s)" % (self.local.strftime("%H:%M"), self.tz)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.str
|
return self.str
|
||||||
@ -458,6 +491,7 @@ class Date:
|
|||||||
|
|
||||||
class Diff:
|
class Diff:
|
||||||
"""A diff between two trees."""
|
"""A diff between two trees."""
|
||||||
|
|
||||||
def __init__(self, ref, changes, body):
|
def __init__(self, ref, changes, body):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
@ -477,23 +511,23 @@ class Diff:
|
|||||||
ref_id = next(lines)
|
ref_id = next(lines)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
# No diff; this can happen in merges without conflicts.
|
# No diff; this can happen in merges without conflicts.
|
||||||
return Diff(None, [], '')
|
return Diff(None, [], "")
|
||||||
|
|
||||||
# First, --numstat information.
|
# First, --numstat information.
|
||||||
changes = []
|
changes = []
|
||||||
l = next(lines)
|
l = next(lines)
|
||||||
while l != '\n':
|
while l != "\n":
|
||||||
l = l.rstrip('\n')
|
l = l.rstrip("\n")
|
||||||
added, deleted, fname = l.split('\t', 2)
|
added, deleted, fname = l.split("\t", 2)
|
||||||
added = added.replace('-', '0')
|
added = added.replace("-", "0")
|
||||||
deleted = deleted.replace('-', '0')
|
deleted = deleted.replace("-", "0")
|
||||||
fname = smstr(unquote(fname))
|
fname = smstr(unquote(fname))
|
||||||
changes.append((int(added), int(deleted), fname))
|
changes.append((int(added), int(deleted), fname))
|
||||||
l = next(lines)
|
l = next(lines)
|
||||||
|
|
||||||
# And now the diff body. We just store as-is, we don't really care for
|
# And now the diff body. We just store as-is, we don't really care for
|
||||||
# the contents.
|
# the contents.
|
||||||
body = ''.join(lines)
|
body = "".join(lines)
|
||||||
|
|
||||||
return Diff(ref_id, changes, body)
|
return Diff(ref_id, changes, body)
|
||||||
|
|
||||||
@ -507,7 +541,7 @@ class Tree:
|
|||||||
|
|
||||||
def ls(self, path, recursive=False):
|
def ls(self, path, recursive=False):
|
||||||
"""Generates (type, name, size) for each file in path."""
|
"""Generates (type, name, size) for each file in path."""
|
||||||
cmd = self.repo.cmd('ls-tree')
|
cmd = self.repo.cmd("ls-tree")
|
||||||
cmd.long = None
|
cmd.long = None
|
||||||
if recursive:
|
if recursive:
|
||||||
cmd.r = None
|
cmd.r = None
|
||||||
@ -521,13 +555,13 @@ class Tree:
|
|||||||
|
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
_mode, otype, _oid, size, name = l.split(None, 4)
|
_mode, otype, _oid, size, name = l.split(None, 4)
|
||||||
if size == '-':
|
if size == "-":
|
||||||
size = None
|
size = None
|
||||||
else:
|
else:
|
||||||
size = int(size)
|
size = int(size)
|
||||||
|
|
||||||
# Remove the quoting (if any); will always give us a str.
|
# Remove the quoting (if any); will always give us a str.
|
||||||
name = unquote(name.strip('\n'))
|
name = unquote(name.strip("\n"))
|
||||||
|
|
||||||
# Strip the leading path, the caller knows it and it's often
|
# Strip the leading path, the caller knows it and it's often
|
||||||
# easier to work with this way.
|
# easier to work with this way.
|
||||||
@ -548,5 +582,5 @@ class Blob:
|
|||||||
@property
|
@property
|
||||||
def utf8_content(self):
|
def utf8_content(self):
|
||||||
if not self._utf8_content:
|
if not self._utf8_content:
|
||||||
self._utf8_content = self.raw_content.decode('utf8', 'replace')
|
self._utf8_content = self.raw_content.decode("utf8", "replace")
|
||||||
return self._utf8_content
|
return self._utf8_content
|
||||||
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
include = "(git-arr|git.py|utils.py)$"
|
62
utils.py
62
utils.py
@ -23,11 +23,13 @@ import mimetypes
|
|||||||
import string
|
import string
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
def shorten(s, width=60):
|
def shorten(s, width=60):
|
||||||
if len(s) < 60:
|
if len(s) < 60:
|
||||||
return s
|
return s
|
||||||
return s[:57] + "..."
|
return s[:57] + "..."
|
||||||
|
|
||||||
|
|
||||||
def can_colorize(s):
|
def can_colorize(s):
|
||||||
"""True if we can colorize the string, False otherwise."""
|
"""True if we can colorize the string, False otherwise."""
|
||||||
if pygments is None:
|
if pygments is None:
|
||||||
@ -41,7 +43,7 @@ def can_colorize(s):
|
|||||||
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
||||||
start = 0
|
start = 0
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
pos = s.find('\n', start)
|
pos = s.find("\n", start)
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -51,6 +53,7 @@ def can_colorize(s):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def can_markdown(repo, fname):
|
def can_markdown(repo, fname):
|
||||||
"""True if we can process file through markdown, False otherwise."""
|
"""True if we can process file through markdown, False otherwise."""
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
@ -61,43 +64,49 @@ def can_markdown(repo, fname):
|
|||||||
|
|
||||||
return fname.endswith(".md")
|
return fname.endswith(".md")
|
||||||
|
|
||||||
|
|
||||||
def can_embed_image(repo, fname):
|
def can_embed_image(repo, fname):
|
||||||
"""True if we can embed image file in HTML, False otherwise."""
|
"""True if we can embed image file in HTML, False otherwise."""
|
||||||
if not repo.info.embed_images:
|
if not repo.info.embed_images:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return (('.' in fname) and
|
return ("." in fname) and (
|
||||||
(fname.split('.')[-1].lower() in [ 'jpg', 'jpeg', 'png', 'gif' ]))
|
fname.split(".")[-1].lower() in ["jpg", "jpeg", "png", "gif"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def colorize_diff(s):
|
def colorize_diff(s):
|
||||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
lexer = lexers.DiffLexer(encoding="utf-8")
|
||||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
|
||||||
cssclass = 'source_code')
|
|
||||||
|
|
||||||
return highlight(s, lexer, formatter)
|
return highlight(s, lexer, formatter)
|
||||||
|
|
||||||
|
|
||||||
def colorize_blob(fname, s):
|
def colorize_blob(fname, s):
|
||||||
try:
|
try:
|
||||||
lexer = lexers.guess_lexer_for_filename(fname, s, encoding = 'utf-8')
|
lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8")
|
||||||
except lexers.ClassNotFound:
|
except lexers.ClassNotFound:
|
||||||
# Only try to guess lexers if the file starts with a shebang,
|
# 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
|
# otherwise it's likely a text file and guess_lexer() is prone to
|
||||||
# make mistakes with those.
|
# make mistakes with those.
|
||||||
lexer = lexers.TextLexer(encoding = 'utf-8')
|
lexer = lexers.TextLexer(encoding="utf-8")
|
||||||
if s.startswith('#!'):
|
if s.startswith("#!"):
|
||||||
try:
|
try:
|
||||||
lexer = lexers.guess_lexer(s[:80], encoding = 'utf-8')
|
lexer = lexers.guess_lexer(s[:80], encoding="utf-8")
|
||||||
except lexers.ClassNotFound:
|
except lexers.ClassNotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
formatter = HtmlFormatter(
|
||||||
cssclass = 'source_code',
|
encoding="utf-8",
|
||||||
linenos = 'table',
|
cssclass="source_code",
|
||||||
|
linenos="table",
|
||||||
anchorlinenos=True,
|
anchorlinenos=True,
|
||||||
lineanchors = 'line')
|
lineanchors="line",
|
||||||
|
)
|
||||||
|
|
||||||
return highlight(s, lexer, formatter)
|
return highlight(s, lexer, formatter)
|
||||||
|
|
||||||
|
|
||||||
def markdown_blob(s):
|
def markdown_blob(s):
|
||||||
extensions = [
|
extensions = [
|
||||||
"markdown.extensions.fenced_code",
|
"markdown.extensions.fenced_code",
|
||||||
@ -106,30 +115,35 @@ def markdown_blob(s):
|
|||||||
]
|
]
|
||||||
return markdown.markdown(s, extensions=extensions)
|
return markdown.markdown(s, extensions=extensions)
|
||||||
|
|
||||||
|
|
||||||
def embed_image_blob(fname, image_data):
|
def embed_image_blob(fname, image_data):
|
||||||
mimetype = mimetypes.guess_type(fname)[0]
|
mimetype = mimetypes.guess_type(fname)[0]
|
||||||
b64img = base64.b64encode(image_data).decode("ascii")
|
b64img = base64.b64encode(image_data).decode("ascii")
|
||||||
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format( \
|
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format(
|
||||||
mimetype, b64img)
|
mimetype, b64img
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_binary(s):
|
def is_binary(s):
|
||||||
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
||||||
return b'\0' in s[:8192]
|
return b"\0" in s[:8192]
|
||||||
|
|
||||||
|
|
||||||
def hexdump(s):
|
def hexdump(s):
|
||||||
graph = string.ascii_letters + string.digits + string.punctuation + ' '
|
graph = string.ascii_letters + string.digits + string.punctuation + " "
|
||||||
s = s.decode("latin1")
|
s = s.decode("latin1")
|
||||||
offset = 0
|
offset = 0
|
||||||
while s:
|
while s:
|
||||||
t = s[:16]
|
t = s[:16]
|
||||||
hexvals = ['%.2x' % ord(c) for c in t]
|
hexvals = ["%.2x" % ord(c) for c in t]
|
||||||
text = ''.join(c if c in graph else '.' 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
|
yield offset, " ".join(hexvals[:8]), " ".join(hexvals[8:]), text
|
||||||
offset += 16
|
offset += 16
|
||||||
s = s[16:]
|
s = s[16:]
|
||||||
|
|
||||||
|
|
||||||
if markdown:
|
if markdown:
|
||||||
|
|
||||||
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
||||||
"""Rewrites relative links to files, to match git-arr's links.
|
"""Rewrites relative links to files, to match git-arr's links.
|
||||||
|
|
||||||
@ -139,6 +153,7 @@ if markdown:
|
|||||||
Note that we're already assuming a degree of sanity in the HTML, so we
|
Note that we're already assuming a degree of sanity in the HTML, so we
|
||||||
don't re-check that the path is reasonable.
|
don't re-check that the path is reasonable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self, root):
|
def run(self, root):
|
||||||
for child in root:
|
for child in root:
|
||||||
if child.tag == "a":
|
if child.tag == "a":
|
||||||
@ -159,9 +174,8 @@ if markdown:
|
|||||||
new_target = os.path.join(head, "f=" + tail + ".html")
|
new_target = os.path.join(head, "f=" + tail + ".html")
|
||||||
tag.set("href", new_target)
|
tag.set("href", new_target)
|
||||||
|
|
||||||
|
|
||||||
class RewriteLocalLinksExtension(markdown.Extension):
|
class RewriteLocalLinksExtension(markdown.Extension):
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md, md_globals):
|
||||||
md.treeprocessors.add(
|
md.treeprocessors.add(
|
||||||
"RewriteLocalLinks", RewriteLocalLinks(), "_end")
|
"RewriteLocalLinks", RewriteLocalLinks(), "_end"
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user