Compare commits
No commits in common. "master" and "0.15" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
.*
|
.*.swp
|
||||||
!.gitignore
|
|
||||||
|
|||||||
56
README
Normal file
56
README
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
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
65
README.md
@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
# 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
Normal file
13
TODO
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
In no particular order.
|
||||||
|
|
||||||
|
- Atom/RSS.
|
||||||
|
- Nicer diff:
|
||||||
|
- Better stat section, with nicer handling of filenames. We should switch to
|
||||||
|
--patch-with-raw and parse from that.
|
||||||
|
- Nicer output, don't use pygments but do our own.
|
||||||
|
- Anchors in diff sections so we can link to them.
|
||||||
|
- Short symlinks to commits, with configurable length.
|
||||||
|
- Handle symlinks properly.
|
||||||
|
- "X hours ago" via javascript (only if it's not too ugly).
|
||||||
|
|
||||||
433
git-arr
433
git-arr
@ -1,17 +1,22 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
git-arr: A git web html generator.
|
git-arr: A git web html generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import configparser
|
from __future__ import print_function
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import bottle # type: ignore
|
try:
|
||||||
|
import configparser
|
||||||
|
except ImportError:
|
||||||
|
import ConfigParser as configparser
|
||||||
|
|
||||||
|
import bottle
|
||||||
|
|
||||||
import git
|
import git
|
||||||
import utils
|
import utils
|
||||||
@ -21,13 +26,12 @@ import utils
|
|||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
bottle.TEMPLATE_PATH.insert(
|
bottle.TEMPLATE_PATH.insert(
|
||||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + "/views/"
|
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
|
||||||
)
|
|
||||||
|
|
||||||
# The path to our static files.
|
# The path to our static files.
|
||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + "/static/"
|
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
|
||||||
|
|
||||||
|
|
||||||
# The list of repositories is a global variable for convenience. It will be
|
# The list of repositories is a global variable for convenience. It will be
|
||||||
@ -42,39 +46,39 @@ def load_config(path):
|
|||||||
as configured.
|
as configured.
|
||||||
"""
|
"""
|
||||||
defaults = {
|
defaults = {
|
||||||
"tree": "yes",
|
'tree': 'yes',
|
||||||
"rootdiff": "yes",
|
'rootdiff': 'yes',
|
||||||
"desc": "",
|
'desc': '',
|
||||||
"recursive": "no",
|
'recursive': 'no',
|
||||||
"prefix": "",
|
'prefix': '',
|
||||||
"commits_in_summary": "10",
|
'commits_in_summary': '10',
|
||||||
"commits_per_page": "50",
|
'commits_per_page': '50',
|
||||||
"max_pages": "250",
|
'max_pages': '250',
|
||||||
"web_url": "",
|
'web_url': '',
|
||||||
"web_url_file": "web_url",
|
'web_url_file': 'web_url',
|
||||||
"git_url": "",
|
'git_url': '',
|
||||||
"git_url_file": "cloneurl",
|
'git_url_file': 'cloneurl',
|
||||||
"embed_markdown": "yes",
|
'embed_markdown': 'yes',
|
||||||
"embed_images": "no",
|
'embed_images': 'no',
|
||||||
"ignore": "",
|
'ignore': '',
|
||||||
"generate_patch": "yes",
|
'generate_patch': 'yes',
|
||||||
}
|
}
|
||||||
|
|
||||||
config = configparser.ConfigParser(defaults)
|
config = configparser.SafeConfigParser(defaults)
|
||||||
config.read(path)
|
config.read(path)
|
||||||
|
|
||||||
# Do a first pass for general sanity checking and recursive expansion.
|
# Do a first pass for general sanity checking and recursive expansion.
|
||||||
for s in config.sections():
|
for s in config.sections():
|
||||||
if config.getboolean(s, "recursive"):
|
if config.getboolean(s, 'recursive'):
|
||||||
root = config.get(s, "path")
|
root = config.get(s, 'path')
|
||||||
prefix = config.get(s, "prefix")
|
prefix = config.get(s, 'prefix')
|
||||||
|
|
||||||
for path in os.listdir(root):
|
for path in os.listdir(root):
|
||||||
fullpath = find_git_dir(root + "/" + path)
|
fullpath = find_git_dir(root + '/' + path)
|
||||||
if not fullpath:
|
if not fullpath:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if os.path.exists(fullpath + "/disable_gitweb"):
|
if os.path.exists(fullpath + '/disable_gitweb'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
section = prefix + path
|
section = prefix + path
|
||||||
@ -82,60 +86,58 @@ def load_config(path):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
config.add_section(section)
|
config.add_section(section)
|
||||||
for opt, value in config.items(s, raw=True):
|
for opt, value in config.items(s, raw = True):
|
||||||
config.set(section, opt, value)
|
config.set(section, opt, value)
|
||||||
|
|
||||||
config.set(section, "path", fullpath)
|
config.set(section, 'path', fullpath)
|
||||||
config.set(section, "recursive", "no")
|
config.set(section, 'recursive', 'no')
|
||||||
|
|
||||||
# This recursive section is no longer useful.
|
# This recursive section is no longer useful.
|
||||||
config.remove_section(s)
|
config.remove_section(s)
|
||||||
|
|
||||||
for s in config.sections():
|
for s in config.sections():
|
||||||
if config.get(s, "ignore") and re.search(config.get(s, "ignore"), s):
|
if config.get(s, 'ignore') and re.search(config.get(s, 'ignore'), s):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fullpath = find_git_dir(config.get(s, "path"))
|
fullpath = find_git_dir(config.get(s, 'path'))
|
||||||
if not fullpath:
|
if not fullpath:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"%s: path %s is not a valid git repository"
|
'%s: path %s is not a valid git repository' % (
|
||||||
% (s, config.get(s, "path"))
|
s, config.get(s, 'path')))
|
||||||
)
|
|
||||||
|
|
||||||
config.set(s, "path", fullpath)
|
config.set(s, 'path', fullpath)
|
||||||
config.set(s, "name", s)
|
config.set(s, 'name', s)
|
||||||
|
|
||||||
desc = config.get(s, "desc")
|
desc = config.get(s, 'desc')
|
||||||
if not desc and os.path.exists(fullpath + "/description"):
|
if not desc and os.path.exists(fullpath + '/description'):
|
||||||
desc = open(fullpath + "/description").read().strip()
|
desc = open(fullpath + '/description').read().strip()
|
||||||
|
|
||||||
r = git.Repo(fullpath, name=s)
|
r = git.Repo(fullpath, name = s)
|
||||||
r.info.desc = desc
|
r.info.desc = desc
|
||||||
r.info.commits_in_summary = config.getint(s, "commits_in_summary")
|
r.info.commits_in_summary = config.getint(s, 'commits_in_summary')
|
||||||
r.info.commits_per_page = config.getint(s, "commits_per_page")
|
r.info.commits_per_page = config.getint(s, 'commits_per_page')
|
||||||
r.info.max_pages = config.getint(s, "max_pages")
|
r.info.max_pages = config.getint(s, 'max_pages')
|
||||||
if r.info.max_pages <= 0:
|
if r.info.max_pages <= 0:
|
||||||
r.info.max_pages = sys.maxsize
|
r.info.max_pages = sys.maxint
|
||||||
r.info.generate_tree = config.getboolean(s, "tree")
|
r.info.generate_tree = config.getboolean(s, 'tree')
|
||||||
r.info.root_diff = config.getboolean(s, "rootdiff")
|
r.info.root_diff = config.getboolean(s, 'rootdiff')
|
||||||
r.info.generate_patch = config.getboolean(s, "generate_patch")
|
r.info.generate_patch = config.getboolean(s, 'generate_patch')
|
||||||
|
|
||||||
r.info.web_url = config.get(s, "web_url")
|
r.info.web_url = config.get(s, 'web_url')
|
||||||
web_url_file = fullpath + "/" + config.get(s, "web_url_file")
|
web_url_file = fullpath + '/' + config.get(s, 'web_url_file')
|
||||||
if not r.info.web_url and os.path.isfile(web_url_file):
|
if not r.info.web_url and os.path.isfile(web_url_file):
|
||||||
r.info.web_url = open(web_url_file).read()
|
r.info.web_url = open(web_url_file).read()
|
||||||
|
|
||||||
r.info.git_url = config.get(s, "git_url")
|
r.info.git_url = config.get(s, 'git_url')
|
||||||
git_url_file = fullpath + "/" + config.get(s, "git_url_file")
|
git_url_file = fullpath + '/' + config.get(s, 'git_url_file')
|
||||||
if not r.info.git_url and os.path.isfile(git_url_file):
|
if not r.info.git_url and os.path.isfile(git_url_file):
|
||||||
r.info.git_url = open(git_url_file).read()
|
r.info.git_url = open(git_url_file).read()
|
||||||
|
|
||||||
r.info.embed_markdown = config.getboolean(s, "embed_markdown")
|
r.info.embed_markdown = config.getboolean(s, 'embed_markdown')
|
||||||
r.info.embed_images = config.getboolean(s, "embed_images")
|
r.info.embed_images = config.getboolean(s, 'embed_images')
|
||||||
|
|
||||||
repos[r.name] = r
|
repos[r.name] = r
|
||||||
|
|
||||||
|
|
||||||
def find_git_dir(path):
|
def find_git_dir(path):
|
||||||
"""Returns the path to the git directory for the given repository.
|
"""Returns the path to the git directory for the given repository.
|
||||||
|
|
||||||
@ -145,26 +147,25 @@ def find_git_dir(path):
|
|||||||
|
|
||||||
An empty string is returned if the given path is not a valid repository.
|
An empty string is returned if the given path is not a valid repository.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(p):
|
def check(p):
|
||||||
"""A dirty check for whether this is a git dir or not."""
|
"""A dirty check for whether this is a git dir or not."""
|
||||||
# Note silent stderr because we expect this to fail and don't want the
|
# Note silent stderr because we expect this to fail and don't want the
|
||||||
# noise; and also we strip the final \n from the output.
|
# noise; and also we strip the final \n from the output.
|
||||||
return git.run_git(
|
return git.run_git(p,
|
||||||
p, ["rev-parse", "--git-dir"], silent_stderr=True
|
['rev-parse', '--git-dir'],
|
||||||
).read()[:-1]
|
silent_stderr = True).read()[:-1]
|
||||||
|
|
||||||
for p in [path, path + "/.git"]:
|
for p in [ path, path + '/.git' ]:
|
||||||
if check(p):
|
if check(p):
|
||||||
return p
|
return p
|
||||||
|
|
||||||
return ""
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def repo_filter(unused_conf):
|
def repo_filter(unused_conf):
|
||||||
"""Bottle route filter for repos."""
|
"""Bottle route filter for repos."""
|
||||||
# TODO: consider allowing /, which is tricky.
|
# TODO: consider allowing /, which is tricky.
|
||||||
regexp = r"[\w\.~-]+"
|
regexp = r'[\w\.~-]+'
|
||||||
|
|
||||||
def to_python(s):
|
def to_python(s):
|
||||||
"""Return the corresponding Python object."""
|
"""Return the corresponding Python object."""
|
||||||
@ -178,31 +179,30 @@ def repo_filter(unused_conf):
|
|||||||
|
|
||||||
return regexp, to_python, to_url
|
return regexp, to_python, to_url
|
||||||
|
|
||||||
|
|
||||||
app = bottle.Bottle()
|
app = bottle.Bottle()
|
||||||
app.router.add_filter("repo", repo_filter)
|
app.router.add_filter('repo', repo_filter)
|
||||||
bottle.app.push(app)
|
bottle.app.push(app)
|
||||||
|
|
||||||
|
|
||||||
def with_utils(f):
|
def with_utils(f):
|
||||||
"""Decorator to add the utilities to the return value.
|
"""Decorator to add the utilities to the return value.
|
||||||
|
|
||||||
Used to wrap functions that return dictionaries which are then passed to
|
Used to wrap functions that return dictionaries which are then passed to
|
||||||
templates.
|
templates.
|
||||||
"""
|
"""
|
||||||
utilities = {
|
utilities = {
|
||||||
"shorten": utils.shorten,
|
'shorten': utils.shorten,
|
||||||
"can_colorize": utils.can_colorize,
|
'can_colorize': utils.can_colorize,
|
||||||
"colorize_diff": utils.colorize_diff,
|
'colorize_diff': utils.colorize_diff,
|
||||||
"colorize_blob": utils.colorize_blob,
|
'colorize_blob': utils.colorize_blob,
|
||||||
"can_markdown": utils.can_markdown,
|
'can_markdown': utils.can_markdown,
|
||||||
"markdown_blob": utils.markdown_blob,
|
'markdown_blob': utils.markdown_blob,
|
||||||
"can_embed_image": utils.can_embed_image,
|
'can_embed_image': utils.can_embed_image,
|
||||||
"embed_image_blob": utils.embed_image_blob,
|
'embed_image_blob': utils.embed_image_blob,
|
||||||
"is_binary": utils.is_binary,
|
'is_binary': utils.is_binary,
|
||||||
"hexdump": utils.hexdump,
|
'hexdump': utils.hexdump,
|
||||||
"abort": bottle.abort,
|
'abort': bottle.abort,
|
||||||
"smstr": git.smstr,
|
'smstr': git.smstr,
|
||||||
}
|
}
|
||||||
|
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
@ -216,108 +216,89 @@ def with_utils(f):
|
|||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
@bottle.route('/')
|
||||||
@bottle.route("/")
|
@bottle.view('index')
|
||||||
@bottle.view("index")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def index():
|
def index():
|
||||||
return dict(repos=repos)
|
return dict(repos = repos)
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/')
|
||||||
@bottle.route("/r/<repo:repo>/")
|
@bottle.view('summary')
|
||||||
@bottle.view("summary")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def summary(repo):
|
def summary(repo):
|
||||||
return dict(repo=repo)
|
return dict(repo = repo)
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/')
|
||||||
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/")
|
@bottle.view('commit')
|
||||||
@bottle.view("commit")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def commit(repo, cid):
|
def commit(repo, cid):
|
||||||
c = repo.commit(cid)
|
c = repo.commit(cid)
|
||||||
if not c:
|
if not c:
|
||||||
bottle.abort(404, "Commit not found")
|
bottle.abort(404, 'Commit not found')
|
||||||
|
|
||||||
return dict(repo=repo, c=c)
|
return dict(repo = repo, c=c)
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>.patch')
|
||||||
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>.patch")
|
@bottle.view('patch',
|
||||||
@bottle.view(
|
# Output is text/plain, don't do HTML escaping.
|
||||||
"patch",
|
template_settings={"noescape": True})
|
||||||
# Output is text/plain, don't do HTML escaping.
|
|
||||||
template_settings={"noescape": True},
|
|
||||||
)
|
|
||||||
def patch(repo, cid):
|
def patch(repo, cid):
|
||||||
c = repo.commit(cid)
|
c = repo.commit(cid)
|
||||||
if not c:
|
if not c:
|
||||||
bottle.abort(404, "Commit not found")
|
bottle.abort(404, 'Commit not found')
|
||||||
|
|
||||||
bottle.response.content_type = "text/plain; charset=utf8"
|
bottle.response.content_type = 'text/plain; charset=utf8'
|
||||||
|
|
||||||
return dict(repo=repo, c=c)
|
return dict(repo = repo, c=c)
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html')
|
||||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html")
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html')
|
||||||
@bottle.route(
|
@bottle.view('blob')
|
||||||
"/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html"
|
|
||||||
)
|
|
||||||
@bottle.view("blob")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def blob(repo, bname, fname, dirname=""):
|
def blob(repo, bname, fname, dirname = ''):
|
||||||
if dirname and not dirname.endswith("/"):
|
if dirname and not dirname.endswith('/'):
|
||||||
dirname = dirname + "/"
|
dirname = dirname + '/'
|
||||||
|
|
||||||
dirname = git.smstr.from_url(dirname)
|
dirname = git.smstr.from_url(dirname)
|
||||||
fname = git.smstr.from_url(fname)
|
fname = git.smstr.from_url(fname)
|
||||||
path = dirname.raw + fname.raw
|
path = dirname.raw + fname.raw
|
||||||
|
|
||||||
# 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)
|
content = repo.blob(path, bname)
|
||||||
if content is None:
|
if content is None:
|
||||||
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
||||||
|
|
||||||
return dict(
|
return dict(repo = repo, branch = bname, dirname = dirname, fname = fname,
|
||||||
repo=repo, branch=bname, dirname=dirname, fname=fname, blob=content
|
blob = content)
|
||||||
)
|
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/')
|
||||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/")
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/')
|
||||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/")
|
@bottle.view('tree')
|
||||||
@bottle.view("tree")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def tree(repo, bname, dirname=""):
|
def tree(repo, bname, dirname = ''):
|
||||||
if dirname and not dirname.endswith("/"):
|
if dirname and not dirname.endswith('/'):
|
||||||
dirname = dirname + "/"
|
dirname = dirname + '/'
|
||||||
|
|
||||||
dirname = git.smstr.from_url(dirname)
|
dirname = git.smstr.from_url(dirname)
|
||||||
|
|
||||||
return dict(
|
return dict(repo = repo, branch = bname, tree = repo.tree(bname),
|
||||||
repo=repo, branch=bname, tree=repo.tree(bname), dirname=dirname
|
dirname = dirname)
|
||||||
)
|
|
||||||
|
|
||||||
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/')
|
||||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/")
|
@bottle.route('/r/<repo:repo>/b/<bname:path>/<offset:int>.html')
|
||||||
@bottle.route("/r/<repo:repo>/b/<bname:path>/<offset:int>.html")
|
@bottle.view('branch')
|
||||||
@bottle.view("branch")
|
|
||||||
@with_utils
|
@with_utils
|
||||||
def branch(repo, bname, offset=0):
|
def branch(repo, bname, offset = 0):
|
||||||
return dict(repo=repo, branch=bname, offset=offset)
|
return dict(repo = repo, branch = bname, offset = offset)
|
||||||
|
|
||||||
|
@bottle.route('/static/<path:path>')
|
||||||
@bottle.route("/static/<path:path>")
|
|
||||||
def static(path):
|
def static(path):
|
||||||
return bottle.static_file(path, root=static_path)
|
return bottle.static_file(path, root = static_path)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Static HTML generation
|
# Static HTML generation
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def is_404(e):
|
def is_404(e):
|
||||||
"""True if e is an HTTPError with status 404, False otherwise."""
|
"""True if e is an HTTPError with status 404, False otherwise."""
|
||||||
# We need this because older bottle.py versions put the status code in
|
# We need this because older bottle.py versions put the status code in
|
||||||
@ -328,19 +309,17 @@ def is_404(e):
|
|||||||
else:
|
else:
|
||||||
return e.status_code == 404
|
return e.status_code == 404
|
||||||
|
|
||||||
|
def generate(output, only = None):
|
||||||
def generate(output: str, only=None):
|
|
||||||
"""Generate static html to the output directory."""
|
"""Generate static html to the output directory."""
|
||||||
|
def write_to(path, func_or_str, args = (), mtime = None):
|
||||||
def write_to(path: str, func_or_str, args=(), mtime=None):
|
path = output + '/' + path
|
||||||
path = output + "/" + path
|
|
||||||
dirname = os.path.dirname(path)
|
dirname = os.path.dirname(path)
|
||||||
|
|
||||||
if not os.path.exists(dirname):
|
if not os.path.exists(dirname):
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
|
|
||||||
if mtime:
|
if mtime:
|
||||||
path_mtime: Union[float, int] = 0
|
path_mtime = 0
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
path_mtime = os.stat(path).st_mtime
|
path_mtime = os.stat(path).st_mtime
|
||||||
|
|
||||||
@ -360,7 +339,7 @@ def generate(output: str, only=None):
|
|||||||
else:
|
else:
|
||||||
# Otherwise, be lazy if we were given a function to run, or write
|
# Otherwise, be lazy if we were given a function to run, or write
|
||||||
# always if they gave us a string.
|
# always if they gave us a string.
|
||||||
if isinstance(func_or_str, str):
|
if isinstance(func_or_str, (str, unicode)):
|
||||||
print(path)
|
print(path)
|
||||||
s = func_or_str
|
s = func_or_str
|
||||||
else:
|
else:
|
||||||
@ -369,99 +348,71 @@ def generate(output: str, only=None):
|
|||||||
print(path)
|
print(path)
|
||||||
s = func_or_str(*args)
|
s = func_or_str(*args)
|
||||||
|
|
||||||
open(path, "w").write(s)
|
open(path, 'w').write(s.encode('utf8', errors = 'xmlcharrefreplace'))
|
||||||
if mtime:
|
if mtime:
|
||||||
os.utime(path, (mtime, mtime))
|
os.utime(path, (mtime, mtime))
|
||||||
|
|
||||||
def link(from_path, to_path):
|
def link(from_path, to_path):
|
||||||
from_path = output + "/" + from_path
|
from_path = output + '/' + from_path
|
||||||
|
|
||||||
if os.path.lexists(from_path):
|
if os.path.lexists(from_path):
|
||||||
return
|
return
|
||||||
print(from_path, "->", to_path)
|
print(from_path, '->', to_path)
|
||||||
os.symlink(to_path, from_path)
|
os.symlink(to_path, from_path)
|
||||||
|
|
||||||
def write_tree(r: git.Repo, bn: str, mtime):
|
def write_tree(r, bn, mtime):
|
||||||
t: git.Tree = r.tree(bn)
|
t = 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
|
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
||||||
# expected to fail for now; we skip them.
|
# expected to fail for now; we skip them.
|
||||||
if "\n" in oname.raw:
|
if '\n' in oname.raw:
|
||||||
print("skipping file with \\n: %r" % (oname.raw))
|
print('skipping file with \\n: %r' % (oname.raw))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if otype == "blob":
|
if otype == 'blob':
|
||||||
dirname = git.smstr(os.path.dirname(oname.raw))
|
dirname = git.smstr(os.path.dirname(oname.raw))
|
||||||
fname = git.smstr(os.path.basename(oname.raw))
|
fname = git.smstr(os.path.basename(oname.raw))
|
||||||
write_to(
|
write_to(
|
||||||
"r/%s/b/%s/t/%s%sf=%s.html"
|
'r/%s/b/%s/t/%s%sf=%s.html' %
|
||||||
% (
|
(str(r.name), str(bn),
|
||||||
str(r.name),
|
dirname.raw, '/' if dirname.raw else '', fname.raw),
|
||||||
str(bn),
|
blob, (r, bn, fname.url, dirname.url), mtime)
|
||||||
dirname.raw,
|
|
||||||
"/" if dirname.raw else "",
|
|
||||||
fname.raw,
|
|
||||||
),
|
|
||||||
blob,
|
|
||||||
(r, bn, fname.url, dirname.url),
|
|
||||||
mtime,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
write_to(
|
write_to('r/%s/b/%s/t/%s/index.html' %
|
||||||
"r/%s/b/%s/t/%s/index.html"
|
(str(r.name), str(bn), oname.raw),
|
||||||
% (str(r.name), str(bn), oname.raw),
|
tree, (r, bn, oname.url), mtime)
|
||||||
tree,
|
|
||||||
(r, bn, oname.url),
|
|
||||||
mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Always generate the index, to keep the "last updated" time fresh.
|
# Always generate the index, to keep the "last updated" time fresh.
|
||||||
write_to("index.html", index())
|
write_to('index.html', index())
|
||||||
|
|
||||||
# We can't call static() because it relies on HTTP headers.
|
# We can't call static() because it relies on HTTP headers.
|
||||||
read_f = lambda f: open(f).read()
|
read_f = lambda f: open(f).read()
|
||||||
write_to(
|
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
||||||
"static/git-arr.css",
|
os.stat(static_path + '/git-arr.css').st_mtime)
|
||||||
read_f,
|
write_to('static/git-arr.js', read_f, [static_path + '/git-arr.js'],
|
||||||
[static_path + "/git-arr.css"],
|
os.stat(static_path + '/git-arr.js').st_mtime)
|
||||||
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.js",
|
|
||||||
read_f,
|
|
||||||
[static_path + "/git-arr.js"],
|
|
||||||
os.stat(static_path + "/git-arr.js").st_mtime,
|
|
||||||
)
|
|
||||||
write_to(
|
|
||||||
"static/syntax.css",
|
|
||||||
read_f,
|
|
||||||
[static_path + "/syntax.css"],
|
|
||||||
os.stat(static_path + "/syntax.css").st_mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
rs = sorted(list(repos.values()), key=lambda r: r.name)
|
rs = sorted(repos.values(), key = lambda r: r.name)
|
||||||
if only:
|
if only:
|
||||||
rs = [r for r in rs if r.name in only]
|
rs = [r for r in rs if r.name in only]
|
||||||
|
|
||||||
for r in rs:
|
for r in rs:
|
||||||
write_to("r/%s/index.html" % r.name, summary(r))
|
write_to('r/%s/index.html' % r.name, summary(r))
|
||||||
for bn in r.branch_names():
|
for bn in r.branch_names():
|
||||||
commit_count = 0
|
commit_count = 0
|
||||||
commit_ids = r.commit_ids(
|
commit_ids = r.commit_ids('refs/heads/' + bn,
|
||||||
"refs/heads/" + bn,
|
limit = r.info.commits_per_page * r.info.max_pages)
|
||||||
limit=r.info.commits_per_page * r.info.max_pages,
|
|
||||||
)
|
|
||||||
for cid in commit_ids:
|
for cid in commit_ids:
|
||||||
write_to(
|
write_to('r/%s/c/%s/index.html' % (r.name, cid),
|
||||||
"r/%s/c/%s/index.html" % (r.name, cid), commit, (r, cid)
|
commit, (r, cid))
|
||||||
)
|
|
||||||
if r.info.generate_patch:
|
if r.info.generate_patch:
|
||||||
write_to(
|
write_to('r/%s/c/%s.patch' % (r.name, cid), patch, (r, cid))
|
||||||
"r/%s/c/%s.patch" % (r.name, cid), patch, (r, cid)
|
|
||||||
)
|
|
||||||
commit_count += 1
|
commit_count += 1
|
||||||
|
|
||||||
# To avoid regenerating files that have not changed, we will
|
# To avoid regenerating files that have not changed, we will
|
||||||
@ -470,83 +421,65 @@ def generate(output: str, only=None):
|
|||||||
# write.
|
# write.
|
||||||
branch_mtime = r.commit(bn).committer_date.epoch
|
branch_mtime = r.commit(bn).committer_date.epoch
|
||||||
|
|
||||||
nr_pages = int(
|
nr_pages = int(math.ceil(
|
||||||
math.ceil(float(commit_count) / r.info.commits_per_page)
|
float(commit_count) / r.info.commits_per_page))
|
||||||
)
|
|
||||||
nr_pages = min(nr_pages, r.info.max_pages)
|
nr_pages = min(nr_pages, r.info.max_pages)
|
||||||
|
|
||||||
for page in range(nr_pages):
|
for page in range(nr_pages):
|
||||||
write_to(
|
write_to('r/%s/b/%s/%d.html' % (r.name, bn, page),
|
||||||
"r/%s/b/%s/%d.html" % (r.name, bn, page),
|
branch, (r, bn, page), branch_mtime)
|
||||||
branch,
|
|
||||||
(r, bn, page),
|
|
||||||
branch_mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
link(
|
link(from_path = 'r/%s/b/%s/index.html' % (r.name, bn),
|
||||||
from_path="r/%s/b/%s/index.html" % (r.name, bn),
|
to_path = '0.html')
|
||||||
to_path="0.html",
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.info.generate_tree:
|
if r.info.generate_tree:
|
||||||
write_tree(r, bn, branch_mtime)
|
write_tree(r, bn, branch_mtime)
|
||||||
|
|
||||||
for tag_name, obj_id in r.tags():
|
for tag_name, obj_id in r.tags():
|
||||||
try:
|
try:
|
||||||
write_to(
|
write_to('r/%s/c/%s/index.html' % (r.name, obj_id),
|
||||||
"r/%s/c/%s/index.html" % (r.name, obj_id),
|
commit, (r, obj_id))
|
||||||
commit,
|
|
||||||
(r, obj_id),
|
|
||||||
)
|
|
||||||
except bottle.HTTPError as e:
|
except bottle.HTTPError as e:
|
||||||
# Some repos can have tags pointing to non-commits. This
|
# Some repos can have tags pointing to non-commits. This
|
||||||
# happens in the Linux Kernel's v2.6.11, which points directly
|
# happens in the Linux Kernel's v2.6.11, which points directly
|
||||||
# to a tree. Ignore them.
|
# to a tree. Ignore them.
|
||||||
if is_404(e):
|
if is_404(e):
|
||||||
print("404 in tag %s (%s)" % (tag_name, obj_id))
|
print('404 in tag %s (%s)' % (tag_name, obj_id))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = optparse.OptionParser("usage: %prog [options] serve|generate")
|
parser = optparse.OptionParser('usage: %prog [options] serve|generate')
|
||||||
parser.add_option(
|
parser.add_option('-c', '--config', metavar = 'FILE',
|
||||||
"-c", "--config", metavar="FILE", help="configuration file"
|
help = 'configuration file')
|
||||||
)
|
parser.add_option('-o', '--output', metavar = 'DIR',
|
||||||
parser.add_option(
|
help = 'output directory (for generate)')
|
||||||
"-o", "--output", metavar="DIR", help="output directory (for generate)"
|
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
|
||||||
)
|
default = [],
|
||||||
parser.add_option(
|
help = 'generate/serve only this repository')
|
||||||
"",
|
|
||||||
"--only",
|
|
||||||
metavar="REPO",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
help="generate/serve only this repository",
|
|
||||||
)
|
|
||||||
opts, args = parser.parse_args()
|
opts, args = parser.parse_args()
|
||||||
|
|
||||||
if not opts.config:
|
if not opts.config:
|
||||||
parser.error("--config is mandatory")
|
parser.error('--config is mandatory')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
load_config(opts.config)
|
load_config(opts.config)
|
||||||
except (configparser.NoOptionError, ValueError) as e:
|
except (configparser.NoOptionError, ValueError) as e:
|
||||||
print("Error parsing config:", e)
|
print('Error parsing config:', e)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
parser.error("Must specify an action (serve|generate)")
|
parser.error('Must specify an action (serve|generate)')
|
||||||
|
|
||||||
if args[0] == "serve":
|
if args[0] == 'serve':
|
||||||
bottle.run(host="localhost", port=8008, reloader=True)
|
bottle.run(host = 'localhost', port = 8008, reloader = True)
|
||||||
elif args[0] == "generate":
|
elif args[0] == 'generate':
|
||||||
if not opts.output:
|
if not opts.output:
|
||||||
parser.error("Must specify --output")
|
parser.error('Must specify --output')
|
||||||
generate(output=opts.output, only=opts.only)
|
generate(output = opts.output, only = opts.only)
|
||||||
else:
|
else:
|
||||||
parser.error("Unknown action %s" % args[0])
|
parser.error('Unknown action %s' % args[0])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
408
git.py
408
git.py
@ -6,93 +6,107 @@ command line tool directly, so please be careful with using untrusted
|
|||||||
parameters.
|
parameters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import functools
|
|
||||||
import sys
|
import sys
|
||||||
import io
|
import io
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import email.utils
|
import email.utils
|
||||||
import datetime
|
import datetime
|
||||||
import urllib.request, urllib.parse, urllib.error
|
import urllib
|
||||||
from html import escape
|
from cgi import escape
|
||||||
from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
|
|
||||||
# Path to the git binary.
|
# Path to the git binary.
|
||||||
GIT_BIN = "git"
|
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 run_git(
|
def __iter__(self):
|
||||||
repo_path: str, params, stdin: bytes = None, silent_stderr=False, raw=False
|
for line in self.fd:
|
||||||
) -> Union[IO[str], IO[bytes]]:
|
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, raw = False):
|
||||||
"""Invokes git with the given parameters.
|
"""Invokes git with the given parameters.
|
||||||
|
|
||||||
This function invokes git with the given parameters, and returns a
|
This function invokes git with the given parameters, and returns a
|
||||||
file-like object with the output (from a pipe).
|
file-like object with the output (from a pipe).
|
||||||
"""
|
"""
|
||||||
params = [GIT_BIN, "--git-dir=%s" % repo_path] + list(params)
|
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
|
||||||
|
|
||||||
stderr = None
|
stderr = None
|
||||||
if silent_stderr:
|
if silent_stderr:
|
||||||
stderr = subprocess.PIPE
|
stderr = subprocess.PIPE
|
||||||
|
|
||||||
if not stdin:
|
if not stdin:
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(params,
|
||||||
params, stdin=None, stdout=subprocess.PIPE, stderr=stderr
|
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(params,
|
||||||
params,
|
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stderr = stderr)
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert p.stdin is not None
|
|
||||||
p.stdin.write(stdin)
|
p.stdin.write(stdin)
|
||||||
p.stdin.close()
|
p.stdin.close()
|
||||||
|
|
||||||
assert p.stdout is not None
|
|
||||||
|
|
||||||
if raw:
|
if raw:
|
||||||
return p.stdout
|
return p.stdout
|
||||||
|
|
||||||
return io.TextIOWrapper(
|
# We need to wrap stdout if we want to decode it as utf8, subprocess
|
||||||
p.stdout, encoding="utf8", errors="backslashreplace"
|
# doesn't support us telling it the encoding.
|
||||||
)
|
if sys.version_info.major == 3:
|
||||||
|
return io.TextIOWrapper(p.stdout, encoding = 'utf8',
|
||||||
|
errors = 'replace')
|
||||||
|
else:
|
||||||
|
return EncodeWrapper(p.stdout)
|
||||||
|
|
||||||
|
|
||||||
class GitCommand(object):
|
class GitCommand (object):
|
||||||
"""Convenient way of invoking git."""
|
"""Convenient way of invoking git."""
|
||||||
|
def __init__(self, path, cmd, *args, **kwargs):
|
||||||
def __init__(self, path: str, cmd: str):
|
|
||||||
self._override = True
|
self._override = True
|
||||||
self._path = path
|
self._path = path
|
||||||
self._cmd = cmd
|
self._cmd = cmd
|
||||||
self._args: List[str] = []
|
self._args = list(args)
|
||||||
self._kwargs: Dict[str, str] = {}
|
self._kwargs = {}
|
||||||
self._stdin_buf: Optional[bytes] = None
|
self._stdin_buf = None
|
||||||
self._raw = False
|
self._raw = False
|
||||||
self._override = False
|
self._override = False
|
||||||
|
for k, v in kwargs:
|
||||||
|
self.__setattr__(k, v)
|
||||||
|
|
||||||
def __setattr__(self, k, v):
|
def __setattr__(self, k, v):
|
||||||
if k == "_override" or self._override:
|
if k == '_override' or self._override:
|
||||||
self.__dict__[k] = v
|
self.__dict__[k] = v
|
||||||
return
|
return
|
||||||
k = k.replace("_", "-")
|
k = k.replace('_', '-')
|
||||||
self._kwargs[k] = v
|
self._kwargs[k] = v
|
||||||
|
|
||||||
def arg(self, a: str):
|
def arg(self, a):
|
||||||
"""Adds an argument."""
|
"""Adds an argument."""
|
||||||
self._args.append(a)
|
self._args.append(a)
|
||||||
|
|
||||||
def raw(self, b: bool):
|
def raw(self, b):
|
||||||
"""Request raw rather than utf8-encoded command output."""
|
"""Request raw rather than utf8-encoded command output."""
|
||||||
self._override = True
|
self._override = True
|
||||||
self._raw = b
|
self._raw = b
|
||||||
self._override = False
|
self._override = False
|
||||||
|
|
||||||
def stdin(self, s: bytes):
|
def stdin(self, s):
|
||||||
"""Sets the contents we will send in stdin."""
|
"""Sets the contents we will send in stdin."""
|
||||||
self._override = True
|
self._override = True
|
||||||
self._stdin_buf = s
|
self._stdin_buf = s
|
||||||
@ -102,37 +116,46 @@ class GitCommand(object):
|
|||||||
"""Runs the git command."""
|
"""Runs the git command."""
|
||||||
params = [self._cmd]
|
params = [self._cmd]
|
||||||
|
|
||||||
for k, v in list(self._kwargs.items()):
|
for k, v in self._kwargs.items():
|
||||||
dash = "--" if len(k) > 1 else "-"
|
dash = '--' if len(k) > 1 else '-'
|
||||||
if v is None:
|
if v is None:
|
||||||
params.append("%s%s" % (dash, k))
|
params.append('%s%s' % (dash, k))
|
||||||
else:
|
else:
|
||||||
params.append("%s%s=%s" % (dash, k, str(v)))
|
params.append('%s%s=%s' % (dash, k, str(v)))
|
||||||
|
|
||||||
params.extend(self._args)
|
params.extend(self._args)
|
||||||
|
|
||||||
return run_git(self._path, params, self._stdin_buf, raw=self._raw)
|
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."""
|
"""An entirely flexible object, which provides a convenient namespace."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
class smstr:
|
class smstr:
|
||||||
"""A "smart" string, containing many representations for ease of use."""
|
"""A "smart" string, containing many representations for ease of use.
|
||||||
|
|
||||||
raw: str # string, probably utf8-encoded, good enough to show.
|
This is a string class that contains:
|
||||||
url: str # escaped for safe embedding in URLs (not human-readable).
|
.raw -> raw string, authoritative source.
|
||||||
html: str # HTML-embeddable representation.
|
.unicode -> unicode representation, may not be perfect if .raw is not
|
||||||
|
proper utf8 but should be good enough to show.
|
||||||
def __init__(self, s: str):
|
.url -> escaped for safe embedding in URLs, can be not quite
|
||||||
self.raw = s
|
readable.
|
||||||
self.url = urllib.request.pathname2url(s)
|
.html -> an HTML-embeddable representation.
|
||||||
|
"""
|
||||||
|
def __init__(self, raw):
|
||||||
|
if not isinstance(raw, str):
|
||||||
|
raise TypeError("The raw string must be instance of 'str'")
|
||||||
|
self.raw = raw
|
||||||
|
self.unicode = raw.decode('utf8', errors = 'replace')
|
||||||
|
self.url = urllib.pathname2url(raw)
|
||||||
self.html = self._to_html()
|
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
|
# 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
|
# misuse. It does mean that some uses become more annoying, so it's a
|
||||||
# tradeoff that may change in the future.
|
# tradeoff that may change in the future.
|
||||||
@ -140,11 +163,11 @@ class smstr:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_url(url):
|
def from_url(url):
|
||||||
"""Returns an smstr() instance from an url-encoded string."""
|
"""Returns an smstr() instance from an url-encoded string."""
|
||||||
return smstr(urllib.request.url2pathname(url))
|
return smstr(urllib.url2pathname(url))
|
||||||
|
|
||||||
def split(self, sep):
|
def split(self, sep):
|
||||||
"""Like str.split()."""
|
"""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):
|
def __add__(self, other):
|
||||||
if isinstance(other, smstr):
|
if isinstance(other, smstr):
|
||||||
@ -153,10 +176,10 @@ class smstr:
|
|||||||
|
|
||||||
def _to_html(self):
|
def _to_html(self):
|
||||||
"""Returns an html representation of the unicode string."""
|
"""Returns an html representation of the unicode string."""
|
||||||
html = ""
|
html = u''
|
||||||
for c in escape(self.raw):
|
for c in escape(self.unicode):
|
||||||
if c in "\t\r\n\r\f\a\b\v\0":
|
if c in '\t\r\n\r\f\a\b\v\0':
|
||||||
esc_c = c.encode("unicode-escape").decode("utf8")
|
esc_c = c.encode('ascii').encode('string_escape')
|
||||||
html += '<span class="ctrlchr">%s</span>' % esc_c
|
html += '<span class="ctrlchr">%s</span>' % esc_c
|
||||||
else:
|
else:
|
||||||
html += c
|
html += c
|
||||||
@ -164,26 +187,17 @@ class smstr:
|
|||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
def unquote(s: str):
|
def unquote(s):
|
||||||
"""Git can return quoted file names, unquote them. Always return a str."""
|
"""Git can return quoted file names, unquote them. Always return a str."""
|
||||||
if not (s[0] == '"' and s[-1] == '"'):
|
if not (s[0] == '"' and s[-1] == '"'):
|
||||||
# Unquoted strings are always safe, no need to mess with them
|
# Unquoted strings are always safe, no need to mess with them; just
|
||||||
|
# make sure we return str.
|
||||||
|
s = s.encode('ascii')
|
||||||
return s
|
return s
|
||||||
|
|
||||||
# The string will be of the form `"<escaped>"`, where <escaped> is a
|
# Get rid of the quotes, we never want them in the output, and convert to
|
||||||
# backslash-escaped representation of the name of the file.
|
# a raw string, un-escaping the backslashes.
|
||||||
# Examples: "with\ttwo\ttabs" , "\303\261aca-utf8", "\361aca-latin1"
|
s = s[1:-1].decode('string-escape')
|
||||||
|
|
||||||
# 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
|
return s
|
||||||
|
|
||||||
@ -191,19 +205,18 @@ def unquote(s: str):
|
|||||||
class Repo:
|
class Repo:
|
||||||
"""A git repository."""
|
"""A git repository."""
|
||||||
|
|
||||||
def __init__(self, path: str, name=None, info=None):
|
def __init__(self, path, name = None, info = None):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.name = name
|
self.name = name
|
||||||
self.info: Any = info or SimpleNamespace()
|
self.info = info or SimpleNamespace()
|
||||||
|
|
||||||
def cmd(self, cmd):
|
def cmd(self, cmd):
|
||||||
"""Returns a GitCommand() on our path."""
|
"""Returns a GitCommand() on our path."""
|
||||||
return GitCommand(self.path, cmd)
|
return GitCommand(self.path, cmd)
|
||||||
|
|
||||||
@functools.lru_cache
|
def for_each_ref(self, pattern = None, sort = None, count = None):
|
||||||
def _for_each_ref(self, pattern=None, sort=None, count=None):
|
|
||||||
"""Returns a list of references."""
|
"""Returns a list of references."""
|
||||||
cmd = self.cmd("for-each-ref")
|
cmd = self.cmd('for-each-ref')
|
||||||
if sort:
|
if sort:
|
||||||
cmd.sort = sort
|
cmd.sort = sort
|
||||||
if count:
|
if count:
|
||||||
@ -211,66 +224,70 @@ class Repo:
|
|||||||
if pattern:
|
if pattern:
|
||||||
cmd.arg(pattern)
|
cmd.arg(pattern)
|
||||||
|
|
||||||
refs = []
|
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
obj_id, obj_type, ref = l.split()
|
obj_id, obj_type, ref = l.split()
|
||||||
refs.append((obj_id, obj_type, ref))
|
yield obj_id, obj_type, ref
|
||||||
return refs
|
|
||||||
|
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
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def branch_names(self):
|
def branch_names(self):
|
||||||
"""Get the names of the branches."""
|
"""Get the names of the branches."""
|
||||||
refs = self._for_each_ref(pattern="refs/heads/", sort="-authordate")
|
return ( name for name, _ in self.branches() )
|
||||||
return [ref[len("refs/heads/") :] for _, _, ref in refs]
|
|
||||||
|
|
||||||
@functools.cache
|
def tags(self, sort = '-taggerdate'):
|
||||||
def tags(self, sort="-taggerdate"):
|
|
||||||
"""Get the (name, obj_id) of the tags."""
|
"""Get the (name, obj_id) of the tags."""
|
||||||
refs = self._for_each_ref(pattern="refs/tags/", sort=sort)
|
refs = self.for_each_ref(pattern = 'refs/tags/', sort = sort)
|
||||||
return [(ref[len("refs/tags/") :], obj_id) for obj_id, _, ref in refs]
|
for obj_id, _, ref in refs:
|
||||||
|
yield ref[len('refs/tags/'):], obj_id
|
||||||
|
|
||||||
@functools.lru_cache
|
def tag_names(self):
|
||||||
def commit_ids(self, ref, limit=None):
|
"""Get the names of the tags."""
|
||||||
|
return ( name for name, _ in self.tags() )
|
||||||
|
|
||||||
|
def commit_ids(self, ref, limit = None):
|
||||||
"""Generate commit ids."""
|
"""Generate commit ids."""
|
||||||
cmd = self.cmd("rev-list")
|
cmd = self.cmd('rev-list')
|
||||||
if limit:
|
if limit:
|
||||||
cmd.max_count = limit
|
cmd.max_count = limit
|
||||||
|
|
||||||
cmd.arg(ref)
|
cmd.arg(ref)
|
||||||
cmd.arg("--")
|
cmd.arg('--')
|
||||||
|
|
||||||
return [l.rstrip("\n") for l in cmd.run()]
|
for l in cmd.run():
|
||||||
|
yield l.rstrip('\n')
|
||||||
|
|
||||||
@functools.lru_cache
|
|
||||||
def commit(self, commit_id):
|
def commit(self, commit_id):
|
||||||
"""Return a single commit."""
|
"""Return a single commit."""
|
||||||
cs = list(self.commits(commit_id, limit=1))
|
cs = list(self.commits(commit_id, limit = 1))
|
||||||
if len(cs) != 1:
|
if len(cs) != 1:
|
||||||
return None
|
return None
|
||||||
return cs[0]
|
return cs[0]
|
||||||
|
|
||||||
@functools.lru_cache
|
def commits(self, ref, limit = None, offset = 0):
|
||||||
def commits(self, ref, limit, offset=0):
|
|
||||||
"""Generate commit objects for the ref."""
|
"""Generate commit objects for the ref."""
|
||||||
cmd = self.cmd("rev-list")
|
cmd = self.cmd('rev-list')
|
||||||
cmd.max_count = limit + offset
|
if limit:
|
||||||
|
cmd.max_count = limit + offset
|
||||||
|
|
||||||
cmd.header = None
|
cmd.header = None
|
||||||
|
|
||||||
cmd.arg(ref)
|
cmd.arg(ref)
|
||||||
cmd.arg("--")
|
cmd.arg('--')
|
||||||
|
|
||||||
info_buffer = ""
|
info_buffer = ''
|
||||||
count = 0
|
count = 0
|
||||||
commits = []
|
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
if "\0" in l:
|
if '\0' in l:
|
||||||
pre, post = l.split("\0", 1)
|
pre, post = l.split('\0', 1)
|
||||||
info_buffer += pre
|
info_buffer += pre
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
if count > offset:
|
if count > offset:
|
||||||
commits.append(Commit.from_str(self, info_buffer))
|
yield Commit.from_str(self, info_buffer)
|
||||||
|
|
||||||
# Start over.
|
# Start over.
|
||||||
info_buffer = post
|
info_buffer = post
|
||||||
@ -280,18 +297,15 @@ class Repo:
|
|||||||
if info_buffer:
|
if info_buffer:
|
||||||
count += 1
|
count += 1
|
||||||
if count > offset:
|
if count > offset:
|
||||||
commits.append(Commit.from_str(self, info_buffer))
|
yield Commit.from_str(self, info_buffer)
|
||||||
|
|
||||||
return commits
|
|
||||||
|
|
||||||
@functools.lru_cache
|
|
||||||
def diff(self, ref):
|
def diff(self, ref):
|
||||||
"""Return a Diff object for the ref."""
|
"""Return a Diff object for the ref."""
|
||||||
cmd = self.cmd("diff-tree")
|
cmd = self.cmd('diff-tree')
|
||||||
cmd.patch = None
|
cmd.patch = None
|
||||||
cmd.numstat = None
|
cmd.numstat = None
|
||||||
cmd.find_renames = None
|
cmd.find_renames = None
|
||||||
if self.info.root_diff:
|
if (self.info.root_diff):
|
||||||
cmd.root = None
|
cmd.root = None
|
||||||
# Note we intentionally do not use -z, as the filename is just for
|
# Note we intentionally do not use -z, as the filename is just for
|
||||||
# reference, and it is safer to let git do the escaping.
|
# reference, and it is safer to let git do the escaping.
|
||||||
@ -300,72 +314,58 @@ class Repo:
|
|||||||
|
|
||||||
return Diff.from_str(cmd.run())
|
return Diff.from_str(cmd.run())
|
||||||
|
|
||||||
@functools.lru_cache
|
|
||||||
def refs(self):
|
def refs(self):
|
||||||
"""Return a dict of obj_id -> ref."""
|
"""Return a dict of obj_id -> ref."""
|
||||||
cmd = self.cmd("show-ref")
|
cmd = self.cmd('show-ref')
|
||||||
cmd.dereference = None
|
cmd.dereference = None
|
||||||
|
|
||||||
r = defaultdict(list)
|
r = defaultdict(list)
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
l = l.strip()
|
l = l.strip()
|
||||||
obj_id, ref = l.split(" ", 1)
|
obj_id, ref = l.split(' ', 1)
|
||||||
r[obj_id].append(ref)
|
r[obj_id].append(ref)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@functools.lru_cache
|
|
||||||
def tree(self, ref):
|
def tree(self, ref):
|
||||||
"""Returns a Tree instance for the given ref."""
|
"""Returns a Tree instance for the given ref."""
|
||||||
return Tree(self, ref)
|
return Tree(self, ref)
|
||||||
|
|
||||||
@functools.lru_cache
|
|
||||||
def blob(self, path, ref):
|
def blob(self, path, ref):
|
||||||
"""Returns a Blob instance for the given path."""
|
"""Returns a Blob instance for the given path."""
|
||||||
cmd = self.cmd("cat-file")
|
cmd = self.cmd('cat-file')
|
||||||
cmd.raw(True)
|
cmd.raw(True)
|
||||||
cmd.batch = "%(objectsize)"
|
cmd.batch = '%(objectsize)'
|
||||||
|
|
||||||
# Format: <ref>:<path>
|
if isinstance(ref, unicode):
|
||||||
# Construct it in binary since the path might not be utf8.
|
ref = ref.encode('utf8')
|
||||||
cmd.stdin(ref.encode("utf8") + b":" + path)
|
cmd.stdin('%s:%s' % (ref, path))
|
||||||
|
|
||||||
out = cmd.run()
|
out = cmd.run()
|
||||||
head = out.readline()
|
head = out.readline()
|
||||||
if not head or head.strip().endswith(b"missing"):
|
if not head or head.strip().endswith('missing'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Blob(out.read()[: int(head)])
|
return Blob(out.read()[:int(head)])
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def last_commit_timestamp(self):
|
def last_commit_timestamp(self):
|
||||||
"""Return the timestamp of the last commit."""
|
"""Return the timestamp of the last commit."""
|
||||||
refs = self._for_each_ref(
|
refs = self.for_each_ref(pattern = 'refs/heads/',
|
||||||
pattern="refs/heads/", sort="-committerdate", count=1
|
sort = '-committerdate', count = 1)
|
||||||
)
|
|
||||||
for obj_id, _, _ in refs:
|
for obj_id, _, _ in refs:
|
||||||
commit = self.commit(obj_id)
|
commit = self.commit(obj_id)
|
||||||
return commit.committer_epoch
|
return commit.committer_epoch
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
class Commit(object):
|
class Commit (object):
|
||||||
"""A git commit."""
|
"""A git commit."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, repo,
|
||||||
self,
|
commit_id, parents, tree,
|
||||||
repo,
|
author, author_epoch, author_tz,
|
||||||
commit_id,
|
committer, committer_epoch, committer_tz,
|
||||||
parents,
|
message):
|
||||||
tree,
|
|
||||||
author,
|
|
||||||
author_epoch,
|
|
||||||
author_tz,
|
|
||||||
committer,
|
|
||||||
committer_epoch,
|
|
||||||
committer_tz,
|
|
||||||
message,
|
|
||||||
):
|
|
||||||
self._repo = repo
|
self._repo = repo
|
||||||
self.id = commit_id
|
self.id = commit_id
|
||||||
self.parents = parents
|
self.parents = parents
|
||||||
@ -378,30 +378,28 @@ class Commit(object):
|
|||||||
self.committer_tz = committer_tz
|
self.committer_tz = committer_tz
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
self.author_name, self.author_email = email.utils.parseaddr(
|
self.author_name, self.author_email = \
|
||||||
self.author
|
email.utils.parseaddr(self.author)
|
||||||
)
|
|
||||||
|
|
||||||
self.committer_name, self.committer_email = email.utils.parseaddr(
|
self.committer_name, self.committer_email = \
|
||||||
self.committer
|
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.author_date = Date(self.author_epoch, self.author_tz)
|
||||||
self.committer_date = Date(self.committer_epoch, self.committer_tz)
|
self.committer_date = Date(self.committer_epoch, self.committer_tz)
|
||||||
|
|
||||||
|
|
||||||
# Only get this lazily when we need it; most of the time it's not
|
# Only get this lazily when we need it; most of the time it's not
|
||||||
# required by the caller.
|
# required by the caller.
|
||||||
self._diff = None
|
self._diff = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<C %s p:%s a:%s s:%r>" % (
|
return '<C %s p:%s a:%s s:%r>' % (
|
||||||
self.id[:7],
|
self.id[:7],
|
||||||
",".join(p[:7] for p in self.parents),
|
','.join(p[:7] for p in self.parents),
|
||||||
self.author_email,
|
self.author_email,
|
||||||
self.subject[:20],
|
self.subject[:20])
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diff(self):
|
def diff(self):
|
||||||
@ -413,68 +411,57 @@ class Commit(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_str(repo, buf):
|
def from_str(repo, buf):
|
||||||
"""Parses git rev-list output, returns a commit object."""
|
"""Parses git rev-list output, returns a commit object."""
|
||||||
if "\n\n" in buf:
|
if '\n\n' in buf:
|
||||||
# Header, commit message
|
# Header, commit message
|
||||||
header, raw_message = buf.split("\n\n", 1)
|
header, raw_message = buf.split('\n\n', 1)
|
||||||
else:
|
else:
|
||||||
# Header only, no commit message
|
# Header only, no commit message
|
||||||
header, raw_message = buf.rstrip(), " "
|
header, raw_message = buf.rstrip(), ' '
|
||||||
|
|
||||||
header_lines = header.split("\n")
|
header_lines = header.split('\n')
|
||||||
commit_id = header_lines.pop(0)
|
commit_id = header_lines.pop(0)
|
||||||
|
|
||||||
header_dict = defaultdict(list)
|
header_dict = defaultdict(list)
|
||||||
for line in header_lines:
|
for line in header_lines:
|
||||||
k, v = line.split(" ", 1)
|
k, v = line.split(' ', 1)
|
||||||
header_dict[k].append(v)
|
header_dict[k].append(v)
|
||||||
|
|
||||||
tree = header_dict["tree"][0]
|
tree = header_dict['tree'][0]
|
||||||
parents = set(header_dict["parent"])
|
parents = set(header_dict['parent'])
|
||||||
|
author, author_epoch, author_tz = \
|
||||||
authorhdr = header_dict["author"][0]
|
header_dict['author'][0].rsplit(' ', 2)
|
||||||
author, author_epoch, author_tz = authorhdr.rsplit(" ", 2)
|
committer, committer_epoch, committer_tz = \
|
||||||
|
header_dict['committer'][0].rsplit(' ', 2)
|
||||||
committerhdr = header_dict["committer"][0]
|
|
||||||
committer, committer_epoch, committer_tz = committerhdr.rsplit(" ", 2)
|
|
||||||
|
|
||||||
# Remove the first four spaces from the message's lines.
|
# Remove the first four spaces from the message's lines.
|
||||||
message = ""
|
message = ''
|
||||||
for line in raw_message.split("\n"):
|
for line in raw_message.split('\n'):
|
||||||
message += line[4:] + "\n"
|
message += line[4:] + '\n'
|
||||||
|
|
||||||
return Commit(
|
|
||||||
repo,
|
|
||||||
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:
|
class Date:
|
||||||
"""Handy representation for a datetime from git."""
|
"""Handy representation for a datetime from git."""
|
||||||
|
|
||||||
def __init__(self, epoch, tz):
|
def __init__(self, epoch, tz):
|
||||||
self.epoch = int(epoch)
|
self.epoch = int(epoch)
|
||||||
self.tz = tz
|
self.tz = tz
|
||||||
self.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
self.utc = datetime.datetime.utcfromtimestamp(self.epoch)
|
||||||
|
|
||||||
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
self.tz_sec_offset_min = int(tz[1:3]) * 60 + int(tz[4:])
|
||||||
if tz[0] == "-":
|
if tz[0] == '-':
|
||||||
self.tz_sec_offset_min = -self.tz_sec_offset_min
|
self.tz_sec_offset_min = -self.tz_sec_offset_min
|
||||||
|
|
||||||
self.local = self.utc + datetime.timedelta(
|
self.local = self.utc + datetime.timedelta(
|
||||||
minutes=self.tz_sec_offset_min
|
minutes = self.tz_sec_offset_min)
|
||||||
)
|
|
||||||
|
|
||||||
self.str = self.utc.strftime("%a, %d %b %Y %H:%M:%S +0000 ")
|
self.str = self.utc.strftime('%a, %d %b %Y %H:%M:%S +0000 ')
|
||||||
self.str += "(%s %s)" % (self.local.strftime("%H:%M"), self.tz)
|
self.str += '(%s %s)' % (self.local.strftime('%H:%M'), self.tz)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.str
|
return self.str
|
||||||
@ -482,7 +469,6 @@ class Date:
|
|||||||
|
|
||||||
class Diff:
|
class Diff:
|
||||||
"""A diff between two trees."""
|
"""A diff between two trees."""
|
||||||
|
|
||||||
def __init__(self, ref, changes, body):
|
def __init__(self, ref, changes, body):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
@ -502,40 +488,37 @@ class Diff:
|
|||||||
ref_id = next(lines)
|
ref_id = next(lines)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
# No diff; this can happen in merges without conflicts.
|
# No diff; this can happen in merges without conflicts.
|
||||||
return Diff(None, [], "")
|
return Diff(None, [], '')
|
||||||
|
|
||||||
# First, --numstat information.
|
# First, --numstat information.
|
||||||
changes = []
|
changes = []
|
||||||
l = next(lines)
|
l = next(lines)
|
||||||
while l != "\n":
|
while l != '\n':
|
||||||
l = l.rstrip("\n")
|
l = l.rstrip('\n')
|
||||||
added, deleted, fname = l.split("\t", 2)
|
added, deleted, fname = l.split('\t', 2)
|
||||||
added = added.replace("-", "0")
|
added = added.replace('-', '0')
|
||||||
deleted = deleted.replace("-", "0")
|
deleted = deleted.replace('-', '0')
|
||||||
fname = smstr(unquote(fname))
|
fname = smstr(unquote(fname))
|
||||||
changes.append((int(added), int(deleted), fname))
|
changes.append((int(added), int(deleted), fname))
|
||||||
l = next(lines)
|
l = next(lines)
|
||||||
|
|
||||||
# And now the diff body. We just store as-is, we don't really care for
|
# And now the diff body. We just store as-is, we don't really care for
|
||||||
# the contents.
|
# the contents.
|
||||||
body = "".join(lines)
|
body = ''.join(lines)
|
||||||
|
|
||||||
return Diff(ref_id, changes, body)
|
return Diff(ref_id, changes, body)
|
||||||
|
|
||||||
|
|
||||||
class Tree:
|
class Tree:
|
||||||
"""A git tree."""
|
""" A git tree."""
|
||||||
|
|
||||||
def __init__(self, repo: Repo, ref: str):
|
def __init__(self, repo, ref):
|
||||||
self.repo = repo
|
self.repo = repo
|
||||||
self.ref = ref
|
self.ref = ref
|
||||||
|
|
||||||
@functools.lru_cache
|
def ls(self, path, recursive = False):
|
||||||
def ls(
|
|
||||||
self, path, recursive=False
|
|
||||||
) -> Iterable[Tuple[str, smstr, Optional[int]]]:
|
|
||||||
"""Generates (type, name, size) for each file in path."""
|
"""Generates (type, name, size) for each file in path."""
|
||||||
cmd = self.repo.cmd("ls-tree")
|
cmd = self.repo.cmd('ls-tree')
|
||||||
cmd.long = None
|
cmd.long = None
|
||||||
if recursive:
|
if recursive:
|
||||||
cmd.r = None
|
cmd.r = None
|
||||||
@ -547,37 +530,34 @@ class Tree:
|
|||||||
else:
|
else:
|
||||||
cmd.arg(path)
|
cmd.arg(path)
|
||||||
|
|
||||||
files = []
|
|
||||||
for l in cmd.run():
|
for l in cmd.run():
|
||||||
_mode, otype, _oid, size, name = l.split(None, 4)
|
_mode, otype, _oid, size, name = l.split(None, 4)
|
||||||
if size == "-":
|
if size == '-':
|
||||||
size = None
|
size = None
|
||||||
else:
|
else:
|
||||||
size = int(size)
|
size = int(size)
|
||||||
|
|
||||||
# Remove the quoting (if any); will always give us a str.
|
# Remove the quoting (if any); will always give us a str.
|
||||||
name = unquote(name.strip("\n"))
|
name = unquote(name.strip('\n'))
|
||||||
|
|
||||||
# Strip the leading path, the caller knows it and it's often
|
# Strip the leading path, the caller knows it and it's often
|
||||||
# easier to work with this way.
|
# easier to work with this way.
|
||||||
name = name[len(path) :]
|
name = name[len(path):]
|
||||||
|
|
||||||
# We use a smart string for the name, as it's often tricky to
|
# We use a smart string for the name, as it's often tricky to
|
||||||
# manipulate otherwise.
|
# manipulate otherwise.
|
||||||
files.append((otype, smstr(name), size))
|
yield otype, smstr(name), size
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
class Blob:
|
class Blob:
|
||||||
"""A git blob."""
|
"""A git blob."""
|
||||||
|
|
||||||
def __init__(self, raw_content: bytes):
|
def __init__(self, raw_content):
|
||||||
self.raw_content = raw_content
|
self.raw_content = raw_content
|
||||||
self._utf8_content = None
|
self._utf8_content = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def utf8_content(self):
|
def utf8_content(self):
|
||||||
if not self._utf8_content:
|
if not self._utf8_content:
|
||||||
self._utf8_content = self.raw_content.decode("utf8", "replace")
|
self._utf8_content = self.raw_content.decode('utf8', 'replace')
|
||||||
return self._utf8_content
|
return self._utf8_content
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
[tool.black]
|
|
||||||
line-length = 79
|
|
||||||
include = "(git-arr|git.py|utils.py)$"
|
|
||||||
@ -1,53 +1,15 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* git-arr style sheet
|
* 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 {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
padding: 0 1em 1em 1em;
|
padding: 0 1em 1em 1em;
|
||||||
color: var(--text-fg);
|
|
||||||
background: var(--body-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
background: var(--h1-bg);
|
background: #ddd;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,29 +21,26 @@ h2, h3 {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
background-color: var(--hr-bg);
|
background-color: #e3e3e3;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* By default, use implied links, more discrete for increased readability. */
|
/* By default, use implied links, more discrete for increased readability. */
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-fg);
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--a-fg);
|
text-decoration: underline;
|
||||||
|
color: #800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Explicit links */
|
/* Explicit links */
|
||||||
a.explicit {
|
a.explicit {
|
||||||
color: var(--a-explicit-fg);
|
color: #038;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.explicit:hover, a.explicit:active {
|
a.explicit:hover, a.explicit:active {
|
||||||
color: var(--a-fg);
|
color: #880000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -89,27 +48,22 @@ a.explicit:hover, a.explicit:active {
|
|||||||
table.nice {
|
table.nice {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td {
|
table.nice td {
|
||||||
padding: 0.15em 0.5em;
|
padding: 0.15em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td.links {
|
table.nice td.links {
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td.main {
|
table.nice td.main {
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice tr:hover {
|
table.nice tr:hover {
|
||||||
background: var(--table-hover-bg);
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Table for commits. */
|
/* Table for commits. */
|
||||||
table.commits td.date {
|
table.commits td.date {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
@ -117,134 +71,106 @@ table.commits td.date {
|
|||||||
min-width: 32em;
|
min-width: 32em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.commits td.author {
|
table.commits td.author {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Table for commit information. */
|
/* Table for commit information. */
|
||||||
table.commit-info tr:hover {
|
table.commit-info tr:hover {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.commit-info td {
|
table.commit-info td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.commit-info span.date, span.email {
|
table.commit-info span.date, span.email {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Reference annotations. */
|
/* Reference annotations. */
|
||||||
span.refs {
|
span.refs {
|
||||||
margin: 0px 0.5em;
|
margin: 0px 0.5em;
|
||||||
padding: 0px 0.25em;
|
padding: 0px 0.25em;
|
||||||
border: solid 1px var(--text-lowcontrast-fg);
|
border: solid 1px gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.head {
|
span.head {
|
||||||
background-color: var(--head-bg);
|
background-color: #88ff88;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.tag {
|
span.tag {
|
||||||
background-color: var(--tag-bg);
|
background-color: #ffff88;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Projects table */
|
/* Projects table */
|
||||||
table.projects td.name a {
|
table.projects td.name a {
|
||||||
color: var(--a-explicit-fg);
|
color: #038;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Age of an object.
|
/* Age of an object.
|
||||||
* Note this is hidden by default as we rely on javascript to show it. */
|
* Note this is hidden by default as we rely on javascript to show it. */
|
||||||
span.age {
|
span.age {
|
||||||
display: none;
|
display: none;
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.age-band0 {
|
span.age-band0 {
|
||||||
color: var(--age-fg0);
|
color: darkgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.age-band1 {
|
span.age-band1 {
|
||||||
color: var(--age-fg1);
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.age-band2 {
|
span.age-band2 {
|
||||||
color: var(--age-fg2);
|
color: seagreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Toggable titles */
|
/* Toggable titles */
|
||||||
div.toggable-title {
|
div.toggable-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.3em;
|
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. */
|
/* Commit message and diff. */
|
||||||
pre.commit-message {
|
pre.commit-message {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
padding: 0.2em 0.5em;
|
padding: 0.2em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre.diff-body {
|
pre.diff-body {
|
||||||
/* Note this is only used as a fallback if pygments is not available. */
|
/* Note this is only used as a fallback if pygments is not available. */
|
||||||
}
|
}
|
||||||
|
|
||||||
table.changed-files {
|
table.changed-files {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.changed-files span.lines-added {
|
table.changed-files span.lines-added {
|
||||||
color: var(--diff-added-fg);
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.changed-files span.lines-deleted {
|
table.changed-files span.lines-deleted {
|
||||||
color: var(--diff-deleted-fg);
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Pagination. */
|
/* Pagination. */
|
||||||
div.paginate {
|
div.paginate {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.paginate span.inactive {
|
div.paginate span.inactive {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Directory listing. */
|
/* Directory listing. */
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
table.ls td.name {
|
table.ls td.name {
|
||||||
min-width: 20em;
|
min-width: 20em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.ls {
|
table.ls {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.ls tr.blob td.size {
|
table.ls tr.blob td.size {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Blob. */
|
/* Blob. */
|
||||||
pre.blob-body {
|
pre.blob-body {
|
||||||
/* Note this is only used as a fallback if pygments is not available. */
|
/* Note this is only used as a fallback if pygments is not available. */
|
||||||
@ -258,79 +184,60 @@ table.blob-binary pre {
|
|||||||
table.blob-binary .offset {
|
table.blob-binary .offset {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
color: var(--text-lowcontrast-fg);
|
color: darkgray;
|
||||||
border-right: 1px solid var(--text-lowcontrast-fg);
|
border-right: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.blob-binary tr.etc {
|
table.blob-binary tr.etc {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Pygments overrides. */
|
/* 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 {
|
div.linenodiv {
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
|
font-size: larger; /* must match div.source_code */
|
||||||
}
|
}
|
||||||
|
|
||||||
div.linenodiv a {
|
div.linenodiv a {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
|
}
|
||||||
|
div.source_code {
|
||||||
|
background: inherit;
|
||||||
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Repository information table. */
|
/* Repository information table. */
|
||||||
table.repo_info tr:hover {
|
table.repo_info tr:hover {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.repo_info td.category {
|
table.repo_info td.category {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
/* So we can copy-paste rows and preserve spaces, useful for the row:
|
/* So we can copy-paste rows and preserve spaces, useful for the row:
|
||||||
* git clone | url */
|
* git clone | url
|
||||||
|
*/
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.repo_info td {
|
table.repo_info td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.ctrlchr {
|
span.ctrlchr {
|
||||||
color: var(--text-lowcontrast-fg);
|
color: gray;
|
||||||
padding: 0 0.2ex 0 0.1ex;
|
padding: 0 0.2ex 0 0.1ex;
|
||||||
margin: 0 0.2ex 0 0.1ex;
|
margin: 0 0.2ex 0 0.1ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Markdown overrides
|
* Markdown overrides
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Colored links (same as explicit links above) */
|
/* Colored links (same as explicit links above) */
|
||||||
div.markdown a {
|
div.markdown a {
|
||||||
color: var(--a-explicit-fg);
|
color: #038;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.markdown a:hover, div.markdown a:active {
|
div.markdown a:hover, div.markdown a:active {
|
||||||
color: var(--a-fg);
|
color: #880000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Restrict max width for readability */
|
/* Restrict max width for readability */
|
||||||
div.markdown {
|
div.markdown {
|
||||||
max-width: 55em;
|
max-width: 55em;
|
||||||
|
|||||||
@ -1,37 +1,30 @@
|
|||||||
|
|
||||||
/* CSS for syntax highlighting.
|
/* CSS for syntax highlighting.
|
||||||
* Generated by pygments (what we use for syntax highlighting).
|
* Generated by pygments (what we use for syntax highlighting):
|
||||||
*
|
*
|
||||||
* Light mode: pygmentize -S default -f html -a .source_code
|
* $ 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 .hll { background-color: #ffffcc }
|
||||||
.source_code { background: #f8f8f8; }
|
.source_code { background: #f8f8f8; }
|
||||||
.source_code .c { color: #3D7B7B; font-style: italic } /* Comment */
|
.source_code .c { color: #408080; font-style: italic } /* Comment */
|
||||||
.source_code .err { border: 1px solid #FF0000 } /* Error */
|
.source_code .err { border: 1px solid #FF0000 } /* Error */
|
||||||
.source_code .k { color: #008000; font-weight: bold } /* Keyword */
|
.source_code .k { color: #008000; font-weight: bold } /* Keyword */
|
||||||
.source_code .o { color: #666666 } /* Operator */
|
.source_code .o { color: #666666 } /* Operator */
|
||||||
.source_code .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
.source_code .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||||
.source_code .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
.source_code .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||||
.source_code .cp { color: #9C6500 } /* Comment.Preproc */
|
.source_code .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||||
.source_code .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
.source_code .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||||
.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 .gd { color: #A00000 } /* Generic.Deleted */
|
||||||
.source_code .ge { font-style: italic } /* Generic.Emph */
|
.source_code .ge { font-style: italic } /* Generic.Emph */
|
||||||
.source_code .gr { color: #E40000 } /* Generic.Error */
|
.source_code .gr { color: #FF0000 } /* Generic.Error */
|
||||||
.source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
.source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||||
.source_code .gi { color: #008400 } /* Generic.Inserted */
|
.source_code .gi { color: #00A000 } /* Generic.Inserted */
|
||||||
.source_code .go { color: #717171 } /* Generic.Output */
|
.source_code .go { color: #808080 } /* Generic.Output */
|
||||||
.source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
.source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||||
.source_code .gs { font-weight: bold } /* Generic.Strong */
|
.source_code .gs { font-weight: bold } /* Generic.Strong */
|
||||||
.source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
.source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||||
.source_code .gt { color: #0044DD } /* Generic.Traceback */
|
.source_code .gt { color: #0040D0 } /* Generic.Traceback */
|
||||||
.source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
.source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||||
.source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
.source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||||
.source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
.source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||||
@ -40,139 +33,38 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
|
|||||||
.source_code .kt { color: #B00040 } /* Keyword.Type */
|
.source_code .kt { color: #B00040 } /* Keyword.Type */
|
||||||
.source_code .m { color: #666666 } /* Literal.Number */
|
.source_code .m { color: #666666 } /* Literal.Number */
|
||||||
.source_code .s { color: #BA2121 } /* Literal.String */
|
.source_code .s { color: #BA2121 } /* Literal.String */
|
||||||
.source_code .na { color: #687822 } /* Name.Attribute */
|
.source_code .na { color: #7D9029 } /* Name.Attribute */
|
||||||
.source_code .nb { color: #008000 } /* Name.Builtin */
|
.source_code .nb { color: #008000 } /* Name.Builtin */
|
||||||
.source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
.source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||||
.source_code .no { color: #880000 } /* Name.Constant */
|
.source_code .no { color: #880000 } /* Name.Constant */
|
||||||
.source_code .nd { color: #AA22FF } /* Name.Decorator */
|
.source_code .nd { color: #AA22FF } /* Name.Decorator */
|
||||||
.source_code .ni { color: #717171; font-weight: bold } /* Name.Entity */
|
.source_code .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||||
.source_code .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
.source_code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||||
.source_code .nf { color: #0000FF } /* Name.Function */
|
.source_code .nf { color: #0000FF } /* Name.Function */
|
||||||
.source_code .nl { color: #767600 } /* Name.Label */
|
.source_code .nl { color: #A0A000 } /* Name.Label */
|
||||||
.source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
.source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||||
.source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
.source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||||
.source_code .nv { color: #19177C } /* Name.Variable */
|
.source_code .nv { color: #19177C } /* Name.Variable */
|
||||||
.source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
.source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||||
.source_code .w { color: #bbbbbb } /* Text.Whitespace */
|
.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 .mf { color: #666666 } /* Literal.Number.Float */
|
||||||
.source_code .mh { color: #666666 } /* Literal.Number.Hex */
|
.source_code .mh { color: #666666 } /* Literal.Number.Hex */
|
||||||
.source_code .mi { color: #666666 } /* Literal.Number.Integer */
|
.source_code .mi { color: #666666 } /* Literal.Number.Integer */
|
||||||
.source_code .mo { color: #666666 } /* Literal.Number.Oct */
|
.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 .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||||
.source_code .sc { color: #BA2121 } /* Literal.String.Char */
|
.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 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||||
.source_code .s2 { color: #BA2121 } /* Literal.String.Double */
|
.source_code .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||||
.source_code .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
|
.source_code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||||
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||||
.source_code .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
.source_code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||||
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
||||||
.source_code .sr { color: #A45A77 } /* Literal.String.Regex */
|
.source_code .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||||
.source_code .s1 { color: #BA2121 } /* Literal.String.Single */
|
.source_code .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||||
.source_code .ss { color: #19177C } /* Literal.String.Symbol */
|
.source_code .ss { color: #19177C } /* Literal.String.Symbol */
|
||||||
.source_code .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
.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 .vc { color: #19177C } /* Name.Variable.Class */
|
||||||
.source_code .vg { color: #19177C } /* Name.Variable.Global */
|
.source_code .vg { color: #19177C } /* Name.Variable.Global */
|
||||||
.source_code .vi { color: #19177C } /* Name.Variable.Instance */
|
.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 */
|
.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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
135
utils.py
135
utils.py
@ -5,44 +5,30 @@ These are mostly used in templates, for presentation purposes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pygments # type: ignore
|
import pygments
|
||||||
from pygments import highlight # type: ignore
|
from pygments import highlight
|
||||||
from pygments import lexers # type: ignore
|
from pygments import lexers
|
||||||
from pygments.formatters import HtmlFormatter # type: ignore
|
from pygments.formatters import HtmlFormatter
|
||||||
|
|
||||||
_html_formatter = HtmlFormatter(
|
|
||||||
encoding="utf-8",
|
|
||||||
cssclass="source_code",
|
|
||||||
linenos="table",
|
|
||||||
anchorlinenos=True,
|
|
||||||
lineanchors="line",
|
|
||||||
)
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pygments = None
|
pygments = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import markdown # type: ignore
|
import markdown
|
||||||
import markdown.treeprocessors # type: ignore
|
import markdown.treeprocessors
|
||||||
except ImportError:
|
except ImportError:
|
||||||
markdown = None
|
markdown = None
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import functools
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import string
|
import string
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
import git
|
def shorten(s, width = 60):
|
||||||
|
|
||||||
|
|
||||||
def shorten(s: str, width=60):
|
|
||||||
if len(s) < 60:
|
if len(s) < 60:
|
||||||
return s
|
return s
|
||||||
return s[:57] + "..."
|
return s[:57] + "..."
|
||||||
|
|
||||||
|
def can_colorize(s):
|
||||||
@functools.lru_cache
|
|
||||||
def can_colorize(s: str):
|
|
||||||
"""True if we can colorize the string, False otherwise."""
|
"""True if we can colorize the string, False otherwise."""
|
||||||
if pygments is None:
|
if pygments is None:
|
||||||
return False
|
return False
|
||||||
@ -55,7 +41,7 @@ def can_colorize(s: str):
|
|||||||
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
||||||
start = 0
|
start = 0
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
pos = s.find("\n", start)
|
pos = s.find('\n', start)
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -65,8 +51,7 @@ def can_colorize(s: str):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def can_markdown(repo, fname):
|
||||||
def can_markdown(repo: git.Repo, fname: str):
|
|
||||||
"""True if we can process file through markdown, False otherwise."""
|
"""True if we can process file through markdown, False otherwise."""
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
return False
|
return False
|
||||||
@ -76,73 +61,73 @@ def can_markdown(repo: git.Repo, fname: str):
|
|||||||
|
|
||||||
return fname.endswith(".md")
|
return fname.endswith(".md")
|
||||||
|
|
||||||
|
|
||||||
def can_embed_image(repo, fname):
|
def can_embed_image(repo, fname):
|
||||||
"""True if we can embed image file in HTML, False otherwise."""
|
"""True if we can embed image file in HTML, False otherwise."""
|
||||||
if not repo.info.embed_images:
|
if not repo.info.embed_images:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return ("." in fname) and (
|
return (('.' in fname) and
|
||||||
fname.split(".")[-1].lower() in ["jpg", "jpeg", "png", "gif"]
|
(fname.split('.')[-1].lower() in [ 'jpg', 'jpeg', 'png', 'gif' ]))
|
||||||
)
|
|
||||||
|
|
||||||
|
def colorize_diff(s):
|
||||||
@functools.lru_cache
|
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
||||||
def colorize_diff(s: str) -> str:
|
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||||
lexer = lexers.DiffLexer(encoding="utf-8")
|
cssclass = 'source_code')
|
||||||
formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
|
|
||||||
|
|
||||||
return highlight(s, lexer, formatter)
|
return highlight(s, lexer, formatter)
|
||||||
|
|
||||||
|
def colorize_blob(fname, s):
|
||||||
@functools.lru_cache
|
|
||||||
def colorize_blob(fname, s: str) -> str:
|
|
||||||
try:
|
try:
|
||||||
lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8")
|
lexer = lexers.guess_lexer_for_filename(fname, s, encoding = 'utf-8')
|
||||||
except lexers.ClassNotFound:
|
except lexers.ClassNotFound:
|
||||||
# Only try to guess lexers if the file starts with a shebang,
|
# Only try to guess lexers if the file starts with a shebang,
|
||||||
# otherwise it's likely a text file and guess_lexer() is prone to
|
# otherwise it's likely a text file and guess_lexer() is prone to
|
||||||
# make mistakes with those.
|
# make mistakes with those.
|
||||||
lexer = lexers.TextLexer(encoding="utf-8")
|
lexer = lexers.TextLexer(encoding = 'utf-8')
|
||||||
if s.startswith("#!"):
|
if s.startswith('#!'):
|
||||||
try:
|
try:
|
||||||
lexer = lexers.guess_lexer(s[:80], encoding="utf-8")
|
lexer = lexers.guess_lexer(s[:80], encoding = 'utf-8')
|
||||||
except lexers.ClassNotFound:
|
except lexers.ClassNotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return highlight(s, lexer, _html_formatter)
|
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||||
|
cssclass = 'source_code',
|
||||||
|
linenos = 'table',
|
||||||
|
anchorlinenos = True,
|
||||||
|
lineanchors = 'line')
|
||||||
|
|
||||||
|
return highlight(s, lexer, formatter)
|
||||||
|
|
||||||
def embed_image_blob(fname: str, image_data: bytes) -> str:
|
def markdown_blob(s):
|
||||||
|
extensions = [
|
||||||
|
"markdown.extensions.fenced_code",
|
||||||
|
"markdown.extensions.tables",
|
||||||
|
RewriteLocalLinksExtension(),
|
||||||
|
]
|
||||||
|
return markdown.markdown(s, extensions = extensions)
|
||||||
|
|
||||||
|
def embed_image_blob(fname, image_data):
|
||||||
mimetype = mimetypes.guess_type(fname)[0]
|
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( \
|
||||||
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format(
|
mimetype, base64.b64encode(image_data))
|
||||||
mimetype, b64img
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def is_binary(s):
|
||||||
@functools.lru_cache
|
|
||||||
def is_binary(b: bytes):
|
|
||||||
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
||||||
return b"\0" in b[:8192]
|
return '\0' in s[:8192]
|
||||||
|
|
||||||
|
def hexdump(s):
|
||||||
@functools.lru_cache
|
graph = string.ascii_letters + string.digits + string.punctuation + ' '
|
||||||
def hexdump(s: bytes):
|
|
||||||
graph = string.ascii_letters + string.digits + string.punctuation + " "
|
|
||||||
b = s.decode("latin1")
|
|
||||||
offset = 0
|
offset = 0
|
||||||
while b:
|
while s:
|
||||||
t = b[:16]
|
t = s[:16]
|
||||||
hexvals = ["%.2x" % ord(c) for c in t]
|
hexvals = ['%.2x' % ord(c) for c in t]
|
||||||
text = "".join(c if c in graph else "." for c in t)
|
text = ''.join(c if c in graph else '.' for c in t)
|
||||||
yield offset, " ".join(hexvals[:8]), " ".join(hexvals[8:]), text
|
yield offset, ' '.join(hexvals[:8]), ' '.join(hexvals[8:]), text
|
||||||
offset += 16
|
offset += 16
|
||||||
b = b[16:]
|
s = s[16:]
|
||||||
|
|
||||||
|
|
||||||
if markdown:
|
if markdown:
|
||||||
|
|
||||||
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
||||||
"""Rewrites relative links to files, to match git-arr's links.
|
"""Rewrites relative links to files, to match git-arr's links.
|
||||||
|
|
||||||
@ -152,7 +137,6 @@ if markdown:
|
|||||||
Note that we're already assuming a degree of sanity in the HTML, so we
|
Note that we're already assuming a degree of sanity in the HTML, so we
|
||||||
don't re-check that the path is reasonable.
|
don't re-check that the path is reasonable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self, root):
|
def run(self, root):
|
||||||
for child in root:
|
for child in root:
|
||||||
if child.tag == "a":
|
if child.tag == "a":
|
||||||
@ -173,24 +157,9 @@ if markdown:
|
|||||||
new_target = os.path.join(head, "f=" + tail + ".html")
|
new_target = os.path.join(head, "f=" + tail + ".html")
|
||||||
tag.set("href", new_target)
|
tag.set("href", new_target)
|
||||||
|
|
||||||
|
|
||||||
class RewriteLocalLinksExtension(markdown.Extension):
|
class RewriteLocalLinksExtension(markdown.Extension):
|
||||||
def extendMarkdown(self, md):
|
def extendMarkdown(self, md, md_globals):
|
||||||
md.treeprocessors.register(
|
md.treeprocessors.add(
|
||||||
RewriteLocalLinks(), "RewriteLocalLinks", 1000
|
"RewriteLocalLinks", RewriteLocalLinks(), "_end")
|
||||||
)
|
|
||||||
|
|
||||||
_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")
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||||
|
|
||||||
<title>git » {{repo.name}} »
|
<title>git » {{repo.name}} »
|
||||||
{{branch}} » {{dirname.raw}}{{fname.raw}}</title>
|
{{branch}} » {{dirname.unicode}}{{fname.unicode}}</title>
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
@ -33,7 +33,7 @@
|
|||||||
% if not c.raw:
|
% if not c.raw:
|
||||||
% continue
|
% continue
|
||||||
% end
|
% end
|
||||||
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
||||||
% base += c + '/'
|
% base += c + '/'
|
||||||
% end
|
% end
|
||||||
<a href="">{{!fname.html}}</a>
|
<a href="">{{!fname.html}}</a>
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<td>empty — 0 bytes</td>
|
<td>empty — 0 bytes</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
% elif can_embed_image(repo, fname.raw):
|
% elif can_embed_image(repo, fname.unicode):
|
||||||
{{!embed_image_blob(fname.raw, blob.raw_content)}}
|
{{!embed_image_blob(fname.raw, blob.raw_content)}}
|
||||||
% elif is_binary(blob.raw_content):
|
% elif is_binary(blob.raw_content):
|
||||||
<table class="nice blob-binary">
|
<table class="nice blob-binary">
|
||||||
@ -72,14 +72,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
% end
|
% end
|
||||||
</table>
|
</table>
|
||||||
% elif can_markdown(repo, fname.raw):
|
% elif can_markdown(repo, fname.unicode):
|
||||||
<div class="markdown">
|
<div class="markdown">
|
||||||
{{!markdown_blob(blob.utf8_content)}}
|
{{!markdown_blob(blob.utf8_content)}}
|
||||||
</div>
|
</div>
|
||||||
% elif can_colorize(blob.utf8_content):
|
% elif can_colorize(blob.utf8_content):
|
||||||
<div class="colorized-src">
|
{{!colorize_blob(fname.unicode, blob.utf8_content)}}
|
||||||
{{!colorize_blob(fname.raw, blob.utf8_content)}}
|
|
||||||
</div>
|
|
||||||
% else:
|
% else:
|
||||||
<pre class="blob-body">
|
<pre class="blob-body">
|
||||||
{{blob.utf8_content}}
|
{{blob.utf8_content}}
|
||||||
|
|||||||
@ -56,9 +56,7 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
% if can_colorize(c.diff.body):
|
% if can_colorize(c.diff.body):
|
||||||
<div class="colorized-src">
|
|
||||||
{{!colorize_diff(c.diff.body)}}
|
{{!colorize_diff(c.diff.body)}}
|
||||||
</div>
|
|
||||||
% else:
|
% else:
|
||||||
<pre class="diff-body">
|
<pre class="diff-body">
|
||||||
{{c.diff.body}}
|
{{c.diff.body}}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<table class="nice toggable ls" id="ls">
|
<table class="nice toggable ls" id="ls">
|
||||||
% key_func = lambda x: (x[0] != 'tree', x[1].raw)
|
% key_func = lambda (t, n, s): (t != 'tree', n.raw)
|
||||||
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
||||||
<tr class="{{type}}">
|
<tr class="{{type}}">
|
||||||
% if type == "blob":
|
% if type == "blob":
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||||
|
|
||||||
<title>git » {{repo.name}} »
|
<title>git » {{repo.name}} »
|
||||||
{{branch}} » {{dirname.raw if dirname.raw else '/'}}</title>
|
{{branch}} » {{dirname.unicode if dirname.unicode else '/'}}</title>
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="{{relroot}}../../../../../static/git-arr.css"/>
|
href="{{relroot}}../../../../../static/git-arr.css"/>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
% if not c.raw:
|
% if not c.raw:
|
||||||
% continue
|
% continue
|
||||||
% end
|
% end
|
||||||
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
||||||
% base += c + '/'
|
% base += c + '/'
|
||||||
% end
|
% end
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user