Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff4ff6757 | ||
|
|
6ea59bad51 | ||
|
|
4b1e1eb84c | ||
|
|
518188288e | ||
|
|
15547b2796 | ||
|
|
9f3df4899f | ||
|
|
bc1ee87dfe | ||
|
|
0d61bbf7f5 | ||
|
|
78f1b6def0 | ||
|
|
4cb2f59dd4 | ||
|
|
e2155f6b33 | ||
|
|
aee18d0edd | ||
|
|
20b99ee568 | ||
|
|
ad950208bf | ||
|
|
1183d6f817 | ||
|
|
cbb36e087c | ||
|
|
722d765973 | ||
|
|
5e75a1e7a1 | ||
|
|
e1349d418c | ||
|
|
5def4c9e01 | ||
|
|
891a944381 | ||
|
|
d7f0e4a265 | ||
|
|
56b0b34930 | ||
|
|
9b21bd6f19 | ||
|
|
c96d0dbea6 | ||
|
|
9c8a6d2408 | ||
|
|
53155e566a | ||
|
|
c648cfb593 | ||
|
|
cacf2ee2cc | ||
|
|
c4e6484bb0 | ||
|
|
88dd6fab76 | ||
|
|
84d628c690 | ||
|
|
5568fd50c2 | ||
|
|
89a637660f | ||
|
|
37e731fc2e | ||
|
|
e6099cf272 | ||
|
|
46640c68b9 | ||
|
|
c91beccdb0 | ||
|
|
6f3942ce38 | ||
|
|
09c2f33f5a | ||
|
|
58037e57c5 | ||
|
|
50c004f8a5 | ||
|
|
1d79988228 | ||
|
|
0ba89d75e6 | ||
|
|
6b83e32bc1 | ||
|
|
43f4132bf1 | ||
|
|
66afd72d6d | ||
|
|
bb9bad89d1 | ||
|
|
56fcfd0278 | ||
|
|
e930f9e4f7 | ||
|
|
93b161c23e | ||
|
|
7f2f67629f | ||
|
|
ac105c8383 | ||
|
|
bebc7fa3f0 | ||
|
|
9ef78aaffd | ||
|
|
d7604dab4d | ||
|
|
aaf2968538 | ||
|
|
420afd3206 | ||
|
|
605421f2d6 | ||
|
|
df00293a7c | ||
|
|
7898b2becd | ||
|
|
47d500715a | ||
|
|
eb7cadd64f | ||
|
|
48a00cb460 | ||
|
|
2f65291ef1 | ||
|
|
f6a75820e8 | ||
|
|
e49c69da2e | ||
|
|
6764bfcfd6 | ||
|
|
54026b7585 | ||
|
|
a42d7da6a4 | ||
|
|
21522f8a3a | ||
|
|
f62ca211eb | ||
|
|
d3bf98ea00 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
.*.swp
|
||||
.*
|
||||
!.gitignore
|
||||
|
||||
56
README
56
README
@ -1,56 +0,0 @@
|
||||
|
||||
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
|
||||
---------------
|
||||
|
||||
You will need Python, and the bottle.py framework (the package is usually
|
||||
called python-bottle in most distributions).
|
||||
|
||||
If pygments is available, it will be used for syntax highlighting, otherwise
|
||||
everything will work fine, just in black and white.
|
||||
|
||||
|
||||
First, create a configuration file for your repositories. You can start by
|
||||
copying sample.conf, which has the list of the available options.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
If you want to report bugs, send patches, or have any questions or comments,
|
||||
just let me know at albertito@blitiri.com.ar.
|
||||
|
||||
65
README.md
Normal file
65
README.md
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
# git-arr - A git repository browser
|
||||
|
||||
[git-arr] is a [git] repository browser that can generate static HTML.
|
||||
|
||||
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.
|
||||
|
||||
[git-arr]: https://blitiri.com.ar/p/git-arr/
|
||||
[git]: https://git-scm.com/
|
||||
[gitweb]: https://git-scm.com/docs/gitweb
|
||||
[cgit]: https://git.zx2c4.com/cgit/about/
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
You will need [Python 3], and the [bottle.py] framework (the package is usually
|
||||
called `python3-bottle` in most distributions).
|
||||
|
||||
If [pygments] is available, it will be used for syntax highlighting, otherwise
|
||||
everything will work fine, just in black and white.
|
||||
|
||||
|
||||
First, create a configuration file for your repositories. You can start by
|
||||
copying `sample.conf`, which has the list of the available options.
|
||||
|
||||
Then, to generate the output to `/var/www/git-arr/` directory, run:
|
||||
|
||||
```sh
|
||||
./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.
|
||||
|
||||
|
||||
[Python 3]: https://www.python.org/
|
||||
[bottle.py]: https://bottlepy.org/
|
||||
[pygments]: https://pygments.org/
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
If you want to report bugs, send patches, or have any questions or comments,
|
||||
just let me know at albertito@blitiri.com.ar.
|
||||
|
||||
13
TODO
13
TODO
@ -1,13 +0,0 @@
|
||||
|
||||
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).
|
||||
|
||||
467
git-arr
467
git-arr
@ -1,21 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
git-arr: A git web html generator.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import configparser
|
||||
import math
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
import bottle
|
||||
import bottle # type: ignore
|
||||
|
||||
import git
|
||||
import utils
|
||||
@ -25,12 +21,13 @@ import utils
|
||||
# Note this assumes they live next to the executable, and that is not a good
|
||||
# assumption; but it's good enough for now.
|
||||
bottle.TEMPLATE_PATH.insert(
|
||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
|
||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + "/views/"
|
||||
)
|
||||
|
||||
# The path to our static files.
|
||||
# Note this assumes they live next to the executable, and that is not a good
|
||||
# assumption; but it's good enough for now.
|
||||
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
|
||||
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
|
||||
@ -45,78 +42,100 @@ def load_config(path):
|
||||
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': 'cloneurl',
|
||||
"tree": "yes",
|
||||
"rootdiff": "yes",
|
||||
"desc": "",
|
||||
"recursive": "no",
|
||||
"prefix": "",
|
||||
"commits_in_summary": "10",
|
||||
"commits_per_page": "50",
|
||||
"max_pages": "250",
|
||||
"web_url": "",
|
||||
"web_url_file": "web_url",
|
||||
"git_url": "",
|
||||
"git_url_file": "cloneurl",
|
||||
"embed_markdown": "yes",
|
||||
"embed_images": "no",
|
||||
"ignore": "",
|
||||
"generate_patch": "yes",
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
config = configparser.ConfigParser(defaults)
|
||||
config.read(path)
|
||||
|
||||
# Do a first pass for general sanity checking and recursive expansion.
|
||||
for s in config.sections():
|
||||
if config.getboolean(s, 'recursive'):
|
||||
for path in os.listdir(config.get(s, 'path')):
|
||||
fullpath = find_git_dir(config.get(s, 'path') + '/' + path)
|
||||
if config.getboolean(s, "recursive"):
|
||||
root = config.get(s, "path")
|
||||
prefix = config.get(s, "prefix")
|
||||
|
||||
for path in os.listdir(root):
|
||||
fullpath = find_git_dir(root + "/" + path)
|
||||
if not fullpath:
|
||||
continue
|
||||
|
||||
if os.path.exists(fullpath + '/disable_gitweb'):
|
||||
if os.path.exists(fullpath + "/disable_gitweb"):
|
||||
continue
|
||||
|
||||
if config.has_section(path):
|
||||
section = prefix + path
|
||||
if config.has_section(section):
|
||||
continue
|
||||
|
||||
config.add_section(path)
|
||||
for opt, value in config.items(s, raw = True):
|
||||
config.set(path, opt, value)
|
||||
config.add_section(section)
|
||||
for opt, value in config.items(s, raw=True):
|
||||
config.set(section, opt, value)
|
||||
|
||||
config.set(path, 'path', fullpath)
|
||||
config.set(path, 'recursive', 'no')
|
||||
config.set(section, "path", fullpath)
|
||||
config.set(section, "recursive", "no")
|
||||
|
||||
# This recursive section is no longer useful.
|
||||
config.remove_section(s)
|
||||
|
||||
for s in config.sections():
|
||||
fullpath = find_git_dir(config.get(s, 'path'))
|
||||
if config.get(s, "ignore") and re.search(config.get(s, "ignore"), s):
|
||||
continue
|
||||
|
||||
fullpath = find_git_dir(config.get(s, "path"))
|
||||
if not fullpath:
|
||||
raise ValueError(
|
||||
'%s: path %s is not a valid git repository' % (
|
||||
s, config.get(s, 'path')))
|
||||
"%s: path %s is not a valid git repository"
|
||||
% (s, config.get(s, "path"))
|
||||
)
|
||||
|
||||
config.set(s, 'path', fullpath)
|
||||
config.set(s, 'name', s)
|
||||
config.set(s, "path", fullpath)
|
||||
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()
|
||||
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 = 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.commits_in_summary = config.getint(s, "commits_in_summary")
|
||||
r.info.commits_per_page = config.getint(s, "commits_per_page")
|
||||
r.info.max_pages = config.getint(s, "max_pages")
|
||||
if r.info.max_pages <= 0:
|
||||
r.info.max_pages = sys.maxsize
|
||||
r.info.generate_tree = config.getboolean(s, "tree")
|
||||
r.info.root_diff = config.getboolean(s, "rootdiff")
|
||||
r.info.generate_patch = config.getboolean(s, "generate_patch")
|
||||
|
||||
r.info.web_url = config.get(s, 'web_url')
|
||||
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
||||
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')
|
||||
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()
|
||||
|
||||
r.info.embed_markdown = config.getboolean(s, "embed_markdown")
|
||||
r.info.embed_images = config.getboolean(s, "embed_images")
|
||||
|
||||
repos[r.name] = r
|
||||
|
||||
|
||||
def find_git_dir(path):
|
||||
"""Returns the path to the git directory for the given repository.
|
||||
|
||||
@ -126,25 +145,26 @@ def find_git_dir(path):
|
||||
|
||||
An empty string is returned if the given path is not a valid repository.
|
||||
"""
|
||||
|
||||
def check(p):
|
||||
"""A dirty check for whether this is a git dir or not."""
|
||||
# Note silent stderr because we expect this to fail and don't want the
|
||||
# noise; and also we strip the final \n from the output.
|
||||
return git.run_git(p,
|
||||
['rev-parse', '--git-dir'],
|
||||
silent_stderr = True).read()[:-1]
|
||||
return git.run_git(
|
||||
p, ["rev-parse", "--git-dir"], silent_stderr=True
|
||||
).read()[:-1]
|
||||
|
||||
for p in [ path, path + '/.git' ]:
|
||||
for p in [path, path + "/.git"]:
|
||||
if check(p):
|
||||
return p
|
||||
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
def repo_filter(unused_conf):
|
||||
"""Bottle route filter for repos."""
|
||||
# TODO: consider allowing /, which is tricky.
|
||||
regexp = r'[\w\.~-]+'
|
||||
regexp = r"[\w\.~-]+"
|
||||
|
||||
def to_python(s):
|
||||
"""Return the corresponding Python object."""
|
||||
@ -158,8 +178,9 @@ def repo_filter(unused_conf):
|
||||
|
||||
return regexp, to_python, to_url
|
||||
|
||||
|
||||
app = bottle.Bottle()
|
||||
app.router.add_filter('repo', repo_filter)
|
||||
app.router.add_filter("repo", repo_filter)
|
||||
bottle.app.push(app)
|
||||
|
||||
|
||||
@ -170,12 +191,18 @@ def with_utils(f):
|
||||
templates.
|
||||
"""
|
||||
utilities = {
|
||||
'shorten': utils.shorten,
|
||||
'can_colorize': utils.can_colorize,
|
||||
'colorize_diff': utils.colorize_diff,
|
||||
'colorize_blob': utils.colorize_blob,
|
||||
'abort': bottle.abort,
|
||||
'smstr': git.smstr,
|
||||
"shorten": utils.shorten,
|
||||
"can_colorize": utils.can_colorize,
|
||||
"colorize_diff": utils.colorize_diff,
|
||||
"colorize_blob": utils.colorize_blob,
|
||||
"can_markdown": utils.can_markdown,
|
||||
"markdown_blob": utils.markdown_blob,
|
||||
"can_embed_image": utils.can_embed_image,
|
||||
"embed_image_blob": utils.embed_image_blob,
|
||||
"is_binary": utils.is_binary,
|
||||
"hexdump": utils.hexdump,
|
||||
"abort": bottle.abort,
|
||||
"smstr": git.smstr,
|
||||
}
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
@ -189,88 +216,131 @@ def with_utils(f):
|
||||
|
||||
return wrapped
|
||||
|
||||
@bottle.route('/')
|
||||
@bottle.view('index')
|
||||
|
||||
@bottle.route("/")
|
||||
@bottle.view("index")
|
||||
@with_utils
|
||||
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
|
||||
def summary(repo):
|
||||
return dict(repo = 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')
|
||||
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/")
|
||||
@bottle.view("commit")
|
||||
@with_utils
|
||||
def commit(repo, cid):
|
||||
c = repo.commit(cid)
|
||||
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>/b/<bname>/t/')
|
||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/<dirname:path>/')
|
||||
@bottle.view('tree')
|
||||
|
||||
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>.patch")
|
||||
@bottle.view(
|
||||
"patch",
|
||||
# Output is text/plain, don't do HTML escaping.
|
||||
template_settings={"noescape": True},
|
||||
)
|
||||
def patch(repo, cid):
|
||||
c = repo.commit(cid)
|
||||
if not c:
|
||||
bottle.abort(404, "Commit not found")
|
||||
|
||||
bottle.response.content_type = "text/plain; charset=utf8"
|
||||
|
||||
return dict(repo=repo, c=c)
|
||||
|
||||
|
||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html")
|
||||
@bottle.route(
|
||||
"/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html"
|
||||
)
|
||||
@bottle.view("blob")
|
||||
@with_utils
|
||||
def 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 + '/'
|
||||
def blob(repo, bname, fname, dirname=""):
|
||||
if dirname and not dirname.endswith("/"):
|
||||
dirname = dirname + "/"
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
fname = git.smstr.from_url(fname)
|
||||
path = dirname.raw + fname.raw
|
||||
|
||||
content = r.blob(path)
|
||||
# Handle backslash-escaped characters, which are not utf8.
|
||||
# This matches the generated links from git.unquote().
|
||||
path = path.encode("utf8").decode("unicode-escape").encode("latin1")
|
||||
|
||||
content = repo.blob(path, bname)
|
||||
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)
|
||||
return dict(
|
||||
repo=repo, branch=bname, dirname=dirname, fname=fname, blob=content
|
||||
)
|
||||
|
||||
@bottle.route('/static/<path:path>')
|
||||
|
||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/")
|
||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/")
|
||||
@bottle.view("tree")
|
||||
@with_utils
|
||||
def tree(repo, bname, dirname=""):
|
||||
if dirname and not dirname.endswith("/"):
|
||||
dirname = dirname + "/"
|
||||
|
||||
dirname = git.smstr.from_url(dirname)
|
||||
|
||||
return dict(
|
||||
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.view("branch")
|
||||
@with_utils
|
||||
def branch(repo, bname, offset=0):
|
||||
return dict(repo=repo, branch=bname, offset=offset)
|
||||
|
||||
|
||||
@bottle.route("/static/<path:path>")
|
||||
def static(path):
|
||||
return bottle.static_file(path, root = static_path)
|
||||
return bottle.static_file(path, root=static_path)
|
||||
|
||||
|
||||
#
|
||||
# Static HTML generation
|
||||
#
|
||||
|
||||
def generate(output, skip_index = False):
|
||||
|
||||
def is_404(e):
|
||||
"""True if e is an HTTPError with status 404, False otherwise."""
|
||||
# We need this because older bottle.py versions put the status code in
|
||||
# e.status as an integer, and newer versions make that a string, and using
|
||||
# e.status_code for the code.
|
||||
if isinstance(e.status, int):
|
||||
return e.status == 404
|
||||
else:
|
||||
return e.status_code == 404
|
||||
|
||||
|
||||
def generate(output: str, only=None):
|
||||
"""Generate static html to the output directory."""
|
||||
def write_to(path, func_or_str, args = (), mtime = None):
|
||||
path = output + '/' + path
|
||||
|
||||
def write_to(path: str, 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
|
||||
path_mtime: Union[float, int] = 0
|
||||
if os.path.exists(path):
|
||||
path_mtime = os.stat(path).st_mtime
|
||||
|
||||
@ -290,7 +360,7 @@ def generate(output, skip_index = False):
|
||||
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)):
|
||||
if isinstance(func_or_str, str):
|
||||
print(path)
|
||||
s = func_or_str
|
||||
else:
|
||||
@ -299,135 +369,184 @@ def generate(output, skip_index = False):
|
||||
print(path)
|
||||
s = func_or_str(*args)
|
||||
|
||||
open(path, 'w').write(s.encode('utf8', errors = 'xmlcharrefreplace'))
|
||||
open(path, "w").write(s)
|
||||
if mtime:
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
def link(from_path, to_path):
|
||||
from_path = output + '/' + from_path
|
||||
from_path = output + "/" + from_path
|
||||
|
||||
if os.path.lexists(from_path):
|
||||
return
|
||||
print(from_path, '->', to_path)
|
||||
print(from_path, "->", to_path)
|
||||
os.symlink(to_path, from_path)
|
||||
|
||||
def write_tree(r, bn, mtime):
|
||||
t = r.tree(bn)
|
||||
def write_tree(r: git.Repo, bn: str, mtime):
|
||||
t: git.Tree = r.tree(bn)
|
||||
|
||||
write_to('r/%s/b/%s/t/index.html' % (r.name, bn),
|
||||
tree, (r, bn), mtime)
|
||||
write_to("r/%s/b/%s/t/index.html" % (r.name, bn), 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
|
||||
# expected to fail for now; we skip them.
|
||||
if '\n' in oname.raw:
|
||||
print('skipping file with \\n: %r' % (oname.raw))
|
||||
if "\n" in oname.raw:
|
||||
print("skipping file with \\n: %r" % (oname.raw))
|
||||
continue
|
||||
|
||||
if otype == 'blob':
|
||||
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)
|
||||
"r/%s/b/%s/t/%s%sf=%s.html"
|
||||
% (
|
||||
str(r.name),
|
||||
str(bn),
|
||||
dirname.raw,
|
||||
"/" if dirname.raw else "",
|
||||
fname.raw,
|
||||
),
|
||||
blob,
|
||||
(r, bn, fname.url, dirname.url),
|
||||
mtime,
|
||||
)
|
||||
else:
|
||||
write_to('r/%s/b/%s/t/%s/index.html' %
|
||||
(str(r.name), str(bn), oname.raw),
|
||||
tree, (r, bn, oname.url), mtime)
|
||||
write_to(
|
||||
"r/%s/b/%s/t/%s/index.html"
|
||||
% (str(r.name), str(bn), oname.raw),
|
||||
tree,
|
||||
(r, bn, oname.url),
|
||||
mtime,
|
||||
)
|
||||
|
||||
if not skip_index:
|
||||
write_to('index.html', index())
|
||||
# Always generate the index, to keep the "last updated" time fresh.
|
||||
write_to("index.html", index())
|
||||
|
||||
# We can't call static() because it relies on HTTP headers.
|
||||
read_f = lambda f: open(f).read()
|
||||
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
||||
os.stat(static_path + '/git-arr.css').st_mtime)
|
||||
write_to('static/syntax.css', read_f, [static_path + '/syntax.css'],
|
||||
os.stat(static_path + '/syntax.css').st_mtime)
|
||||
write_to(
|
||||
"static/git-arr.css",
|
||||
read_f,
|
||||
[static_path + "/git-arr.css"],
|
||||
os.stat(static_path + "/git-arr.css").st_mtime,
|
||||
)
|
||||
write_to(
|
||||
"static/git-arr.js",
|
||||
read_f,
|
||||
[static_path + "/git-arr.js"],
|
||||
os.stat(static_path + "/git-arr.js").st_mtime,
|
||||
)
|
||||
write_to(
|
||||
"static/syntax.css",
|
||||
read_f,
|
||||
[static_path + "/syntax.css"],
|
||||
os.stat(static_path + "/syntax.css").st_mtime,
|
||||
)
|
||||
|
||||
for r in sorted(repos.values(), key = lambda r: r.name):
|
||||
write_to('r/%s/index.html' % r.name, summary(r))
|
||||
rs = sorted(list(repos.values()), key=lambda r: r.name)
|
||||
if only:
|
||||
rs = [r for r in rs if r.name in only]
|
||||
|
||||
for r in rs:
|
||||
write_to("r/%s/index.html" % r.name, summary(r))
|
||||
for bn in r.branch_names():
|
||||
commit_count = 0
|
||||
commit_ids = r.commit_ids('refs/heads/' + bn,
|
||||
limit = r.info.commits_per_page * r.info.max_pages)
|
||||
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))
|
||||
write_to(
|
||||
"r/%s/c/%s/index.html" % (r.name, cid), commit, (r, cid)
|
||||
)
|
||||
if r.info.generate_patch:
|
||||
write_to(
|
||||
"r/%s/c/%s.patch" % (r.name, cid), patch, (r, cid)
|
||||
)
|
||||
commit_count += 1
|
||||
|
||||
# To avoid regenerating files that have not changed, we will
|
||||
# instruct write_to() to set their mtime to the branch's committer
|
||||
# date, and then compare against it to decide wether or not to
|
||||
# date, and then compare against it to decide whether or not to
|
||||
# write.
|
||||
branch_mtime = r.commit(bn).committer_date.epoch
|
||||
|
||||
nr_pages = int(math.ceil(
|
||||
float(commit_count) / r.info.commits_per_page))
|
||||
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)
|
||||
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')
|
||||
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))
|
||||
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))
|
||||
if is_404(e):
|
||||
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',
|
||||
default = [],
|
||||
help = 'generate/serve only this repository')
|
||||
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",
|
||||
default=[],
|
||||
help="generate/serve only this repository",
|
||||
)
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
if not opts.config:
|
||||
parser.error('--config is mandatory')
|
||||
parser.error("--config is mandatory")
|
||||
|
||||
try:
|
||||
load_config(opts.config)
|
||||
except (configparser.NoOptionError, ValueError) as e:
|
||||
print('Error parsing config:', e)
|
||||
print("Error parsing config:", e)
|
||||
return
|
||||
|
||||
if not args:
|
||||
parser.error('Must specify an action (serve|generate)')
|
||||
parser.error("Must specify an action (serve|generate)")
|
||||
|
||||
if opts.only:
|
||||
for rname in list(repos.keys()):
|
||||
if rname not in opts.only:
|
||||
del repos[rname]
|
||||
|
||||
if args[0] == 'serve':
|
||||
bottle.run(host = 'localhost', port = 8008, reloader = True)
|
||||
elif args[0] == 'generate':
|
||||
if 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,
|
||||
skip_index = len(opts.only) > 0)
|
||||
parser.error("Must specify --output")
|
||||
generate(output=opts.output, only=opts.only)
|
||||
else:
|
||||
parser.error('Unknown action %s' % args[0])
|
||||
parser.error("Unknown action %s" % args[0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
457
git.py
457
git.py
@ -6,97 +6,93 @@ command line tool directly, so please be careful with using untrusted
|
||||
parameters.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import sys
|
||||
import io
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
import email.utils
|
||||
import datetime
|
||||
import urllib
|
||||
from cgi import escape
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
from html import escape
|
||||
from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
|
||||
# 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, silent_stderr = False):
|
||||
def run_git(
|
||||
repo_path: str, params, stdin: bytes = None, silent_stderr=False, raw=False
|
||||
) -> Union[IO[str], IO[bytes]]:
|
||||
"""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)
|
||||
params = [GIT_BIN, "--git-dir=%s" % repo_path] + list(params)
|
||||
|
||||
stderr = None
|
||||
if silent_stderr:
|
||||
stderr = subprocess.PIPE
|
||||
|
||||
if not stdin:
|
||||
p = subprocess.Popen(params,
|
||||
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
|
||||
p = subprocess.Popen(
|
||||
params, stdin=None, stdout=subprocess.PIPE, stderr=stderr
|
||||
)
|
||||
else:
|
||||
p = subprocess.Popen(params,
|
||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
|
||||
stderr = stderr)
|
||||
p = subprocess.Popen(
|
||||
params,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
assert p.stdin is not None
|
||||
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)
|
||||
assert p.stdout is not None
|
||||
|
||||
if raw:
|
||||
return p.stdout
|
||||
|
||||
return io.TextIOWrapper(
|
||||
p.stdout, encoding="utf8", errors="backslashreplace"
|
||||
)
|
||||
|
||||
|
||||
class GitCommand (object):
|
||||
class GitCommand(object):
|
||||
"""Convenient way of invoking git."""
|
||||
def __init__(self, path, cmd, *args, **kwargs):
|
||||
|
||||
def __init__(self, path: str, cmd: str):
|
||||
self._override = True
|
||||
self._path = path
|
||||
self._cmd = cmd
|
||||
self._args = list(args)
|
||||
self._kwargs = {}
|
||||
self._stdin_buf = None
|
||||
self._args: List[str] = []
|
||||
self._kwargs: Dict[str, str] = {}
|
||||
self._stdin_buf: Optional[bytes] = None
|
||||
self._raw = False
|
||||
self._override = False
|
||||
for k, v in kwargs:
|
||||
self.__setattr__(k, v)
|
||||
|
||||
def __setattr__(self, k, v):
|
||||
if k == '_override' or self._override:
|
||||
if k == "_override" or self._override:
|
||||
self.__dict__[k] = v
|
||||
return
|
||||
k = k.replace('_', '-')
|
||||
k = k.replace("_", "-")
|
||||
self._kwargs[k] = v
|
||||
|
||||
def arg(self, a):
|
||||
def arg(self, a: str):
|
||||
"""Adds an argument."""
|
||||
self._args.append(a)
|
||||
|
||||
def stdin(self, s):
|
||||
def raw(self, b: bool):
|
||||
"""Request raw rather than utf8-encoded command output."""
|
||||
self._override = True
|
||||
self._raw = b
|
||||
self._override = False
|
||||
|
||||
def stdin(self, s: bytes):
|
||||
"""Sets the contents we will send in stdin."""
|
||||
self._override = True
|
||||
self._stdin_buf = s
|
||||
@ -106,46 +102,37 @@ class GitCommand (object):
|
||||
"""Runs the git command."""
|
||||
params = [self._cmd]
|
||||
|
||||
for k, v in self._kwargs.items():
|
||||
dash = '--' if len(k) > 1 else '-'
|
||||
for k, v in list(self._kwargs.items()):
|
||||
dash = "--" if len(k) > 1 else "-"
|
||||
if v is None:
|
||||
params.append('%s%s' % (dash, k))
|
||||
params.append("%s%s" % (dash, k))
|
||||
else:
|
||||
params.append('%s%s=%s' % (dash, k, str(v)))
|
||||
params.append("%s%s=%s" % (dash, k, str(v)))
|
||||
|
||||
params.extend(self._args)
|
||||
|
||||
return run_git(self._path, params, self._stdin_buf)
|
||||
return run_git(self._path, params, self._stdin_buf, raw=self._raw)
|
||||
|
||||
|
||||
class SimpleNamespace (object):
|
||||
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.
|
||||
"""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)
|
||||
raw: str # string, probably utf8-encoded, good enough to show.
|
||||
url: str # escaped for safe embedding in URLs (not human-readable).
|
||||
html: str # HTML-embeddable representation.
|
||||
|
||||
def __init__(self, s: str):
|
||||
self.raw = s
|
||||
self.url = urllib.request.pathname2url(s)
|
||||
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.
|
||||
@ -153,11 +140,11 @@ class smstr:
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
"""Returns an smstr() instance from an url-encoded string."""
|
||||
return smstr(urllib.url2pathname(url))
|
||||
return smstr(urllib.request.url2pathname(url))
|
||||
|
||||
def split(self, sep):
|
||||
"""Like str.split()."""
|
||||
return [ smstr(s) for s in self.raw.split(sep) ]
|
||||
return [smstr(s) for s in self.raw.split(sep)]
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, smstr):
|
||||
@ -166,10 +153,10 @@ class smstr:
|
||||
|
||||
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 = ""
|
||||
for c in escape(self.raw):
|
||||
if c in "\t\r\n\r\f\a\b\v\0":
|
||||
esc_c = c.encode("unicode-escape").decode("utf8")
|
||||
html += '<span class="ctrlchr">%s</span>' % esc_c
|
||||
else:
|
||||
html += c
|
||||
@ -177,17 +164,26 @@ class smstr:
|
||||
return html
|
||||
|
||||
|
||||
def unquote(s):
|
||||
def unquote(s: str):
|
||||
"""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')
|
||||
# Unquoted strings are always safe, no need to mess with them
|
||||
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')
|
||||
# The string will be of the form `"<escaped>"`, where <escaped> is a
|
||||
# backslash-escaped representation of the name of the file.
|
||||
# Examples: "with\ttwo\ttabs" , "\303\261aca-utf8", "\361aca-latin1"
|
||||
|
||||
# Get rid of the quotes, we never want them in the output.
|
||||
s = s[1:-1]
|
||||
|
||||
# Un-escape the backslashes.
|
||||
# latin1 is ok to use here because in Python it just maps the code points
|
||||
# 0-255 to the bytes 0x-0xff, which is what we expect.
|
||||
s = s.encode("latin1").decode("unicode-escape")
|
||||
|
||||
# Convert to utf8.
|
||||
s = s.encode("latin1").decode("utf8", errors="backslashreplace")
|
||||
|
||||
return s
|
||||
|
||||
@ -195,93 +191,86 @@ def unquote(s):
|
||||
class Repo:
|
||||
"""A git repository."""
|
||||
|
||||
def __init__(self, path, branch = None, name = None, info = None):
|
||||
def __init__(self, path: str, 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()
|
||||
self.info: Any = 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):
|
||||
@functools.lru_cache
|
||||
def _for_each_ref(self, pattern=None, sort=None, count=None):
|
||||
"""Returns a list of references."""
|
||||
cmd = self.cmd('for-each-ref')
|
||||
cmd = self.cmd("for-each-ref")
|
||||
if sort:
|
||||
cmd.sort = sort
|
||||
if count:
|
||||
cmd.count = count
|
||||
if pattern:
|
||||
cmd.arg(pattern)
|
||||
|
||||
refs = []
|
||||
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
|
||||
refs.append((obj_id, obj_type, ref))
|
||||
return refs
|
||||
|
||||
@functools.cache
|
||||
def branch_names(self):
|
||||
"""Get the names of the branches."""
|
||||
return ( name for name, _ in self.branches() )
|
||||
refs = self._for_each_ref(pattern="refs/heads/", sort="-authordate")
|
||||
return [ref[len("refs/heads/") :] for _, _, ref in refs]
|
||||
|
||||
def tags(self, sort = '-taggerdate'):
|
||||
@functools.cache
|
||||
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
|
||||
refs = self._for_each_ref(pattern="refs/tags/", sort=sort)
|
||||
return [(ref[len("refs/tags/") :], obj_id) for obj_id, _, ref in refs]
|
||||
|
||||
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):
|
||||
@functools.lru_cache
|
||||
def commit_ids(self, ref, limit=None):
|
||||
"""Generate commit ids."""
|
||||
cmd = self.cmd('rev-list')
|
||||
cmd = self.cmd("rev-list")
|
||||
if limit:
|
||||
cmd.max_count = limit
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg("--")
|
||||
|
||||
for l in cmd.run():
|
||||
yield l.rstrip('\n')
|
||||
return [l.rstrip("\n") for l in cmd.run()]
|
||||
|
||||
@functools.lru_cache
|
||||
def commit(self, commit_id):
|
||||
"""Return a single commit."""
|
||||
cs = list(self.commits(commit_id, limit = 1))
|
||||
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):
|
||||
@functools.lru_cache
|
||||
def commits(self, ref, limit, offset=0):
|
||||
"""Generate commit objects for the ref."""
|
||||
cmd = self.cmd('rev-list')
|
||||
if limit:
|
||||
cmd.max_count = limit + offset
|
||||
cmd = self.cmd("rev-list")
|
||||
cmd.max_count = limit + offset
|
||||
|
||||
cmd.header = None
|
||||
|
||||
cmd.arg(ref)
|
||||
cmd.arg("--")
|
||||
|
||||
info_buffer = ''
|
||||
info_buffer = ""
|
||||
count = 0
|
||||
commits = []
|
||||
for l in cmd.run():
|
||||
if '\0' in l:
|
||||
pre, post = l.split('\0', 1)
|
||||
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)
|
||||
commits.append(Commit.from_str(self, info_buffer))
|
||||
|
||||
# Start over.
|
||||
info_buffer = post
|
||||
@ -291,14 +280,19 @@ class Repo:
|
||||
if info_buffer:
|
||||
count += 1
|
||||
if count > offset:
|
||||
yield Commit.from_str(self, info_buffer)
|
||||
commits.append(Commit.from_str(self, info_buffer))
|
||||
|
||||
return commits
|
||||
|
||||
@functools.lru_cache
|
||||
def diff(self, ref):
|
||||
"""Return a Diff object for the ref."""
|
||||
cmd = self.cmd('diff-tree')
|
||||
cmd = self.cmd("diff-tree")
|
||||
cmd.patch = None
|
||||
cmd.numstat = None
|
||||
cmd.find_renames = None
|
||||
if self.info.root_diff:
|
||||
cmd.root = None
|
||||
# Note we intentionally do not use -z, as the filename is just for
|
||||
# reference, and it is safer to let git do the escaping.
|
||||
|
||||
@ -306,52 +300,72 @@ class Repo:
|
||||
|
||||
return Diff.from_str(cmd.run())
|
||||
|
||||
@functools.lru_cache
|
||||
def refs(self):
|
||||
"""Return a dict of obj_id -> ref."""
|
||||
cmd = self.cmd('show-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)
|
||||
obj_id, ref = l.split(" ", 1)
|
||||
r[obj_id].append(ref)
|
||||
|
||||
return r
|
||||
|
||||
def tree(self, ref = None):
|
||||
@functools.lru_cache
|
||||
def tree(self, ref):
|
||||
"""Returns a Tree instance for the given ref."""
|
||||
if not ref:
|
||||
ref = self.branch
|
||||
return Tree(self, ref)
|
||||
|
||||
def blob(self, path, ref = None):
|
||||
"""Returns the contents of the given path."""
|
||||
if not ref:
|
||||
ref = self.branch
|
||||
cmd = self.cmd('cat-file')
|
||||
cmd.batch = None
|
||||
@functools.lru_cache
|
||||
def blob(self, path, ref):
|
||||
"""Returns a Blob instance for the given path."""
|
||||
cmd = self.cmd("cat-file")
|
||||
cmd.raw(True)
|
||||
cmd.batch = "%(objectsize)"
|
||||
|
||||
if isinstance(ref, unicode):
|
||||
ref = ref.encode('utf8')
|
||||
cmd.stdin('%s:%s' % (ref, path))
|
||||
# Format: <ref>:<path>
|
||||
# Construct it in binary since the path might not be utf8.
|
||||
cmd.stdin(ref.encode("utf8") + b":" + path)
|
||||
|
||||
out = cmd.run()
|
||||
head = out.readline()
|
||||
if not head or head.strip().endswith('missing'):
|
||||
if not head or head.strip().endswith(b"missing"):
|
||||
return None
|
||||
|
||||
return out.read()
|
||||
return Blob(out.read()[: int(head)])
|
||||
|
||||
@functools.cache
|
||||
def last_commit_timestamp(self):
|
||||
"""Return the timestamp of the last commit."""
|
||||
refs = self._for_each_ref(
|
||||
pattern="refs/heads/", sort="-committerdate", count=1
|
||||
)
|
||||
for obj_id, _, _ in refs:
|
||||
commit = self.commit(obj_id)
|
||||
return commit.committer_epoch
|
||||
return -1
|
||||
|
||||
|
||||
class Commit (object):
|
||||
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):
|
||||
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
|
||||
@ -364,28 +378,30 @@ class Commit (object):
|
||||
self.committer_tz = committer_tz
|
||||
self.message = message
|
||||
|
||||
self.author_name, self.author_email = \
|
||||
email.utils.parseaddr(self.author)
|
||||
self.author_name, self.author_email = email.utils.parseaddr(
|
||||
self.author
|
||||
)
|
||||
|
||||
self.committer_name, self.committer_email = \
|
||||
email.utils.parseaddr(self.committer)
|
||||
self.committer_name, self.committer_email = email.utils.parseaddr(
|
||||
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.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])
|
||||
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):
|
||||
@ -397,52 +413,68 @@ class Commit (object):
|
||||
@staticmethod
|
||||
def from_str(repo, buf):
|
||||
"""Parses git rev-list output, returns a commit object."""
|
||||
header, raw_message = buf.split('\n\n', 1)
|
||||
if "\n\n" in buf:
|
||||
# Header, commit message
|
||||
header, raw_message = buf.split("\n\n", 1)
|
||||
else:
|
||||
# Header only, no commit message
|
||||
header, raw_message = buf.rstrip(), " "
|
||||
|
||||
header_lines = header.split('\n')
|
||||
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)
|
||||
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)
|
||||
tree = header_dict["tree"][0]
|
||||
parents = set(header_dict["parent"])
|
||||
|
||||
authorhdr = header_dict["author"][0]
|
||||
author, author_epoch, author_tz = authorhdr.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.
|
||||
message = ''
|
||||
for line in raw_message.split('\n'):
|
||||
message += line[4:] + '\n'
|
||||
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,
|
||||
)
|
||||
|
||||
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.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
||||
|
||||
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.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 += '(%s %s)' % (self.local.strftime('%H:%M'), self.tz)
|
||||
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
|
||||
@ -450,6 +482,7 @@ class Date:
|
||||
|
||||
class Diff:
|
||||
"""A diff between two trees."""
|
||||
|
||||
def __init__(self, ref, changes, body):
|
||||
"""Constructor.
|
||||
|
||||
@ -469,60 +502,82 @@ class Diff:
|
||||
ref_id = next(lines)
|
||||
except StopIteration:
|
||||
# No diff; this can happen in merges without conflicts.
|
||||
return Diff(None, [], '')
|
||||
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')
|
||||
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)
|
||||
body = "".join(lines)
|
||||
|
||||
return Diff(ref_id, changes, body)
|
||||
|
||||
|
||||
class Tree:
|
||||
""" A git tree."""
|
||||
"""A git tree."""
|
||||
|
||||
def __init__(self, repo, ref):
|
||||
def __init__(self, repo: Repo, ref: str):
|
||||
self.repo = repo
|
||||
self.ref = ref
|
||||
|
||||
def ls(self, path, recursive = False):
|
||||
@functools.lru_cache
|
||||
def ls(
|
||||
self, path, recursive=False
|
||||
) -> Iterable[Tuple[str, smstr, Optional[int]]]:
|
||||
"""Generates (type, name, size) for each file in path."""
|
||||
cmd = self.repo.cmd('ls-tree')
|
||||
cmd = self.repo.cmd("ls-tree")
|
||||
cmd.long = None
|
||||
if recursive:
|
||||
cmd.r = None
|
||||
cmd.t = None
|
||||
|
||||
cmd.arg(self.ref)
|
||||
cmd.arg(path)
|
||||
if not path:
|
||||
cmd.arg(".")
|
||||
else:
|
||||
cmd.arg(path)
|
||||
|
||||
files = []
|
||||
for l in cmd.run():
|
||||
_mode, otype, _oid, size, name = l.split(None, 4)
|
||||
if size == '-':
|
||||
if size == "-":
|
||||
size = None
|
||||
else:
|
||||
size = int(size)
|
||||
|
||||
# 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
|
||||
# easier to work with this way.
|
||||
name = name[len(path):]
|
||||
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
|
||||
files.append((otype, smstr(name), size))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
class Blob:
|
||||
"""A git blob."""
|
||||
|
||||
def __init__(self, raw_content: bytes):
|
||||
self.raw_content = raw_content
|
||||
self._utf8_content = None
|
||||
|
||||
@property
|
||||
def utf8_content(self):
|
||||
if not self._utf8_content:
|
||||
self._utf8_content = self.raw_content.decode("utf8", "replace")
|
||||
return self._utf8_content
|
||||
|
||||
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)$"
|
||||
30
sample.conf
30
sample.conf
@ -11,6 +11,15 @@ path = /srv/git/repo/
|
||||
# Useful to disable an expensive operation in very large repositories.
|
||||
#tree = yes
|
||||
|
||||
# Show a "creation event" diff for a root commit? (optional)
|
||||
# For projects placed under revision control from inception, the root commit
|
||||
# diff is often meaningful. However, in cases when a well established, large
|
||||
# project is placed under revision control belatedly, the root commit may
|
||||
# represent a lump import of the entire project, in which case such a
|
||||
# "creation event" diff would likely be considered meaningless noise.
|
||||
# Default: yes
|
||||
#rootdiff = yes
|
||||
|
||||
# How many commits to show in the summary page (optional).
|
||||
#commits_in_summary = 10
|
||||
|
||||
@ -19,8 +28,9 @@ path = /srv/git/repo/
|
||||
|
||||
# Maximum number of per-branch pages for static generation (optional).
|
||||
# When generating static html, this is the maximum number of pages we will
|
||||
# generate for each branch's commit listings.
|
||||
#max_pages = 5
|
||||
# generate for each branch's commit listings. Zero (0) means unlimited.
|
||||
# Default: 250
|
||||
#max_pages = 250
|
||||
|
||||
# Project website (optional).
|
||||
# URL to the project's website. %(name)s will be replaced with the current
|
||||
@ -48,6 +58,22 @@ path = /srv/git/repo/
|
||||
# excluded.
|
||||
#recursive = no
|
||||
|
||||
# Render Markdown blobs (*.md) formatted rather than as raw text? (optional)
|
||||
# Requires 'markdown' module.
|
||||
# Default: yes
|
||||
#embed_markdown = yes
|
||||
|
||||
# Render image blobs graphically rather than as raw binary data? (optional)
|
||||
# Default: no
|
||||
#embed_images = no
|
||||
|
||||
# Ignore repositories that match this regular expression.
|
||||
# Generally used with recursive = yes, to ignore repeated repositories (for
|
||||
# example, if using symlinks).
|
||||
# For ignoring specific repositories, putting a "disable_gitweb" is a much
|
||||
# better alternative.
|
||||
# Default: empty (don't ignore)
|
||||
#ignore = \.git$
|
||||
|
||||
# Another repository, we don't generate a tree for it because it's too big.
|
||||
[linux]
|
||||
|
||||
@ -1,17 +1,53 @@
|
||||
|
||||
/*
|
||||
* git-arr style sheet
|
||||
*/
|
||||
:root {
|
||||
--body-bg: white;
|
||||
--text-fg: black;
|
||||
--h1-bg: #ddd;
|
||||
--hr-bg: #e3e3e3;
|
||||
--text-lowcontrast-fg: grey;
|
||||
--a-fg: #800;
|
||||
--a-explicit-fg: #038;
|
||||
--table-hover-bg: #eee;
|
||||
--head-bg: #88ff88;
|
||||
--tag-bg: #ffff88;
|
||||
--age-fg0: darkgreen;
|
||||
--age-fg1: green;
|
||||
--age-fg2: seagreen;
|
||||
--diff-added-fg: green;
|
||||
--diff-deleted-fg: red;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--body-bg: #121212;
|
||||
--text-fg: #c9d1d9;
|
||||
--h1-bg: #2f2f2f;
|
||||
--hr-bg: #e3e3e3;
|
||||
--text-lowcontrast-fg: grey;
|
||||
--a-fg: #d4b263;
|
||||
--a-explicit-fg: #44b4ec;
|
||||
--table-hover-bg: #313131;
|
||||
--head-bg: #020;
|
||||
--tag-bg: #333000;
|
||||
--age-fg0: #51a552;
|
||||
--age-fg1: #468646;
|
||||
--age-fg2: #2f722f;
|
||||
--diff-added-fg: #00A000;
|
||||
--diff-deleted-fg: #A00000;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: small;
|
||||
padding: 0 1em 1em 1em;
|
||||
color: var(--text-fg);
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: x-large;
|
||||
background: #ddd;
|
||||
background: var(--h1-bg);
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
@ -23,146 +59,279 @@ h2, h3 {
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
background-color: #e3e3e3;
|
||||
background-color: var(--hr-bg);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
|
||||
/* By default, use implied links, more discrete for increased readability. */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
color: var(--text-fg);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
color: #800;
|
||||
color: var(--a-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Explicit links */
|
||||
a.explicit {
|
||||
color: #038;
|
||||
color: var(--a-explicit-fg);
|
||||
}
|
||||
|
||||
a.explicit:hover, a.explicit:active {
|
||||
color: #880000;
|
||||
color: var(--a-fg);
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
background: var(--table-hover-bg);
|
||||
}
|
||||
|
||||
|
||||
/* Table for commits. */
|
||||
table.commits td.date {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
table.commits td.subject {
|
||||
min-width: 32em;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
table.commits td.subject {
|
||||
min-width: 32em;
|
||||
}
|
||||
}
|
||||
|
||||
table.commits td.author {
|
||||
color: gray;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Reference annotations. */
|
||||
span.refs {
|
||||
margin: 0px 0.5em;
|
||||
padding: 0px 0.25em;
|
||||
border: solid 1px gray;
|
||||
border: solid 1px var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
span.head {
|
||||
background-color: #88ff88;
|
||||
background-color: var(--head-bg);
|
||||
}
|
||||
|
||||
span.tag {
|
||||
background-color: #ffff88;
|
||||
background-color: var(--tag-bg);
|
||||
}
|
||||
|
||||
|
||||
/* Projects table */
|
||||
table.projects td.name a {
|
||||
color: var(--a-explicit-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Age of an object.
|
||||
* Note this is hidden by default as we rely on javascript to show it. */
|
||||
span.age {
|
||||
display: none;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
span.age-band0 {
|
||||
color: var(--age-fg0);
|
||||
}
|
||||
|
||||
span.age-band1 {
|
||||
color: var(--age-fg1);
|
||||
}
|
||||
|
||||
span.age-band2 {
|
||||
color: var(--age-fg2);
|
||||
}
|
||||
|
||||
|
||||
/* Toggable titles */
|
||||
div.toggable-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
/* Sometimes, <pre> elements (commit messages, diffs, blobs) have very
|
||||
* long lines. In those case, use automatic overflow, which will
|
||||
* introduce a horizontal scroll bar for this element only (more
|
||||
* comfortable than stretching the page, which is the default). */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Commit message and diff. */
|
||||
pre.commit-message {
|
||||
font-size: large;
|
||||
padding: 0.2em 2em;
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
|
||||
pre.diff-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
table.changed-files {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table.changed-files span.lines-added {
|
||||
color: green;
|
||||
color: var(--diff-added-fg);
|
||||
}
|
||||
|
||||
table.changed-files span.lines-deleted {
|
||||
color: red;
|
||||
color: var(--diff-deleted-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Pagination. */
|
||||
div.paginate {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
div.paginate span.inactive {
|
||||
color: gray;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Directory listing. */
|
||||
table.ls td.name {
|
||||
min-width: 20em;
|
||||
@media (min-width: 600px) {
|
||||
table.ls td.name {
|
||||
min-width: 20em;
|
||||
}
|
||||
}
|
||||
|
||||
table.ls {
|
||||
font-family: monospace;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
table.ls tr.blob td.size {
|
||||
color: gray;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Blob. */
|
||||
pre.blob-body {
|
||||
/* Note this is only used as a fallback if pygments is not available. */
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
table.blob-binary pre {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.blob-binary .offset {
|
||||
text-align: right;
|
||||
font-size: x-small;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
border-right: 1px solid var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
table.blob-binary tr.etc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* Pygments overrides. */
|
||||
div.colorized-src {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
div.colorized-src .source_code {
|
||||
/* Ignore pygments style's background. */
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
td.code > div.source_code {
|
||||
/* This is a workaround, in pygments 2.11 there's a bug where the wrapper
|
||||
* div is inside the table, so we need to override the descendant (because
|
||||
* the style sets it on ".source_code" and the most specific value wins).
|
||||
* Once we no longer support 2.11, we can remove this. */
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
div.linenodiv {
|
||||
padding-right: 0.5em;
|
||||
color: gray;
|
||||
font-size: medium;
|
||||
}
|
||||
div.source_code {
|
||||
background: inherit;
|
||||
font-size: medium;
|
||||
|
||||
div.linenodiv a {
|
||||
color: var(--text-lowcontrast-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Repository information table. */
|
||||
table.repo_info tr:hover {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
table.repo_info td.category {
|
||||
font-weight: bold;
|
||||
/* So we can copy-paste rows and preserve spaces, useful for the row:
|
||||
* git clone | url */
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
table.repo_info td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
span.ctrlchr {
|
||||
color: gray;
|
||||
color: var(--text-lowcontrast-fg);
|
||||
padding: 0 0.2ex 0 0.1ex;
|
||||
margin: 0 0.2ex 0 0.1ex;
|
||||
margin: 0 0.2ex 0 0.1ex;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Markdown overrides
|
||||
*/
|
||||
|
||||
/* Colored links (same as explicit links above) */
|
||||
div.markdown a {
|
||||
color: var(--a-explicit-fg);
|
||||
}
|
||||
|
||||
div.markdown a:hover, div.markdown a:active {
|
||||
color: var(--a-fg);
|
||||
}
|
||||
|
||||
|
||||
/* Restrict max width for readability */
|
||||
div.markdown {
|
||||
max-width: 55em;
|
||||
}
|
||||
|
||||
73
static/git-arr.js
Normal file
73
static/git-arr.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* Miscellaneous javascript functions for git-arr. */
|
||||
|
||||
/* Return the current timestamp. */
|
||||
function now() {
|
||||
return (new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
/* Return a human readable string telling "how long ago" for a timestamp. */
|
||||
function how_long_ago(timestamp) {
|
||||
if (timestamp < 0)
|
||||
return "never";
|
||||
|
||||
var seconds = Math.floor(now() - timestamp);
|
||||
|
||||
var interval = Math.floor(seconds / (365 * 24 * 60 * 60));
|
||||
if (interval > 1)
|
||||
return interval + " years ago";
|
||||
|
||||
interval = Math.floor(seconds / (30 * 24 * 60 * 60));
|
||||
if (interval > 1)
|
||||
return interval + " months ago";
|
||||
|
||||
interval = Math.floor(seconds / (24 * 60 * 60));
|
||||
|
||||
if (interval > 1)
|
||||
return interval + " days ago";
|
||||
interval = Math.floor(seconds / (60 * 60));
|
||||
|
||||
if (interval > 1)
|
||||
return interval + " hours ago";
|
||||
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1)
|
||||
return interval + " minutes ago";
|
||||
|
||||
if (seconds > 1)
|
||||
return Math.floor(seconds) + " seconds ago";
|
||||
|
||||
return "about now";
|
||||
}
|
||||
|
||||
/* Go through the document and replace the contents of the span.age elements
|
||||
* with a human-friendly variant, and then show them. */
|
||||
function replace_timestamps() {
|
||||
var elements = document.getElementsByClassName("age");
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var e = elements[i];
|
||||
|
||||
var timestamp = e.innerHTML;
|
||||
e.innerHTML = how_long_ago(timestamp);
|
||||
e.style.display = "inline";
|
||||
|
||||
if (timestamp > 0) {
|
||||
var age = now() - timestamp;
|
||||
if (age < (2 * 60 * 60))
|
||||
e.className = e.className + " age-band0";
|
||||
else if (age < (3 * 24 * 60 * 60))
|
||||
e.className = e.className + " age-band1";
|
||||
else if (age < (30 * 24 * 60 * 60))
|
||||
e.className = e.className + " age-band2";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
var e = document.getElementById(id);
|
||||
|
||||
if (e.style.display == "") {
|
||||
e.style.display = "none"
|
||||
} else if (e.style.display == "none") {
|
||||
e.style.display = ""
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,37 @@
|
||||
|
||||
/* CSS for syntax highlighting.
|
||||
* Generated by pygments (what we use for syntax highlighting):
|
||||
* Generated by pygments (what we use for syntax highlighting).
|
||||
*
|
||||
* $ pygmentize -S default -f html -a .source_code
|
||||
* Light mode: pygmentize -S default -f html -a .source_code
|
||||
*/
|
||||
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.source_code .hll { background-color: #ffffcc }
|
||||
.source_code { background: #f8f8f8; }
|
||||
.source_code .c { color: #408080; font-style: italic } /* Comment */
|
||||
.source_code { background: #f8f8f8; }
|
||||
.source_code .c { color: #3D7B7B; 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 .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
||||
.source_code .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
||||
.source_code .cp { color: #9C6500 } /* Comment.Preproc */
|
||||
.source_code .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
||||
.source_code .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
|
||||
.source_code .cs { color: #3D7B7B; 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 .gr { color: #E40000 } /* 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 .gi { color: #008400 } /* Generic.Inserted */
|
||||
.source_code .go { color: #717171 } /* 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 .gt { color: #0044DD } /* 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 */
|
||||
@ -33,38 +40,139 @@
|
||||
.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 .na { color: #687822 } /* 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 .ni { color: #717171; font-weight: bold } /* Name.Entity */
|
||||
.source_code .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
||||
.source_code .nf { color: #0000FF } /* Name.Function */
|
||||
.source_code .nl { color: #A0A000 } /* Name.Label */
|
||||
.source_code .nl { color: #767600 } /* 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 .mb { color: #666666 } /* Literal.Number.Bin */
|
||||
.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 .sa { color: #BA2121 } /* Literal.String.Affix */
|
||||
.source_code .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.source_code .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.source_code .dl { color: #BA2121 } /* Literal.String.Delimiter */
|
||||
.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 .se { color: #AA5D1F; 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 .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
||||
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
||||
.source_code .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.source_code .sr { color: #A45A77 } /* 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 .fm { color: #0000FF } /* Name.Function.Magic */
|
||||
.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 .vm { color: #19177C } /* Name.Variable.Magic */
|
||||
.source_code .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
|
||||
/*
|
||||
* Dark mode: pygmentize -S native -f html -a .source_code
|
||||
*/
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.source_code .hll { background-color: #404040 }
|
||||
.source_code { background: #202020; color: #d0d0d0 }
|
||||
.source_code .c { color: #ababab; font-style: italic } /* Comment */
|
||||
.source_code .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
.source_code .esc { color: #d0d0d0 } /* Escape */
|
||||
.source_code .g { color: #d0d0d0 } /* Generic */
|
||||
.source_code .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
||||
.source_code .l { color: #d0d0d0 } /* Literal */
|
||||
.source_code .n { color: #d0d0d0 } /* Name */
|
||||
.source_code .o { color: #d0d0d0 } /* Operator */
|
||||
.source_code .x { color: #d0d0d0 } /* Other */
|
||||
.source_code .p { color: #d0d0d0 } /* Punctuation */
|
||||
.source_code .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
||||
.source_code .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
||||
.source_code .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
||||
.source_code .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
||||
.source_code .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
||||
.source_code .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
.source_code .gd { color: #d22323 } /* Generic.Deleted */
|
||||
.source_code .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
.source_code .gr { color: #d22323 } /* Generic.Error */
|
||||
.source_code .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
.source_code .gi { color: #589819 } /* Generic.Inserted */
|
||||
.source_code .go { color: #cccccc } /* Generic.Output */
|
||||
.source_code .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
.source_code .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
.source_code .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
.source_code .gt { color: #d22323 } /* Generic.Traceback */
|
||||
.source_code .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
||||
.source_code .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
||||
.source_code .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
||||
.source_code .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
||||
.source_code .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
||||
.source_code .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
||||
.source_code .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
.source_code .m { color: #51b2fd } /* Literal.Number */
|
||||
.source_code .s { color: #ed9d13 } /* Literal.String */
|
||||
.source_code .na { color: #bbbbbb } /* Name.Attribute */
|
||||
.source_code .nb { color: #2fbccd } /* Name.Builtin */
|
||||
.source_code .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
||||
.source_code .no { color: #40ffff } /* Name.Constant */
|
||||
.source_code .nd { color: #ffa500 } /* Name.Decorator */
|
||||
.source_code .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
.source_code .ne { color: #bbbbbb } /* Name.Exception */
|
||||
.source_code .nf { color: #71adff } /* Name.Function */
|
||||
.source_code .nl { color: #d0d0d0 } /* Name.Label */
|
||||
.source_code .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
||||
.source_code .nx { color: #d0d0d0 } /* Name.Other */
|
||||
.source_code .py { color: #d0d0d0 } /* Name.Property */
|
||||
.source_code .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
||||
.source_code .nv { color: #40ffff } /* Name.Variable */
|
||||
.source_code .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
||||
.source_code .w { color: #666666 } /* Text.Whitespace */
|
||||
.source_code .mb { color: #51b2fd } /* Literal.Number.Bin */
|
||||
.source_code .mf { color: #51b2fd } /* Literal.Number.Float */
|
||||
.source_code .mh { color: #51b2fd } /* Literal.Number.Hex */
|
||||
.source_code .mi { color: #51b2fd } /* Literal.Number.Integer */
|
||||
.source_code .mo { color: #51b2fd } /* Literal.Number.Oct */
|
||||
.source_code .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
.source_code .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
.source_code .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
.source_code .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
.source_code .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
.source_code .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
.source_code .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
.source_code .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
.source_code .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
.source_code .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
.source_code .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
.source_code .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
.source_code .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
.source_code .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
||||
.source_code .fm { color: #71adff } /* Name.Function.Magic */
|
||||
.source_code .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
.source_code .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
.source_code .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
.source_code .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
.source_code .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
||||
|
||||
/* Dark mode - my overrides, because the defaults are too bright. */
|
||||
|
||||
.source_code .gh { color: rgb(189, 193, 198); }
|
||||
.source_code .gu { color: rgb(189, 193, 198); }
|
||||
}
|
||||
|
||||
|
||||
166
utils.py
166
utils.py
@ -5,20 +5,44 @@ 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
|
||||
import pygments # type: ignore
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments import lexers # type: ignore
|
||||
from pygments.formatters import HtmlFormatter # type: ignore
|
||||
|
||||
_html_formatter = HtmlFormatter(
|
||||
encoding="utf-8",
|
||||
cssclass="source_code",
|
||||
linenos="table",
|
||||
anchorlinenos=True,
|
||||
lineanchors="line",
|
||||
)
|
||||
except ImportError:
|
||||
pygments = None
|
||||
|
||||
try:
|
||||
import markdown # type: ignore
|
||||
import markdown.treeprocessors # type: ignore
|
||||
except ImportError:
|
||||
markdown = None
|
||||
|
||||
def shorten(s, width = 60):
|
||||
import base64
|
||||
import functools
|
||||
import mimetypes
|
||||
import string
|
||||
import os.path
|
||||
|
||||
import git
|
||||
|
||||
|
||||
def shorten(s: str, width=60):
|
||||
if len(s) < 60:
|
||||
return s
|
||||
return s[:57] + "..."
|
||||
|
||||
def can_colorize(s):
|
||||
|
||||
@functools.lru_cache
|
||||
def can_colorize(s: str):
|
||||
"""True if we can colorize the string, False otherwise."""
|
||||
if pygments is None:
|
||||
return False
|
||||
@ -31,7 +55,7 @@ def can_colorize(s):
|
||||
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
||||
start = 0
|
||||
for i in range(5):
|
||||
pos = s.find('\n', start)
|
||||
pos = s.find("\n", start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
@ -41,30 +65,132 @@ def can_colorize(s):
|
||||
|
||||
return True
|
||||
|
||||
def colorize_diff(s):
|
||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
cssclass = 'source_code')
|
||||
|
||||
def can_markdown(repo: git.Repo, fname: str):
|
||||
"""True if we can process file through markdown, False otherwise."""
|
||||
if markdown is None:
|
||||
return False
|
||||
|
||||
if not repo.info.embed_markdown:
|
||||
return False
|
||||
|
||||
return fname.endswith(".md")
|
||||
|
||||
|
||||
def can_embed_image(repo, fname):
|
||||
"""True if we can embed image file in HTML, False otherwise."""
|
||||
if not repo.info.embed_images:
|
||||
return False
|
||||
|
||||
return ("." in fname) and (
|
||||
fname.split(".")[-1].lower() in ["jpg", "jpeg", "png", "gif"]
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def colorize_diff(s: str) -> str:
|
||||
lexer = lexers.DiffLexer(encoding="utf-8")
|
||||
formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
|
||||
|
||||
return highlight(s, lexer, formatter)
|
||||
|
||||
def colorize_blob(fname, s):
|
||||
|
||||
@functools.lru_cache
|
||||
def colorize_blob(fname, s: str) -> str:
|
||||
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:
|
||||
# Only try to guess lexers if the file starts with a shebang,
|
||||
# otherwise it's likely a text file and guess_lexer() is prone to
|
||||
# make mistakes with those.
|
||||
lexer = lexers.TextLexer(encoding = 'utf-8')
|
||||
if s.startswith('#!'):
|
||||
lexer = lexers.TextLexer(encoding="utf-8")
|
||||
if s.startswith("#!"):
|
||||
try:
|
||||
lexer = lexers.guess_lexer(s[:80], encoding = 'utf-8')
|
||||
lexer = lexers.guess_lexer(s[:80], encoding="utf-8")
|
||||
except lexers.ClassNotFound:
|
||||
pass
|
||||
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
cssclass = 'source_code',
|
||||
linenos = 'table')
|
||||
return highlight(s, lexer, _html_formatter)
|
||||
|
||||
return highlight(s, lexer, formatter)
|
||||
|
||||
def embed_image_blob(fname: str, image_data: bytes) -> str:
|
||||
mimetype = mimetypes.guess_type(fname)[0]
|
||||
b64img = base64.b64encode(image_data).decode("ascii")
|
||||
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format(
|
||||
mimetype, b64img
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def is_binary(b: bytes):
|
||||
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
||||
return b"\0" in b[:8192]
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def hexdump(s: bytes):
|
||||
graph = string.ascii_letters + string.digits + string.punctuation + " "
|
||||
b = s.decode("latin1")
|
||||
offset = 0
|
||||
while b:
|
||||
t = b[:16]
|
||||
hexvals = ["%.2x" % ord(c) for c in t]
|
||||
text = "".join(c if c in graph else "." for c in t)
|
||||
yield offset, " ".join(hexvals[:8]), " ".join(hexvals[8:]), text
|
||||
offset += 16
|
||||
b = b[16:]
|
||||
|
||||
|
||||
if markdown:
|
||||
|
||||
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
||||
"""Rewrites relative links to files, to match git-arr's links.
|
||||
|
||||
A link of "[example](a/file.md)" will be rewritten such that it links to
|
||||
"a/f=file.md.html".
|
||||
|
||||
Note that we're already assuming a degree of sanity in the HTML, so we
|
||||
don't re-check that the path is reasonable.
|
||||
"""
|
||||
|
||||
def run(self, root):
|
||||
for child in root:
|
||||
if child.tag == "a":
|
||||
self.rewrite_href(child)
|
||||
|
||||
# Continue recursively.
|
||||
self.run(child)
|
||||
|
||||
def rewrite_href(self, tag):
|
||||
"""Rewrite an <a>'s href."""
|
||||
target = tag.get("href")
|
||||
if not target:
|
||||
return
|
||||
if "://" in target or target.startswith("/"):
|
||||
return
|
||||
|
||||
head, tail = os.path.split(target)
|
||||
new_target = os.path.join(head, "f=" + tail + ".html")
|
||||
tag.set("href", new_target)
|
||||
|
||||
class RewriteLocalLinksExtension(markdown.Extension):
|
||||
def extendMarkdown(self, md):
|
||||
md.treeprocessors.register(
|
||||
RewriteLocalLinks(), "RewriteLocalLinks", 1000
|
||||
)
|
||||
|
||||
_md_extensions = [
|
||||
"markdown.extensions.fenced_code",
|
||||
"markdown.extensions.tables",
|
||||
RewriteLocalLinksExtension(),
|
||||
]
|
||||
|
||||
@functools.lru_cache
|
||||
def markdown_blob(s: str) -> str:
|
||||
return markdown.markdown(s, extensions=_md_extensions)
|
||||
|
||||
else:
|
||||
|
||||
@functools.lru_cache
|
||||
def markdown_blob(s: str) -> str:
|
||||
raise RuntimeError("markdown_blob() called without markdown support")
|
||||
|
||||
@ -1,46 +1,88 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% reltree = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.branch}} » {{dirname.unicode}}/{{fname.unicode}}</title>
|
||||
{{branch}} » {{dirname.raw}}{{fname.raw}}</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/syntax.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body class="tree">
|
||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
||||
<a href="{{relroot}}">tree</a>
|
||||
<a href="{{reltree}}../">{{branch}}</a> »
|
||||
<a href="{{reltree}}">tree</a>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||
% base = smstr(reltree)
|
||||
% for c in dirname.split('/'):
|
||||
% if not c.raw: continue
|
||||
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
||||
% if not c.raw:
|
||||
% continue
|
||||
% end
|
||||
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
||||
% base += c + '/'
|
||||
% end
|
||||
<a href="">{{!fname.html}}</a>
|
||||
</h3>
|
||||
|
||||
% if can_colorize(blob):
|
||||
{{!colorize_blob(fname.unicode, blob)}}
|
||||
% if len(blob.raw_content) == 0:
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<td>empty — 0 bytes</td>
|
||||
</tr>
|
||||
</table>
|
||||
% elif can_embed_image(repo, fname.raw):
|
||||
{{!embed_image_blob(fname.raw, blob.raw_content)}}
|
||||
% elif is_binary(blob.raw_content):
|
||||
<table class="nice blob-binary">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
binary — {{'{:,}'.format(len(blob.raw_content))}} bytes
|
||||
</td>
|
||||
</tr>
|
||||
% lim = 256
|
||||
% for offset, hex1, hex2, text in hexdump(blob.raw_content[:lim]):
|
||||
<tr>
|
||||
<td class="offset">{{offset}}</td>
|
||||
<td><pre>{{hex1}}</pre></td>
|
||||
<td><pre>{{hex2}}</pre></td>
|
||||
<td><pre>{{text}}</pre></td>
|
||||
</tr>
|
||||
% end
|
||||
% if lim < len(blob.raw_content):
|
||||
<tr class="etc">
|
||||
<td></td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
% elif can_markdown(repo, fname.raw):
|
||||
<div class="markdown">
|
||||
{{!markdown_blob(blob.utf8_content)}}
|
||||
</div>
|
||||
% elif can_colorize(blob.utf8_content):
|
||||
<div class="colorized-src">
|
||||
{{!colorize_blob(fname.raw, blob.utf8_content)}}
|
||||
</div>
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
{{blob}}
|
||||
{{blob.utf8_content}}
|
||||
</pre>
|
||||
% end
|
||||
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git » {{repo.name}} » {{repo.branch}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
||||
|
||||
% relroot = '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} » {{branch}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{relroot}}../../../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body class="branch">
|
||||
<h1><a href="../../../../">git</a> »
|
||||
<a href="../../">{{repo.name}}</a> »
|
||||
<a href="./">{{repo.branch}}</a>
|
||||
<h1><a href="{{relroot}}../../../../">git</a> »
|
||||
<a href="{{relroot}}../../">{{repo.name}}</a> »
|
||||
<a href="./">{{branch}}</a>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
<a class="explicit" href="t/">Browse current source tree</a>
|
||||
</p>
|
||||
|
||||
% commits = repo.commits("refs/heads/" + repo.branch,
|
||||
% limit = repo.info.commits_per_page,
|
||||
% commits = repo.commits("refs/heads/" + branch,
|
||||
% limit = repo.info.commits_per_page + 1,
|
||||
% offset = repo.info.commits_per_page * offset)
|
||||
% commits = list(commits)
|
||||
|
||||
@ -26,16 +29,21 @@
|
||||
% abort(404, "No more commits")
|
||||
% end
|
||||
|
||||
% more = len(commits) > repo.info.commits_per_page
|
||||
% if more:
|
||||
% commits = commits[:-1]
|
||||
% end
|
||||
% more = more and offset + 1 < repo.info.max_pages
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
% include paginate more = more, offset = offset
|
||||
|
||||
% kwargs = dict(repo=repo, commits=commits,
|
||||
% shorten=shorten, repo_root="../..")
|
||||
% shorten=shorten, repo_root=relroot + "../..")
|
||||
% include commit-list **kwargs
|
||||
|
||||
<p/>
|
||||
|
||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
||||
% include paginate more = more, offset = offset
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
% end
|
||||
% end
|
||||
|
||||
<table class="nice commits">
|
||||
<table class="nice commits" id="commits">
|
||||
|
||||
% refs = repo.refs()
|
||||
% if not defined("commits"):
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git » {{repo.name}} » commit {{c.id[:7]}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../../../static/syntax.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body class="commit">
|
||||
@ -22,10 +22,10 @@
|
||||
<span class="date" title="{{c.author_date}}">
|
||||
{{c.author_date.utc}} UTC</span></td></tr>
|
||||
<tr><td>committer</td>
|
||||
<td><span class="name">{{c.author_name}}</span>
|
||||
<span class="email"><{{c.author_email}}></span><br/>
|
||||
<span class="date" title="{{c.author_date}}">
|
||||
{{c.author_date.utc}} UTC</span></td></tr>
|
||||
<td><span class="name">{{c.committer_name}}</span>
|
||||
<span class="email"><{{c.committer_email}}></span><br/>
|
||||
<span class="date" title="{{c.committer_date}}">
|
||||
{{c.committer_date.utc}} UTC</span></td></tr>
|
||||
|
||||
% for p in c.parents:
|
||||
<tr><td>parent</td>
|
||||
@ -56,7 +56,9 @@
|
||||
<hr/>
|
||||
|
||||
% if can_colorize(c.diff.body):
|
||||
<div class="colorized-src">
|
||||
{{!colorize_diff(c.diff.body)}}
|
||||
</div>
|
||||
% else:
|
||||
<pre class="diff-body">
|
||||
{{c.diff.body}}
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git</title>
|
||||
<link rel="stylesheet" type="text/css" href="static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<script async src="static/git-arr.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="index">
|
||||
<body class="index" onload="replace_timestamps()">
|
||||
<h1>git</h1>
|
||||
|
||||
<table class="nice">
|
||||
<table class="nice projects">
|
||||
<tr>
|
||||
<th>project</th>
|
||||
<th>description</th>
|
||||
@ -18,8 +19,9 @@
|
||||
|
||||
% for repo in sorted(repos.values(), key = lambda r: r.name):
|
||||
<tr>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
||||
<td class="name"><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||
<td class="desc"><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
||||
<td><span class="age">{{repo.last_commit_timestamp()}}</span></td>
|
||||
</tr>
|
||||
%end
|
||||
</table>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<span class="inactive">← prev</span>
|
||||
% end
|
||||
<span class="sep">|</span>
|
||||
% if nelem >= max_per_page:
|
||||
% if more:
|
||||
<a href="{{offset + 1}}.html">next →</a>
|
||||
% else:
|
||||
<span class="inactive">next →</span>
|
||||
|
||||
8
views/patch.tpl
Normal file
8
views/patch.tpl
Normal file
@ -0,0 +1,8 @@
|
||||
From: {{c.author_name}} <{{c.author_email}}>
|
||||
Date: {{c.author_date}}
|
||||
Subject: {{c.subject}}
|
||||
|
||||
{{c.body.strip()}}
|
||||
---
|
||||
|
||||
{{c.diff.body}}
|
||||
@ -1,10 +1,11 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>git » {{repo.name}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<script async src="../../static/git-arr.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="summary">
|
||||
@ -25,7 +26,7 @@
|
||||
% end
|
||||
% if repo.info.git_url:
|
||||
<tr>
|
||||
<td class="category">repository</td>
|
||||
<td class="category">git clone </td>
|
||||
<td>{{! '<br/>'.join(repo.info.git_url.split())}}</td>
|
||||
</tr>
|
||||
% end
|
||||
@ -35,19 +36,25 @@
|
||||
% end
|
||||
|
||||
% if "master" in repo.branch_names():
|
||||
<div class="toggable-title" onclick="toggle('commits')">
|
||||
<a href="b/master/">commits (master)</a>
|
||||
</div>
|
||||
% kwargs = dict(repo = repo, start_ref = "refs/heads/master",
|
||||
% limit = repo.info.commits_in_summary,
|
||||
% shorten = shorten, repo_root = ".", offset = 0)
|
||||
% include commit-list **kwargs
|
||||
<hr/>
|
||||
<div class="toggable-title" onclick="toggle('ls')">
|
||||
<a href="b/master/t/">tree (master)</a>
|
||||
</div>
|
||||
% kwargs = dict(repo = repo, tree=repo.tree("master"),
|
||||
% treeroot="b/master/t", dirname=smstr.from_url(""))
|
||||
% include tree-list **kwargs
|
||||
<hr/>
|
||||
% end
|
||||
|
||||
<hr/>
|
||||
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>branches</th>
|
||||
</tr>
|
||||
|
||||
<div class="toggable-title" onclick="toggle('branches')">branches</div>
|
||||
<table class="nice toggable" id="branches">
|
||||
% for b in repo.branch_names():
|
||||
<tr>
|
||||
<td class="main"><a href="b/{{b}}/">{{b}}</a></td>
|
||||
@ -63,11 +70,8 @@
|
||||
|
||||
% tags = list(repo.tags())
|
||||
% if tags:
|
||||
<table class="nice">
|
||||
<tr>
|
||||
<th>tags</th>
|
||||
</tr>
|
||||
|
||||
<div class="toggable-title" onclick="toggle('tags')">tags</div>
|
||||
<table class="nice toggable" id="tags">
|
||||
% for name, obj_id in tags:
|
||||
<tr>
|
||||
<td><a href="c/{{obj_id}}/">{{name}}</a></td>
|
||||
|
||||
16
views/tree-list.html
Normal file
16
views/tree-list.html
Normal file
@ -0,0 +1,16 @@
|
||||
<table class="nice toggable ls" id="ls">
|
||||
% key_func = lambda x: (x[0] != 'tree', x[1].raw)
|
||||
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
||||
<tr class="{{type}}">
|
||||
% if type == "blob":
|
||||
<td class="name"><a href="{{treeroot}}/f={{name.url}}.html">
|
||||
{{!name.html}}</a></td>
|
||||
<td class="size">{{size}}</td>
|
||||
% elif type == "tree":
|
||||
<td class="name">
|
||||
<a class="explicit" href="{{treeroot}}/{{name.url}}/">
|
||||
{{!name.html}}/</a></td>
|
||||
% end
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
@ -1,54 +1,43 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
% if not dirname.raw:
|
||||
% relroot = './'
|
||||
% reltree = './'
|
||||
% else:
|
||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
||||
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||
% end
|
||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||
|
||||
<title>git » {{repo.name}} »
|
||||
{{repo.branch}} » {{dirname.unicode}}</title>
|
||||
{{branch}} » {{dirname.raw if dirname.raw else '/'}}</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body class="tree">
|
||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
||||
<a href="{{relroot}}">tree</a>
|
||||
<a href="{{reltree}}../">{{branch}}</a> »
|
||||
<a href="{{reltree}}">tree</a>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
||||
% base = smstr(relroot)
|
||||
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||
% base = smstr(reltree)
|
||||
% for c in dirname.split('/'):
|
||||
% if not c.raw: continue
|
||||
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
||||
% if not c.raw:
|
||||
% continue
|
||||
% end
|
||||
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
||||
% base += c + '/'
|
||||
% end
|
||||
</h3>
|
||||
|
||||
<table class="nice ls">
|
||||
% key_func = lambda (t, n, s): (0 if t == 'tree' else 1, n.raw)
|
||||
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
||||
<tr class="{{type}}">
|
||||
% if type == "blob":
|
||||
<td class="name"><a href="./f={{name.url}}.html">
|
||||
{{!name.html}}</a></td>
|
||||
<td class="size">{{size}}</td>
|
||||
% elif type == "tree":
|
||||
<td class="name">
|
||||
<a class="explicit" href="./{{name.url}}/">
|
||||
{{!name.html}}/</a></td>
|
||||
% end
|
||||
</tr>
|
||||
% end
|
||||
</table>
|
||||
% kwargs = dict(repo = repo, tree=tree, treeroot=".")
|
||||
% include tree-list **kwargs
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user