Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff4ff6757 | ||
|
|
6ea59bad51 | ||
|
|
4b1e1eb84c | ||
|
|
518188288e | ||
|
|
15547b2796 | ||
|
|
9f3df4899f | ||
|
|
bc1ee87dfe | ||
|
|
0d61bbf7f5 | ||
|
|
78f1b6def0 | ||
|
|
4cb2f59dd4 | ||
|
|
e2155f6b33 | ||
|
|
aee18d0edd | ||
|
|
20b99ee568 | ||
|
|
ad950208bf | ||
|
|
1183d6f817 | ||
|
|
cbb36e087c | ||
|
|
722d765973 | ||
|
|
5e75a1e7a1 | ||
|
|
e1349d418c | ||
|
|
5def4c9e01 | ||
|
|
891a944381 | ||
|
|
d7f0e4a265 | ||
|
|
56b0b34930 | ||
|
|
9b21bd6f19 | ||
|
|
c96d0dbea6 | ||
|
|
9c8a6d2408 | ||
|
|
53155e566a | ||
|
|
c648cfb593 | ||
|
|
cacf2ee2cc | ||
|
|
c4e6484bb0 | ||
|
|
88dd6fab76 | ||
|
|
84d628c690 | ||
|
|
5568fd50c2 | ||
|
|
89a637660f | ||
|
|
37e731fc2e | ||
|
|
e6099cf272 | ||
|
|
46640c68b9 | ||
|
|
c91beccdb0 | ||
|
|
6f3942ce38 | ||
|
|
09c2f33f5a | ||
|
|
58037e57c5 | ||
|
|
50c004f8a5 | ||
|
|
1d79988228 | ||
|
|
0ba89d75e6 | ||
|
|
6b83e32bc1 | ||
|
|
43f4132bf1 | ||
|
|
66afd72d6d | ||
|
|
bb9bad89d1 | ||
|
|
56fcfd0278 | ||
|
|
e930f9e4f7 | ||
|
|
93b161c23e | ||
|
|
7f2f67629f | ||
|
|
ac105c8383 | ||
|
|
bebc7fa3f0 | ||
|
|
9ef78aaffd | ||
|
|
d7604dab4d | ||
|
|
aaf2968538 | ||
|
|
420afd3206 | ||
|
|
605421f2d6 | ||
|
|
df00293a7c | ||
|
|
7898b2becd | ||
|
|
47d500715a | ||
|
|
eb7cadd64f | ||
|
|
48a00cb460 | ||
|
|
2f65291ef1 | ||
|
|
f6a75820e8 | ||
|
|
e49c69da2e | ||
|
|
6764bfcfd6 | ||
|
|
54026b7585 | ||
|
|
a42d7da6a4 | ||
|
|
21522f8a3a | ||
|
|
f62ca211eb | ||
|
|
d3bf98ea00 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
.*.swp
|
.*
|
||||||
|
!.gitignore
|
||||||
|
|||||||
56
README
56
README
@ -1,56 +0,0 @@
|
|||||||
|
|
||||||
git-arr - A git repository browser
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
git-arr is a git repository browser that can generate static HTML instead of
|
|
||||||
having to run dynamically.
|
|
||||||
|
|
||||||
It is smaller, with less features and a different set of tradeoffs than
|
|
||||||
other similar software, so if you're looking for a robust and featureful git
|
|
||||||
browser, please look at gitweb or cgit instead.
|
|
||||||
|
|
||||||
However, if you want to generate static HTML at the expense of features, then
|
|
||||||
it's probably going to be useful.
|
|
||||||
|
|
||||||
It's open source under the MIT licence, please see the LICENSE file for more
|
|
||||||
information.
|
|
||||||
|
|
||||||
|
|
||||||
Getting started
|
|
||||||
---------------
|
|
||||||
|
|
||||||
You will need Python, and the bottle.py framework (the package is usually
|
|
||||||
called python-bottle in most distributions).
|
|
||||||
|
|
||||||
If pygments is available, it will be used for syntax highlighting, otherwise
|
|
||||||
everything will work fine, just in black and white.
|
|
||||||
|
|
||||||
|
|
||||||
First, create a configuration file for your repositories. You can start by
|
|
||||||
copying sample.conf, which has the list of the available options.
|
|
||||||
|
|
||||||
Then, to generate the output to "/var/www/git-arr/" directory, run:
|
|
||||||
|
|
||||||
$ ./git-arr --config config.conf generate --output /var/www/git-arr/
|
|
||||||
|
|
||||||
That's it!
|
|
||||||
|
|
||||||
The first time you generate, depending on the size of your repositories, it
|
|
||||||
can take some time. Subsequent runs should take less time, as it is smart
|
|
||||||
enough to only generate what has changed.
|
|
||||||
|
|
||||||
|
|
||||||
You can also use git-arr dynamically, although it's not its intended mode of
|
|
||||||
use, by running:
|
|
||||||
|
|
||||||
$ ./git-arr --config config.conf serve
|
|
||||||
|
|
||||||
That can be useful when making changes to the software itself.
|
|
||||||
|
|
||||||
|
|
||||||
Contact
|
|
||||||
-------
|
|
||||||
|
|
||||||
If you want to report bugs, send patches, or have any questions or comments,
|
|
||||||
just let me know at albertito@blitiri.com.ar.
|
|
||||||
|
|
||||||
65
README.md
Normal file
65
README.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
# git-arr - A git repository browser
|
||||||
|
|
||||||
|
[git-arr] is a [git] repository browser that can generate static HTML.
|
||||||
|
|
||||||
|
It is smaller, with less features and a different set of tradeoffs than
|
||||||
|
other similar software, so if you're looking for a robust and featureful git
|
||||||
|
browser, please look at [gitweb] or [cgit] instead.
|
||||||
|
|
||||||
|
However, if you want to generate static HTML at the expense of features, then
|
||||||
|
it's probably going to be useful.
|
||||||
|
|
||||||
|
It's open source under the MIT licence, please see the `LICENSE` file for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
[git-arr]: https://blitiri.com.ar/p/git-arr/
|
||||||
|
[git]: https://git-scm.com/
|
||||||
|
[gitweb]: https://git-scm.com/docs/gitweb
|
||||||
|
[cgit]: https://git.zx2c4.com/cgit/about/
|
||||||
|
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
You will need [Python 3], and the [bottle.py] framework (the package is usually
|
||||||
|
called `python3-bottle` in most distributions).
|
||||||
|
|
||||||
|
If [pygments] is available, it will be used for syntax highlighting, otherwise
|
||||||
|
everything will work fine, just in black and white.
|
||||||
|
|
||||||
|
|
||||||
|
First, create a configuration file for your repositories. You can start by
|
||||||
|
copying `sample.conf`, which has the list of the available options.
|
||||||
|
|
||||||
|
Then, to generate the output to `/var/www/git-arr/` directory, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./git-arr --config config.conf generate --output /var/www/git-arr/
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it!
|
||||||
|
|
||||||
|
The first time you generate, depending on the size of your repositories, it
|
||||||
|
can take some time. Subsequent runs should take less time, as it is smart
|
||||||
|
enough to only generate what has changed.
|
||||||
|
|
||||||
|
You can also use git-arr dynamically, although it's not its intended mode of
|
||||||
|
use, by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
./git-arr --config config.conf serve
|
||||||
|
```
|
||||||
|
|
||||||
|
That can be useful when making changes to the software itself.
|
||||||
|
|
||||||
|
|
||||||
|
[Python 3]: https://www.python.org/
|
||||||
|
[bottle.py]: https://bottlepy.org/
|
||||||
|
[pygments]: https://pygments.org/
|
||||||
|
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you want to report bugs, send patches, or have any questions or comments,
|
||||||
|
just let me know at albertito@blitiri.com.ar.
|
||||||
|
|
||||||
13
TODO
13
TODO
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
In no particular order.
|
|
||||||
|
|
||||||
- Atom/RSS.
|
|
||||||
- Nicer diff:
|
|
||||||
- Better stat section, with nicer handling of filenames. We should switch to
|
|
||||||
--patch-with-raw and parse from that.
|
|
||||||
- Nicer output, don't use pygments but do our own.
|
|
||||||
- Anchors in diff sections so we can link to them.
|
|
||||||
- Short symlinks to commits, with configurable length.
|
|
||||||
- Handle symlinks properly.
|
|
||||||
- "X hours ago" via javascript (only if it's not too ugly).
|
|
||||||
|
|
||||||
469
git-arr
469
git-arr
@ -1,21 +1,17 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
git-arr: A git web html generator.
|
git-arr: A git web html generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import print_function
|
import configparser
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import math
|
import math
|
||||||
import optparse
|
import optparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
try:
|
import bottle # type: ignore
|
||||||
import configparser
|
|
||||||
except ImportError:
|
|
||||||
import ConfigParser as configparser
|
|
||||||
|
|
||||||
import bottle
|
|
||||||
|
|
||||||
import git
|
import git
|
||||||
import utils
|
import utils
|
||||||
@ -25,12 +21,13 @@ import utils
|
|||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
bottle.TEMPLATE_PATH.insert(
|
bottle.TEMPLATE_PATH.insert(
|
||||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
|
0, os.path.abspath(os.path.dirname(sys.argv[0])) + "/views/"
|
||||||
|
)
|
||||||
|
|
||||||
# The path to our static files.
|
# The path to our static files.
|
||||||
# Note this assumes they live next to the executable, and that is not a good
|
# Note this assumes they live next to the executable, and that is not a good
|
||||||
# assumption; but it's good enough for now.
|
# assumption; but it's good enough for now.
|
||||||
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
|
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + "/static/"
|
||||||
|
|
||||||
|
|
||||||
# The list of repositories is a global variable for convenience. It will be
|
# The list of repositories is a global variable for convenience. It will be
|
||||||
@ -45,78 +42,100 @@ def load_config(path):
|
|||||||
as configured.
|
as configured.
|
||||||
"""
|
"""
|
||||||
defaults = {
|
defaults = {
|
||||||
'tree': 'yes',
|
"tree": "yes",
|
||||||
'desc': '',
|
"rootdiff": "yes",
|
||||||
'recursive': 'no',
|
"desc": "",
|
||||||
'commits_in_summary': '10',
|
"recursive": "no",
|
||||||
'commits_per_page': '50',
|
"prefix": "",
|
||||||
'max_pages': '5',
|
"commits_in_summary": "10",
|
||||||
'web_url': '',
|
"commits_per_page": "50",
|
||||||
'web_url_file': 'web_url',
|
"max_pages": "250",
|
||||||
'git_url': '',
|
"web_url": "",
|
||||||
'git_url_file': 'cloneurl',
|
"web_url_file": "web_url",
|
||||||
|
"git_url": "",
|
||||||
|
"git_url_file": "cloneurl",
|
||||||
|
"embed_markdown": "yes",
|
||||||
|
"embed_images": "no",
|
||||||
|
"ignore": "",
|
||||||
|
"generate_patch": "yes",
|
||||||
}
|
}
|
||||||
|
|
||||||
config = configparser.SafeConfigParser(defaults)
|
config = configparser.ConfigParser(defaults)
|
||||||
config.read(path)
|
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"):
|
||||||
for path in os.listdir(config.get(s, 'path')):
|
root = config.get(s, "path")
|
||||||
fullpath = find_git_dir(config.get(s, 'path') + '/' + path)
|
prefix = config.get(s, "prefix")
|
||||||
|
|
||||||
|
for path in os.listdir(root):
|
||||||
|
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
|
||||||
|
|
||||||
if config.has_section(path):
|
section = prefix + path
|
||||||
|
if config.has_section(section):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config.add_section(path)
|
config.add_section(section)
|
||||||
for opt, value in config.items(s, raw = True):
|
for opt, value in config.items(s, raw=True):
|
||||||
config.set(path, opt, value)
|
config.set(section, opt, value)
|
||||||
|
|
||||||
config.set(path, 'path', fullpath)
|
config.set(section, "path", fullpath)
|
||||||
config.set(path, '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():
|
||||||
fullpath = find_git_dir(config.get(s, 'path'))
|
if config.get(s, "ignore") and re.search(config.get(s, "ignore"), s):
|
||||||
|
continue
|
||||||
|
|
||||||
|
fullpath = find_git_dir(config.get(s, "path"))
|
||||||
if not fullpath:
|
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")
|
||||||
r.info.generate_tree = config.getboolean(s, 'tree')
|
if r.info.max_pages <= 0:
|
||||||
|
r.info.max_pages = sys.maxsize
|
||||||
|
r.info.generate_tree = config.getboolean(s, "tree")
|
||||||
|
r.info.root_diff = config.getboolean(s, "rootdiff")
|
||||||
|
r.info.generate_patch = config.getboolean(s, "generate_patch")
|
||||||
|
|
||||||
r.info.web_url = config.get(s, 'web_url')
|
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_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.
|
||||||
|
|
||||||
@ -126,25 +145,26 @@ def find_git_dir(path):
|
|||||||
|
|
||||||
An empty string is returned if the given path is not a valid repository.
|
An empty string is returned if the given path is not a valid repository.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(p):
|
def check(p):
|
||||||
"""A dirty check for whether this is a git dir or not."""
|
"""A dirty check for whether this is a git dir or not."""
|
||||||
# Note silent stderr because we expect this to fail and don't want the
|
# Note silent stderr because we expect this to fail and don't want the
|
||||||
# noise; and also we strip the final \n from the output.
|
# noise; and also we strip the final \n from the output.
|
||||||
return git.run_git(p,
|
return git.run_git(
|
||||||
['rev-parse', '--git-dir'],
|
p, ["rev-parse", "--git-dir"], silent_stderr=True
|
||||||
silent_stderr = True).read()[:-1]
|
).read()[:-1]
|
||||||
|
|
||||||
for p in [ path, path + '/.git' ]:
|
for p in [path, path + "/.git"]:
|
||||||
if check(p):
|
if check(p):
|
||||||
return p
|
return p
|
||||||
|
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def repo_filter(unused_conf):
|
def repo_filter(unused_conf):
|
||||||
"""Bottle route filter for repos."""
|
"""Bottle route filter for repos."""
|
||||||
# TODO: consider allowing /, which is tricky.
|
# TODO: consider allowing /, which is tricky.
|
||||||
regexp = r'[\w\.~-]+'
|
regexp = r"[\w\.~-]+"
|
||||||
|
|
||||||
def to_python(s):
|
def to_python(s):
|
||||||
"""Return the corresponding Python object."""
|
"""Return the corresponding Python object."""
|
||||||
@ -158,24 +178,31 @@ 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,
|
||||||
'abort': bottle.abort,
|
"can_markdown": utils.can_markdown,
|
||||||
'smstr': git.smstr,
|
"markdown_blob": utils.markdown_blob,
|
||||||
|
"can_embed_image": utils.can_embed_image,
|
||||||
|
"embed_image_blob": utils.embed_image_blob,
|
||||||
|
"is_binary": utils.is_binary,
|
||||||
|
"hexdump": utils.hexdump,
|
||||||
|
"abort": bottle.abort,
|
||||||
|
"smstr": git.smstr,
|
||||||
}
|
}
|
||||||
|
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
@ -189,88 +216,131 @@ def with_utils(f):
|
|||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
@bottle.route('/')
|
|
||||||
@bottle.view('index')
|
@bottle.route("/")
|
||||||
|
@bottle.view("index")
|
||||||
@with_utils
|
@with_utils
|
||||||
def index():
|
def index():
|
||||||
return dict(repos = repos)
|
return dict(repos=repos)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/')
|
|
||||||
@bottle.view('summary')
|
@bottle.route("/r/<repo:repo>/")
|
||||||
|
@bottle.view("summary")
|
||||||
@with_utils
|
@with_utils
|
||||||
def summary(repo):
|
def summary(repo):
|
||||||
return dict(repo = repo)
|
return dict(repo=repo)
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname>/')
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname>/<offset:int>.html')
|
|
||||||
@bottle.view('branch')
|
|
||||||
@with_utils
|
|
||||||
def branch(repo, bname, offset = 0):
|
|
||||||
return dict(repo = repo.new_in_branch(bname), offset = offset)
|
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/c/<cid:re:[0-9a-z]{5,40}>/')
|
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>/")
|
||||||
@bottle.view('commit')
|
@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>/b/<bname>/t/')
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/<dirname:path>/')
|
@bottle.route("/r/<repo:repo>/c/<cid:re:[0-9a-f]{5,40}>.patch")
|
||||||
@bottle.view('tree')
|
@bottle.view(
|
||||||
|
"patch",
|
||||||
|
# Output is text/plain, don't do HTML escaping.
|
||||||
|
template_settings={"noescape": True},
|
||||||
|
)
|
||||||
|
def patch(repo, cid):
|
||||||
|
c = repo.commit(cid)
|
||||||
|
if not c:
|
||||||
|
bottle.abort(404, "Commit not found")
|
||||||
|
|
||||||
|
bottle.response.content_type = "text/plain; charset=utf8"
|
||||||
|
|
||||||
|
return dict(repo=repo, c=c)
|
||||||
|
|
||||||
|
|
||||||
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/f=<fname:path>.html")
|
||||||
|
@bottle.route(
|
||||||
|
"/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/f=<fname:path>.html"
|
||||||
|
)
|
||||||
|
@bottle.view("blob")
|
||||||
@with_utils
|
@with_utils
|
||||||
def tree(repo, bname, 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)
|
|
||||||
|
|
||||||
r = repo.new_in_branch(bname)
|
|
||||||
return dict(repo = r, tree = r.tree(), dirname = dirname)
|
|
||||||
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/f=<fname:path>.html')
|
|
||||||
@bottle.route('/r/<repo:repo>/b/<bname>/t/<dirname:path>/f=<fname:path>.html')
|
|
||||||
@bottle.view('blob')
|
|
||||||
@with_utils
|
|
||||||
def blob(repo, bname, fname, dirname = ''):
|
|
||||||
r = repo.new_in_branch(bname)
|
|
||||||
|
|
||||||
if dirname and not dirname.endswith('/'):
|
|
||||||
dirname = dirname + '/'
|
|
||||||
|
|
||||||
dirname = git.smstr.from_url(dirname)
|
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
|
||||||
|
|
||||||
content = r.blob(path)
|
# Handle backslash-escaped characters, which are not utf8.
|
||||||
|
# This matches the generated links from git.unquote().
|
||||||
|
path = path.encode("utf8").decode("unicode-escape").encode("latin1")
|
||||||
|
|
||||||
|
content = repo.blob(path, bname)
|
||||||
if content is None:
|
if content is None:
|
||||||
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
bottle.abort(404, "File %r not found in branch %s" % (path, bname))
|
||||||
|
|
||||||
return dict(repo = r, dirname = dirname, fname = fname, blob = content)
|
return dict(
|
||||||
|
repo=repo, branch=bname, dirname=dirname, fname=fname, blob=content
|
||||||
|
)
|
||||||
|
|
||||||
@bottle.route('/static/<path:path>')
|
|
||||||
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/")
|
||||||
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/t/<dirname:path>/")
|
||||||
|
@bottle.view("tree")
|
||||||
|
@with_utils
|
||||||
|
def tree(repo, bname, dirname=""):
|
||||||
|
if dirname and not dirname.endswith("/"):
|
||||||
|
dirname = dirname + "/"
|
||||||
|
|
||||||
|
dirname = git.smstr.from_url(dirname)
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
repo=repo, branch=bname, tree=repo.tree(bname), dirname=dirname
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/")
|
||||||
|
@bottle.route("/r/<repo:repo>/b/<bname:path>/<offset:int>.html")
|
||||||
|
@bottle.view("branch")
|
||||||
|
@with_utils
|
||||||
|
def branch(repo, bname, offset=0):
|
||||||
|
return dict(repo=repo, branch=bname, offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
@bottle.route("/static/<path:path>")
|
||||||
def static(path):
|
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 generate(output, skip_index = False):
|
|
||||||
|
def is_404(e):
|
||||||
|
"""True if e is an HTTPError with status 404, False otherwise."""
|
||||||
|
# We need this because older bottle.py versions put the status code in
|
||||||
|
# e.status as an integer, and newer versions make that a string, and using
|
||||||
|
# e.status_code for the code.
|
||||||
|
if isinstance(e.status, int):
|
||||||
|
return e.status == 404
|
||||||
|
else:
|
||||||
|
return e.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def generate(output: str, only=None):
|
||||||
"""Generate static html to the output directory."""
|
"""Generate static html to the output directory."""
|
||||||
def write_to(path, func_or_str, args = (), mtime = None):
|
|
||||||
path = output + '/' + path
|
def write_to(path: str, func_or_str, args=(), mtime=None):
|
||||||
|
path = output + "/" + path
|
||||||
dirname = os.path.dirname(path)
|
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 = 0
|
path_mtime: Union[float, int] = 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
|
||||||
|
|
||||||
@ -290,7 +360,7 @@ def generate(output, skip_index = False):
|
|||||||
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, unicode)):
|
if isinstance(func_or_str, str):
|
||||||
print(path)
|
print(path)
|
||||||
s = func_or_str
|
s = func_or_str
|
||||||
else:
|
else:
|
||||||
@ -299,135 +369,184 @@ def generate(output, skip_index = False):
|
|||||||
print(path)
|
print(path)
|
||||||
s = func_or_str(*args)
|
s = func_or_str(*args)
|
||||||
|
|
||||||
open(path, 'w').write(s.encode('utf8', errors = 'xmlcharrefreplace'))
|
open(path, "w").write(s)
|
||||||
if mtime:
|
if mtime:
|
||||||
os.utime(path, (mtime, mtime))
|
os.utime(path, (mtime, mtime))
|
||||||
|
|
||||||
def link(from_path, to_path):
|
def link(from_path, to_path):
|
||||||
from_path = output + '/' + from_path
|
from_path = output + "/" + from_path
|
||||||
|
|
||||||
if os.path.lexists(from_path):
|
if os.path.lexists(from_path):
|
||||||
return
|
return
|
||||||
print(from_path, '->', to_path)
|
print(from_path, "->", to_path)
|
||||||
os.symlink(to_path, from_path)
|
os.symlink(to_path, from_path)
|
||||||
|
|
||||||
def write_tree(r, bn, mtime):
|
def write_tree(r: git.Repo, bn: str, mtime):
|
||||||
t = r.tree(bn)
|
t: git.Tree = r.tree(bn)
|
||||||
|
|
||||||
write_to('r/%s/b/%s/t/index.html' % (r.name, bn),
|
write_to("r/%s/b/%s/t/index.html" % (r.name, bn), tree, (r, bn), mtime)
|
||||||
tree, (r, bn), mtime)
|
|
||||||
|
|
||||||
for otype, oname, _ in t.ls('', recursive = True):
|
for otype, oname, _ in t.ls("", recursive=True):
|
||||||
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
# FIXME: bottle cannot route paths with '\n' so those are sadly
|
||||||
# expected to fail for now; we skip them.
|
# expected to fail for now; we skip them.
|
||||||
if '\n' in oname.raw:
|
if "\n" in oname.raw:
|
||||||
print('skipping file with \\n: %r' % (oname.raw))
|
print("skipping file with \\n: %r" % (oname.raw))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if otype == 'blob':
|
if otype == "blob":
|
||||||
dirname = git.smstr(os.path.dirname(oname.raw))
|
dirname = git.smstr(os.path.dirname(oname.raw))
|
||||||
fname = git.smstr(os.path.basename(oname.raw))
|
fname = git.smstr(os.path.basename(oname.raw))
|
||||||
write_to(
|
write_to(
|
||||||
'r/%s/b/%s/t/%s/f=%s.html' %
|
"r/%s/b/%s/t/%s%sf=%s.html"
|
||||||
(str(r.name), str(bn), dirname.raw, fname.raw),
|
% (
|
||||||
blob, (r, bn, fname.url, dirname.url), mtime)
|
str(r.name),
|
||||||
|
str(bn),
|
||||||
|
dirname.raw,
|
||||||
|
"/" if dirname.raw else "",
|
||||||
|
fname.raw,
|
||||||
|
),
|
||||||
|
blob,
|
||||||
|
(r, bn, fname.url, dirname.url),
|
||||||
|
mtime,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
write_to('r/%s/b/%s/t/%s/index.html' %
|
write_to(
|
||||||
(str(r.name), str(bn), oname.raw),
|
"r/%s/b/%s/t/%s/index.html"
|
||||||
tree, (r, bn, oname.url), mtime)
|
% (str(r.name), str(bn), oname.raw),
|
||||||
|
tree,
|
||||||
|
(r, bn, oname.url),
|
||||||
|
mtime,
|
||||||
|
)
|
||||||
|
|
||||||
if not skip_index:
|
# Always generate the index, to keep the "last updated" time fresh.
|
||||||
write_to('index.html', index())
|
write_to("index.html", index())
|
||||||
|
|
||||||
# We can't call static() because it relies on HTTP headers.
|
# We can't call static() because it relies on HTTP headers.
|
||||||
read_f = lambda f: open(f).read()
|
read_f = lambda f: open(f).read()
|
||||||
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
write_to(
|
||||||
os.stat(static_path + '/git-arr.css').st_mtime)
|
"static/git-arr.css",
|
||||||
write_to('static/syntax.css', read_f, [static_path + '/syntax.css'],
|
read_f,
|
||||||
os.stat(static_path + '/syntax.css').st_mtime)
|
[static_path + "/git-arr.css"],
|
||||||
|
os.stat(static_path + "/git-arr.css").st_mtime,
|
||||||
|
)
|
||||||
|
write_to(
|
||||||
|
"static/git-arr.js",
|
||||||
|
read_f,
|
||||||
|
[static_path + "/git-arr.js"],
|
||||||
|
os.stat(static_path + "/git-arr.js").st_mtime,
|
||||||
|
)
|
||||||
|
write_to(
|
||||||
|
"static/syntax.css",
|
||||||
|
read_f,
|
||||||
|
[static_path + "/syntax.css"],
|
||||||
|
os.stat(static_path + "/syntax.css").st_mtime,
|
||||||
|
)
|
||||||
|
|
||||||
for r in sorted(repos.values(), key = lambda r: r.name):
|
rs = sorted(list(repos.values()), key=lambda r: r.name)
|
||||||
write_to('r/%s/index.html' % r.name, summary(r))
|
if only:
|
||||||
|
rs = [r for r in rs if r.name in only]
|
||||||
|
|
||||||
|
for r in rs:
|
||||||
|
write_to("r/%s/index.html" % r.name, summary(r))
|
||||||
for bn in r.branch_names():
|
for bn in r.branch_names():
|
||||||
commit_count = 0
|
commit_count = 0
|
||||||
commit_ids = r.commit_ids('refs/heads/' + bn,
|
commit_ids = r.commit_ids(
|
||||||
limit = r.info.commits_per_page * r.info.max_pages)
|
"refs/heads/" + bn,
|
||||||
|
limit=r.info.commits_per_page * r.info.max_pages,
|
||||||
|
)
|
||||||
for cid in commit_ids:
|
for cid in commit_ids:
|
||||||
write_to('r/%s/c/%s/index.html' % (r.name, cid),
|
write_to(
|
||||||
commit, (r, cid))
|
"r/%s/c/%s/index.html" % (r.name, cid), commit, (r, cid)
|
||||||
|
)
|
||||||
|
if r.info.generate_patch:
|
||||||
|
write_to(
|
||||||
|
"r/%s/c/%s.patch" % (r.name, cid), patch, (r, cid)
|
||||||
|
)
|
||||||
commit_count += 1
|
commit_count += 1
|
||||||
|
|
||||||
# To avoid regenerating files that have not changed, we will
|
# To avoid regenerating files that have not changed, we will
|
||||||
# instruct write_to() to set their mtime to the branch's committer
|
# instruct write_to() to set their mtime to the branch's committer
|
||||||
# date, and then compare against it to decide wether or not to
|
# date, and then compare against it to decide whether or not to
|
||||||
# write.
|
# write.
|
||||||
branch_mtime = r.commit(bn).committer_date.epoch
|
branch_mtime = r.commit(bn).committer_date.epoch
|
||||||
|
|
||||||
nr_pages = int(math.ceil(
|
nr_pages = int(
|
||||||
float(commit_count) / r.info.commits_per_page))
|
math.ceil(float(commit_count) / r.info.commits_per_page)
|
||||||
|
)
|
||||||
nr_pages = min(nr_pages, r.info.max_pages)
|
nr_pages = min(nr_pages, r.info.max_pages)
|
||||||
|
|
||||||
for page in range(nr_pages):
|
for page in range(nr_pages):
|
||||||
write_to('r/%s/b/%s/%d.html' % (r.name, bn, page),
|
write_to(
|
||||||
branch, (r, bn, page), branch_mtime)
|
"r/%s/b/%s/%d.html" % (r.name, bn, page),
|
||||||
|
branch,
|
||||||
|
(r, bn, page),
|
||||||
|
branch_mtime,
|
||||||
|
)
|
||||||
|
|
||||||
link(from_path = 'r/%s/b/%s/index.html' % (r.name, bn),
|
link(
|
||||||
to_path = '0.html')
|
from_path="r/%s/b/%s/index.html" % (r.name, bn),
|
||||||
|
to_path="0.html",
|
||||||
|
)
|
||||||
|
|
||||||
if r.info.generate_tree:
|
if r.info.generate_tree:
|
||||||
write_tree(r, bn, branch_mtime)
|
write_tree(r, bn, branch_mtime)
|
||||||
|
|
||||||
for tag_name, obj_id in r.tags():
|
for tag_name, obj_id in r.tags():
|
||||||
try:
|
try:
|
||||||
write_to('r/%s/c/%s/index.html' % (r.name, obj_id),
|
write_to(
|
||||||
commit, (r, obj_id))
|
"r/%s/c/%s/index.html" % (r.name, obj_id),
|
||||||
|
commit,
|
||||||
|
(r, obj_id),
|
||||||
|
)
|
||||||
except bottle.HTTPError as e:
|
except bottle.HTTPError as e:
|
||||||
# Some repos can have tags pointing to non-commits. This
|
# Some repos can have tags pointing to non-commits. This
|
||||||
# happens in the Linux Kernel's v2.6.11, which points directly
|
# happens in the Linux Kernel's v2.6.11, which points directly
|
||||||
# to a tree. Ignore them.
|
# to a tree. Ignore them.
|
||||||
if e.status == 404:
|
if is_404(e):
|
||||||
print('404 in tag %s (%s)' % (tag_name, obj_id))
|
print("404 in tag %s (%s)" % (tag_name, obj_id))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = optparse.OptionParser('usage: %prog [options] serve|generate')
|
parser = optparse.OptionParser("usage: %prog [options] serve|generate")
|
||||||
parser.add_option('-c', '--config', metavar = 'FILE',
|
parser.add_option(
|
||||||
help = 'configuration file')
|
"-c", "--config", metavar="FILE", help="configuration file"
|
||||||
parser.add_option('-o', '--output', metavar = 'DIR',
|
)
|
||||||
help = 'output directory (for generate)')
|
parser.add_option(
|
||||||
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
|
"-o", "--output", metavar="DIR", help="output directory (for generate)"
|
||||||
default = [],
|
)
|
||||||
help = 'generate/serve only this repository')
|
parser.add_option(
|
||||||
|
"",
|
||||||
|
"--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 opts.only:
|
if args[0] == "serve":
|
||||||
for rname in list(repos.keys()):
|
bottle.run(host="localhost", port=8008, reloader=True)
|
||||||
if rname not in opts.only:
|
elif args[0] == "generate":
|
||||||
del repos[rname]
|
|
||||||
|
|
||||||
if args[0] == 'serve':
|
|
||||||
bottle.run(host = 'localhost', port = 8008, reloader = True)
|
|
||||||
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,
|
generate(output=opts.output, only=opts.only)
|
||||||
skip_index = len(opts.only) > 0)
|
|
||||||
else:
|
else:
|
||||||
parser.error('Unknown action %s' % args[0])
|
parser.error("Unknown action %s" % args[0])
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
457
git.py
457
git.py
@ -6,97 +6,93 @@ 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
|
import urllib.request, urllib.parse, urllib.error
|
||||||
from cgi import escape
|
from html 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 __iter__(self):
|
def run_git(
|
||||||
for line in self.fd:
|
repo_path: str, params, stdin: bytes = None, silent_stderr=False, raw=False
|
||||||
yield line.decode(self.encoding, errors = self.errors)
|
) -> Union[IO[str], IO[bytes]]:
|
||||||
|
|
||||||
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):
|
|
||||||
"""Invokes git with the given parameters.
|
"""Invokes git with the given parameters.
|
||||||
|
|
||||||
This function invokes git with the given parameters, and returns a
|
This function invokes git with the given parameters, and returns a
|
||||||
file-like object with the output (from a pipe).
|
file-like object with the output (from a pipe).
|
||||||
"""
|
"""
|
||||||
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
|
params = [GIT_BIN, "--git-dir=%s" % repo_path] + list(params)
|
||||||
|
|
||||||
stderr = None
|
stderr = None
|
||||||
if silent_stderr:
|
if silent_stderr:
|
||||||
stderr = subprocess.PIPE
|
stderr = subprocess.PIPE
|
||||||
|
|
||||||
if not stdin:
|
if not stdin:
|
||||||
p = subprocess.Popen(params,
|
p = subprocess.Popen(
|
||||||
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
|
params, stdin=None, stdout=subprocess.PIPE, stderr=stderr
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
p = subprocess.Popen(params,
|
p = subprocess.Popen(
|
||||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
|
params,
|
||||||
stderr = stderr)
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert p.stdin is not None
|
||||||
p.stdin.write(stdin)
|
p.stdin.write(stdin)
|
||||||
p.stdin.close()
|
p.stdin.close()
|
||||||
|
|
||||||
# We need to wrap stdout if we want to decode it as utf8, subprocess
|
assert p.stdout is not None
|
||||||
# doesn't support us telling it the encoding.
|
|
||||||
if sys.version_info.major == 3:
|
if raw:
|
||||||
return io.TextIOWrapper(p.stdout, encoding = 'utf8',
|
return p.stdout
|
||||||
errors = 'replace')
|
|
||||||
else:
|
return io.TextIOWrapper(
|
||||||
return EncodeWrapper(p.stdout)
|
p.stdout, encoding="utf8", errors="backslashreplace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GitCommand (object):
|
class GitCommand(object):
|
||||||
"""Convenient way of invoking git."""
|
"""Convenient way of invoking git."""
|
||||||
def __init__(self, path, cmd, *args, **kwargs):
|
|
||||||
|
def __init__(self, path: str, cmd: str):
|
||||||
self._override = True
|
self._override = True
|
||||||
self._path = path
|
self._path = path
|
||||||
self._cmd = cmd
|
self._cmd = cmd
|
||||||
self._args = list(args)
|
self._args: List[str] = []
|
||||||
self._kwargs = {}
|
self._kwargs: Dict[str, str] = {}
|
||||||
self._stdin_buf = None
|
self._stdin_buf: Optional[bytes] = None
|
||||||
|
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):
|
def arg(self, a: str):
|
||||||
"""Adds an argument."""
|
"""Adds an argument."""
|
||||||
self._args.append(a)
|
self._args.append(a)
|
||||||
|
|
||||||
def stdin(self, s):
|
def raw(self, b: bool):
|
||||||
|
"""Request raw rather than utf8-encoded command output."""
|
||||||
|
self._override = True
|
||||||
|
self._raw = b
|
||||||
|
self._override = False
|
||||||
|
|
||||||
|
def stdin(self, s: bytes):
|
||||||
"""Sets the contents we will send in stdin."""
|
"""Sets the contents we will send in stdin."""
|
||||||
self._override = True
|
self._override = True
|
||||||
self._stdin_buf = s
|
self._stdin_buf = s
|
||||||
@ -106,46 +102,37 @@ class GitCommand (object):
|
|||||||
"""Runs the git command."""
|
"""Runs the git command."""
|
||||||
params = [self._cmd]
|
params = [self._cmd]
|
||||||
|
|
||||||
for k, v in self._kwargs.items():
|
for k, v in list(self._kwargs.items()):
|
||||||
dash = '--' if len(k) > 1 else '-'
|
dash = "--" if len(k) > 1 else "-"
|
||||||
if v is None:
|
if v is None:
|
||||||
params.append('%s%s' % (dash, k))
|
params.append("%s%s" % (dash, k))
|
||||||
else:
|
else:
|
||||||
params.append('%s%s=%s' % (dash, k, str(v)))
|
params.append("%s%s=%s" % (dash, k, str(v)))
|
||||||
|
|
||||||
params.extend(self._args)
|
params.extend(self._args)
|
||||||
|
|
||||||
return run_git(self._path, params, self._stdin_buf)
|
return run_git(self._path, params, self._stdin_buf, raw=self._raw)
|
||||||
|
|
||||||
|
|
||||||
class SimpleNamespace (object):
|
class SimpleNamespace(object):
|
||||||
"""An entirely flexible object, which provides a convenient namespace."""
|
"""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."""
|
||||||
|
|
||||||
This is a string class that contains:
|
raw: str # string, probably utf8-encoded, good enough to show.
|
||||||
.raw -> raw string, authoritative source.
|
url: str # escaped for safe embedding in URLs (not human-readable).
|
||||||
.unicode -> unicode representation, may not be perfect if .raw is not
|
html: str # HTML-embeddable representation.
|
||||||
proper utf8 but should be good enough to show.
|
|
||||||
.url -> escaped for safe embedding in URLs, can be not quite
|
def __init__(self, s: str):
|
||||||
readable.
|
self.raw = s
|
||||||
.html -> an HTML-embeddable representation.
|
self.url = urllib.request.pathname2url(s)
|
||||||
"""
|
|
||||||
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.
|
||||||
@ -153,11 +140,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.url2pathname(url))
|
return smstr(urllib.request.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):
|
||||||
@ -166,10 +153,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 = u''
|
html = ""
|
||||||
for c in escape(self.unicode):
|
for c in escape(self.raw):
|
||||||
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('ascii').encode('string_escape')
|
esc_c = c.encode("unicode-escape").decode("utf8")
|
||||||
html += '<span class="ctrlchr">%s</span>' % esc_c
|
html += '<span class="ctrlchr">%s</span>' % esc_c
|
||||||
else:
|
else:
|
||||||
html += c
|
html += c
|
||||||
@ -177,17 +164,26 @@ class smstr:
|
|||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
def unquote(s):
|
def unquote(s: str):
|
||||||
"""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; just
|
# Unquoted strings are always safe, no need to mess with them
|
||||||
# make sure we return str.
|
|
||||||
s = s.encode('ascii')
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
# Get rid of the quotes, we never want them in the output, and convert to
|
# The string will be of the form `"<escaped>"`, where <escaped> is a
|
||||||
# a raw string, un-escaping the backslashes.
|
# backslash-escaped representation of the name of the file.
|
||||||
s = s[1:-1].decode('string-escape')
|
# Examples: "with\ttwo\ttabs" , "\303\261aca-utf8", "\361aca-latin1"
|
||||||
|
|
||||||
|
# Get rid of the quotes, we never want them in the output.
|
||||||
|
s = s[1:-1]
|
||||||
|
|
||||||
|
# Un-escape the backslashes.
|
||||||
|
# latin1 is ok to use here because in Python it just maps the code points
|
||||||
|
# 0-255 to the bytes 0x-0xff, which is what we expect.
|
||||||
|
s = s.encode("latin1").decode("unicode-escape")
|
||||||
|
|
||||||
|
# Convert to utf8.
|
||||||
|
s = s.encode("latin1").decode("utf8", errors="backslashreplace")
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@ -195,93 +191,86 @@ def unquote(s):
|
|||||||
class Repo:
|
class Repo:
|
||||||
"""A git repository."""
|
"""A git repository."""
|
||||||
|
|
||||||
def __init__(self, path, branch = None, name = None, info = None):
|
def __init__(self, path: str, name=None, info=None):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.branch = branch
|
|
||||||
|
|
||||||
# We don't need these, but provide them for the users' convenience.
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.info = info or SimpleNamespace()
|
self.info: Any = 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)
|
||||||
|
|
||||||
def for_each_ref(self, pattern = None, sort = None):
|
@functools.lru_cache
|
||||||
|
def _for_each_ref(self, pattern=None, sort=None, count=None):
|
||||||
"""Returns a list of references."""
|
"""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:
|
||||||
|
cmd.count = count
|
||||||
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()
|
||||||
yield obj_id, obj_type, ref
|
refs.append((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."""
|
||||||
return ( name for name, _ in self.branches() )
|
refs = self._for_each_ref(pattern="refs/heads/", sort="-authordate")
|
||||||
|
return [ref[len("refs/heads/") :] for _, _, ref in refs]
|
||||||
|
|
||||||
def tags(self, sort = '-taggerdate'):
|
@functools.cache
|
||||||
|
def tags(self, sort="-taggerdate"):
|
||||||
"""Get the (name, obj_id) of the tags."""
|
"""Get the (name, obj_id) of the tags."""
|
||||||
refs = self.for_each_ref(pattern = 'refs/tags/', sort = sort)
|
refs = self._for_each_ref(pattern="refs/tags/", sort=sort)
|
||||||
for obj_id, _, ref in refs:
|
return [(ref[len("refs/tags/") :], obj_id) for obj_id, _, ref in refs]
|
||||||
yield ref[len('refs/tags/'):], obj_id
|
|
||||||
|
|
||||||
def tag_names(self):
|
@functools.lru_cache
|
||||||
"""Get the names of the tags."""
|
def commit_ids(self, ref, limit=None):
|
||||||
return ( name for name, _ in self.tags() )
|
|
||||||
|
|
||||||
def new_in_branch(self, branch):
|
|
||||||
"""Returns a new Repo, but on the specific branch."""
|
|
||||||
return Repo(self.path, branch = branch, name = self.name,
|
|
||||||
info = self.info)
|
|
||||||
|
|
||||||
def commit_ids(self, ref, limit = None):
|
|
||||||
"""Generate commit ids."""
|
"""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("--")
|
||||||
|
|
||||||
for l in cmd.run():
|
return [l.rstrip("\n") 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]
|
||||||
|
|
||||||
def commits(self, ref, limit = None, offset = 0):
|
@functools.lru_cache
|
||||||
|
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")
|
||||||
if limit:
|
cmd.max_count = limit + offset
|
||||||
cmd.max_count = limit + offset
|
|
||||||
|
|
||||||
cmd.header = None
|
cmd.header = None
|
||||||
|
|
||||||
cmd.arg(ref)
|
cmd.arg(ref)
|
||||||
|
cmd.arg("--")
|
||||||
|
|
||||||
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:
|
||||||
yield Commit.from_str(self, info_buffer)
|
commits.append(Commit.from_str(self, info_buffer))
|
||||||
|
|
||||||
# Start over.
|
# Start over.
|
||||||
info_buffer = post
|
info_buffer = post
|
||||||
@ -291,14 +280,19 @@ class Repo:
|
|||||||
if info_buffer:
|
if info_buffer:
|
||||||
count += 1
|
count += 1
|
||||||
if count > offset:
|
if count > offset:
|
||||||
yield Commit.from_str(self, info_buffer)
|
commits.append(Commit.from_str(self, info_buffer))
|
||||||
|
|
||||||
|
return commits
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
def diff(self, ref):
|
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:
|
||||||
|
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.
|
||||||
|
|
||||||
@ -306,52 +300,72 @@ 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
|
||||||
|
|
||||||
def tree(self, ref = None):
|
@functools.lru_cache
|
||||||
|
def tree(self, ref):
|
||||||
"""Returns a Tree instance for the given ref."""
|
"""Returns a Tree instance for the given ref."""
|
||||||
if not ref:
|
|
||||||
ref = self.branch
|
|
||||||
return Tree(self, ref)
|
return Tree(self, ref)
|
||||||
|
|
||||||
def blob(self, path, ref = None):
|
@functools.lru_cache
|
||||||
"""Returns the contents of the given path."""
|
def blob(self, path, ref):
|
||||||
if not ref:
|
"""Returns a Blob instance for the given path."""
|
||||||
ref = self.branch
|
cmd = self.cmd("cat-file")
|
||||||
cmd = self.cmd('cat-file')
|
cmd.raw(True)
|
||||||
cmd.batch = None
|
cmd.batch = "%(objectsize)"
|
||||||
|
|
||||||
if isinstance(ref, unicode):
|
# Format: <ref>:<path>
|
||||||
ref = ref.encode('utf8')
|
# Construct it in binary since the path might not be utf8.
|
||||||
cmd.stdin('%s:%s' % (ref, path))
|
cmd.stdin(ref.encode("utf8") + b":" + path)
|
||||||
|
|
||||||
out = cmd.run()
|
out = cmd.run()
|
||||||
head = out.readline()
|
head = out.readline()
|
||||||
if not head or head.strip().endswith('missing'):
|
if not head or head.strip().endswith(b"missing"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return out.read()
|
return Blob(out.read()[: int(head)])
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def last_commit_timestamp(self):
|
||||||
|
"""Return the timestamp of the last commit."""
|
||||||
|
refs = self._for_each_ref(
|
||||||
|
pattern="refs/heads/", sort="-committerdate", count=1
|
||||||
|
)
|
||||||
|
for obj_id, _, _ in refs:
|
||||||
|
commit = self.commit(obj_id)
|
||||||
|
return commit.committer_epoch
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
class Commit (object):
|
class Commit(object):
|
||||||
"""A git commit."""
|
"""A git commit."""
|
||||||
|
|
||||||
def __init__(self, repo,
|
def __init__(
|
||||||
commit_id, parents, tree,
|
self,
|
||||||
author, author_epoch, author_tz,
|
repo,
|
||||||
committer, committer_epoch, committer_tz,
|
commit_id,
|
||||||
message):
|
parents,
|
||||||
|
tree,
|
||||||
|
author,
|
||||||
|
author_epoch,
|
||||||
|
author_tz,
|
||||||
|
committer,
|
||||||
|
committer_epoch,
|
||||||
|
committer_tz,
|
||||||
|
message,
|
||||||
|
):
|
||||||
self._repo = repo
|
self._repo = repo
|
||||||
self.id = commit_id
|
self.id = commit_id
|
||||||
self.parents = parents
|
self.parents = parents
|
||||||
@ -364,28 +378,30 @@ class Commit (object):
|
|||||||
self.committer_tz = committer_tz
|
self.committer_tz = committer_tz
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
self.author_name, self.author_email = \
|
self.author_name, self.author_email = email.utils.parseaddr(
|
||||||
email.utils.parseaddr(self.author)
|
self.author
|
||||||
|
)
|
||||||
|
|
||||||
self.committer_name, self.committer_email = \
|
self.committer_name, self.committer_email = email.utils.parseaddr(
|
||||||
email.utils.parseaddr(self.committer)
|
self.committer
|
||||||
|
)
|
||||||
|
|
||||||
self.subject, self.body = self.message.split('\n', 1)
|
self.subject, self.body = self.message.split("\n", 1)
|
||||||
|
|
||||||
self.author_date = Date(self.author_epoch, self.author_tz)
|
self.author_date = Date(self.author_epoch, self.author_tz)
|
||||||
self.committer_date = Date(self.committer_epoch, self.committer_tz)
|
self.committer_date = Date(self.committer_epoch, self.committer_tz)
|
||||||
|
|
||||||
|
|
||||||
# Only get this lazily when we need it; most of the time it's not
|
# Only get this lazily when we need it; most of the time it's not
|
||||||
# required by the caller.
|
# required by the caller.
|
||||||
self._diff = None
|
self._diff = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<C %s p:%s a:%s s:%r>' % (
|
return "<C %s p:%s a:%s s:%r>" % (
|
||||||
self.id[:7],
|
self.id[:7],
|
||||||
','.join(p[:7] for p in self.parents),
|
",".join(p[:7] for p in self.parents),
|
||||||
self.author_email,
|
self.author_email,
|
||||||
self.subject[:20])
|
self.subject[:20],
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diff(self):
|
def diff(self):
|
||||||
@ -397,52 +413,68 @@ class Commit (object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_str(repo, buf):
|
def from_str(repo, buf):
|
||||||
"""Parses git rev-list output, returns a commit object."""
|
"""Parses git rev-list output, returns a commit object."""
|
||||||
header, raw_message = buf.split('\n\n', 1)
|
if "\n\n" in buf:
|
||||||
|
# Header, commit message
|
||||||
|
header, raw_message = buf.split("\n\n", 1)
|
||||||
|
else:
|
||||||
|
# Header only, no commit message
|
||||||
|
header, raw_message = buf.rstrip(), " "
|
||||||
|
|
||||||
header_lines = header.split('\n')
|
header_lines = header.split("\n")
|
||||||
commit_id = header_lines.pop(0)
|
commit_id = header_lines.pop(0)
|
||||||
|
|
||||||
header_dict = defaultdict(list)
|
header_dict = defaultdict(list)
|
||||||
for line in header_lines:
|
for line in header_lines:
|
||||||
k, v = line.split(' ', 1)
|
k, v = line.split(" ", 1)
|
||||||
header_dict[k].append(v)
|
header_dict[k].append(v)
|
||||||
|
|
||||||
tree = header_dict['tree'][0]
|
tree = header_dict["tree"][0]
|
||||||
parents = set(header_dict['parent'])
|
parents = set(header_dict["parent"])
|
||||||
author, author_epoch, author_tz = \
|
|
||||||
header_dict['author'][0].rsplit(' ', 2)
|
authorhdr = header_dict["author"][0]
|
||||||
committer, committer_epoch, committer_tz = \
|
author, author_epoch, author_tz = authorhdr.rsplit(" ", 2)
|
||||||
header_dict['committer'][0].rsplit(' ', 2)
|
|
||||||
|
committerhdr = header_dict["committer"][0]
|
||||||
|
committer, committer_epoch, committer_tz = committerhdr.rsplit(" ", 2)
|
||||||
|
|
||||||
# Remove the first four spaces from the message's lines.
|
# Remove the first four spaces from the message's lines.
|
||||||
message = ''
|
message = ""
|
||||||
for line in raw_message.split('\n'):
|
for line in raw_message.split("\n"):
|
||||||
message += line[4:] + '\n'
|
message += line[4:] + "\n"
|
||||||
|
|
||||||
|
return Commit(
|
||||||
|
repo,
|
||||||
|
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.fromtimestamp(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
|
||||||
@ -450,6 +482,7 @@ class Date:
|
|||||||
|
|
||||||
class Diff:
|
class Diff:
|
||||||
"""A diff between two trees."""
|
"""A diff between two trees."""
|
||||||
|
|
||||||
def __init__(self, ref, changes, body):
|
def __init__(self, ref, changes, body):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
@ -469,60 +502,82 @@ 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, ref):
|
def __init__(self, repo: Repo, ref: str):
|
||||||
self.repo = repo
|
self.repo = repo
|
||||||
self.ref = ref
|
self.ref = ref
|
||||||
|
|
||||||
def ls(self, path, recursive = False):
|
@functools.lru_cache
|
||||||
|
def ls(
|
||||||
|
self, path, recursive=False
|
||||||
|
) -> Iterable[Tuple[str, smstr, Optional[int]]]:
|
||||||
"""Generates (type, name, size) for each file in path."""
|
"""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
|
||||||
cmd.t = None
|
cmd.t = None
|
||||||
|
|
||||||
cmd.arg(self.ref)
|
cmd.arg(self.ref)
|
||||||
cmd.arg(path)
|
if not path:
|
||||||
|
cmd.arg(".")
|
||||||
|
else:
|
||||||
|
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.
|
||||||
yield otype, smstr(name), size
|
files.append((otype, smstr(name), size))
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
class Blob:
|
||||||
|
"""A git blob."""
|
||||||
|
|
||||||
|
def __init__(self, raw_content: bytes):
|
||||||
|
self.raw_content = raw_content
|
||||||
|
self._utf8_content = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def utf8_content(self):
|
||||||
|
if not self._utf8_content:
|
||||||
|
self._utf8_content = self.raw_content.decode("utf8", "replace")
|
||||||
|
return self._utf8_content
|
||||||
|
|||||||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
include = "(git-arr|git.py|utils.py)$"
|
||||||
30
sample.conf
30
sample.conf
@ -11,6 +11,15 @@ path = /srv/git/repo/
|
|||||||
# Useful to disable an expensive operation in very large repositories.
|
# Useful to disable an expensive operation in very large repositories.
|
||||||
#tree = yes
|
#tree = yes
|
||||||
|
|
||||||
|
# Show a "creation event" diff for a root commit? (optional)
|
||||||
|
# For projects placed under revision control from inception, the root commit
|
||||||
|
# diff is often meaningful. However, in cases when a well established, large
|
||||||
|
# project is placed under revision control belatedly, the root commit may
|
||||||
|
# represent a lump import of the entire project, in which case such a
|
||||||
|
# "creation event" diff would likely be considered meaningless noise.
|
||||||
|
# Default: yes
|
||||||
|
#rootdiff = yes
|
||||||
|
|
||||||
# How many commits to show in the summary page (optional).
|
# How many commits to show in the summary page (optional).
|
||||||
#commits_in_summary = 10
|
#commits_in_summary = 10
|
||||||
|
|
||||||
@ -19,8 +28,9 @@ path = /srv/git/repo/
|
|||||||
|
|
||||||
# Maximum number of per-branch pages for static generation (optional).
|
# Maximum number of per-branch pages for static generation (optional).
|
||||||
# When generating static html, this is the maximum number of pages we will
|
# When generating static html, this is the maximum number of pages we will
|
||||||
# generate for each branch's commit listings.
|
# generate for each branch's commit listings. Zero (0) means unlimited.
|
||||||
#max_pages = 5
|
# Default: 250
|
||||||
|
#max_pages = 250
|
||||||
|
|
||||||
# Project website (optional).
|
# Project website (optional).
|
||||||
# URL to the project's website. %(name)s will be replaced with the current
|
# URL to the project's website. %(name)s will be replaced with the current
|
||||||
@ -48,6 +58,22 @@ path = /srv/git/repo/
|
|||||||
# excluded.
|
# excluded.
|
||||||
#recursive = no
|
#recursive = no
|
||||||
|
|
||||||
|
# Render Markdown blobs (*.md) formatted rather than as raw text? (optional)
|
||||||
|
# Requires 'markdown' module.
|
||||||
|
# Default: yes
|
||||||
|
#embed_markdown = yes
|
||||||
|
|
||||||
|
# Render image blobs graphically rather than as raw binary data? (optional)
|
||||||
|
# Default: no
|
||||||
|
#embed_images = no
|
||||||
|
|
||||||
|
# Ignore repositories that match this regular expression.
|
||||||
|
# Generally used with recursive = yes, to ignore repeated repositories (for
|
||||||
|
# example, if using symlinks).
|
||||||
|
# For ignoring specific repositories, putting a "disable_gitweb" is a much
|
||||||
|
# better alternative.
|
||||||
|
# Default: empty (don't ignore)
|
||||||
|
#ignore = \.git$
|
||||||
|
|
||||||
# Another repository, we don't generate a tree for it because it's too big.
|
# Another repository, we don't generate a tree for it because it's too big.
|
||||||
[linux]
|
[linux]
|
||||||
|
|||||||
@ -1,17 +1,53 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 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;
|
||||||
font-size: small;
|
|
||||||
padding: 0 1em 1em 1em;
|
padding: 0 1em 1em 1em;
|
||||||
|
color: var(--text-fg);
|
||||||
|
background: var(--body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: x-large;
|
background: var(--h1-bg);
|
||||||
background: #ddd;
|
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,146 +59,279 @@ h2, h3 {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #e3e3e3;
|
background-color: var(--hr-bg);
|
||||||
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: black;
|
color: var(--text-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
color: var(--a-fg);
|
||||||
color: #800;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Explicit links */
|
/* Explicit links */
|
||||||
a.explicit {
|
a.explicit {
|
||||||
color: #038;
|
color: var(--a-explicit-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.explicit:hover, a.explicit:active {
|
a.explicit:hover, a.explicit:active {
|
||||||
color: #880000;
|
color: var(--a-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Normal table, for listing things like repositories, branches, etc. */
|
/* Normal table, for listing things like repositories, branches, etc. */
|
||||||
table.nice {
|
table.nice {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: small;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td {
|
table.nice td {
|
||||||
padding: 0.15em 0.5em;
|
padding: 0.15em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td.links {
|
table.nice td.links {
|
||||||
font-size: smaller;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice td.main {
|
table.nice td.main {
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.nice tr:hover {
|
table.nice tr:hover {
|
||||||
background: #eee;
|
background: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Table for commits. */
|
/* Table for commits. */
|
||||||
table.commits td.date {
|
table.commits td.date {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
table.commits td.subject {
|
|
||||||
min-width: 32em;
|
@media (min-width: 600px) {
|
||||||
|
table.commits td.subject {
|
||||||
|
min-width: 32em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.commits td.author {
|
table.commits td.author {
|
||||||
color: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 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: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 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 gray;
|
border: solid 1px var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.head {
|
span.head {
|
||||||
background-color: #88ff88;
|
background-color: var(--head-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.tag {
|
span.tag {
|
||||||
background-color: #ffff88;
|
background-color: var(--tag-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Projects table */
|
||||||
|
table.projects td.name a {
|
||||||
|
color: var(--a-explicit-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Age of an object.
|
||||||
|
* Note this is hidden by default as we rely on javascript to show it. */
|
||||||
|
span.age {
|
||||||
|
display: none;
|
||||||
|
color: var(--text-lowcontrast-fg);
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.age-band0 {
|
||||||
|
color: var(--age-fg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.age-band1 {
|
||||||
|
color: var(--age-fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.age-band2 {
|
||||||
|
color: var(--age-fg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Toggable titles */
|
||||||
|
div.toggable-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
/* Sometimes, <pre> elements (commit messages, diffs, blobs) have very
|
||||||
|
* long lines. In those case, use automatic overflow, which will
|
||||||
|
* introduce a horizontal scroll bar for this element only (more
|
||||||
|
* comfortable than stretching the page, which is the default). */
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Commit message and diff. */
|
/* Commit message and diff. */
|
||||||
pre.commit-message {
|
pre.commit-message {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
padding: 0.2em 2em;
|
padding: 0.2em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
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. */
|
||||||
font-size: medium;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.changed-files {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
table.changed-files span.lines-added {
|
table.changed-files span.lines-added {
|
||||||
color: green;
|
color: var(--diff-added-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.changed-files span.lines-deleted {
|
table.changed-files span.lines-deleted {
|
||||||
color: red;
|
color: var(--diff-deleted-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Pagination. */
|
/* Pagination. */
|
||||||
div.paginate {
|
div.paginate {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.paginate span.inactive {
|
div.paginate span.inactive {
|
||||||
color: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Directory listing. */
|
/* Directory listing. */
|
||||||
table.ls td.name {
|
@media (min-width: 600px) {
|
||||||
min-width: 20em;
|
table.ls td.name {
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.ls {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
table.ls tr.blob td.size {
|
table.ls tr.blob td.size {
|
||||||
color: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 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. */
|
||||||
font-size: medium;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.blob-binary pre {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.blob-binary .offset {
|
||||||
|
text-align: right;
|
||||||
|
font-size: x-small;
|
||||||
|
color: var(--text-lowcontrast-fg);
|
||||||
|
border-right: 1px solid var(--text-lowcontrast-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.blob-binary tr.etc {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Pygments overrides. */
|
/* 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;
|
||||||
color: gray;
|
|
||||||
font-size: medium;
|
|
||||||
}
|
}
|
||||||
div.source_code {
|
|
||||||
background: inherit;
|
div.linenodiv a {
|
||||||
font-size: medium;
|
color: var(--text-lowcontrast-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 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:
|
||||||
|
* git clone | url */
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.repo_info td {
|
table.repo_info td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.ctrlchr {
|
span.ctrlchr {
|
||||||
color: gray;
|
color: var(--text-lowcontrast-fg);
|
||||||
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Colored links (same as explicit links above) */
|
||||||
|
div.markdown a {
|
||||||
|
color: var(--a-explicit-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.markdown a:hover, div.markdown a:active {
|
||||||
|
color: var(--a-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Restrict max width for readability */
|
||||||
|
div.markdown {
|
||||||
|
max-width: 55em;
|
||||||
}
|
}
|
||||||
|
|||||||
73
static/git-arr.js
Normal file
73
static/git-arr.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/* Miscellaneous javascript functions for git-arr. */
|
||||||
|
|
||||||
|
/* Return the current timestamp. */
|
||||||
|
function now() {
|
||||||
|
return (new Date().getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return a human readable string telling "how long ago" for a timestamp. */
|
||||||
|
function how_long_ago(timestamp) {
|
||||||
|
if (timestamp < 0)
|
||||||
|
return "never";
|
||||||
|
|
||||||
|
var seconds = Math.floor(now() - timestamp);
|
||||||
|
|
||||||
|
var interval = Math.floor(seconds / (365 * 24 * 60 * 60));
|
||||||
|
if (interval > 1)
|
||||||
|
return interval + " years ago";
|
||||||
|
|
||||||
|
interval = Math.floor(seconds / (30 * 24 * 60 * 60));
|
||||||
|
if (interval > 1)
|
||||||
|
return interval + " months ago";
|
||||||
|
|
||||||
|
interval = Math.floor(seconds / (24 * 60 * 60));
|
||||||
|
|
||||||
|
if (interval > 1)
|
||||||
|
return interval + " days ago";
|
||||||
|
interval = Math.floor(seconds / (60 * 60));
|
||||||
|
|
||||||
|
if (interval > 1)
|
||||||
|
return interval + " hours ago";
|
||||||
|
|
||||||
|
interval = Math.floor(seconds / 60);
|
||||||
|
if (interval > 1)
|
||||||
|
return interval + " minutes ago";
|
||||||
|
|
||||||
|
if (seconds > 1)
|
||||||
|
return Math.floor(seconds) + " seconds ago";
|
||||||
|
|
||||||
|
return "about now";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Go through the document and replace the contents of the span.age elements
|
||||||
|
* with a human-friendly variant, and then show them. */
|
||||||
|
function replace_timestamps() {
|
||||||
|
var elements = document.getElementsByClassName("age");
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
var e = elements[i];
|
||||||
|
|
||||||
|
var timestamp = e.innerHTML;
|
||||||
|
e.innerHTML = how_long_ago(timestamp);
|
||||||
|
e.style.display = "inline";
|
||||||
|
|
||||||
|
if (timestamp > 0) {
|
||||||
|
var age = now() - timestamp;
|
||||||
|
if (age < (2 * 60 * 60))
|
||||||
|
e.className = e.className + " age-band0";
|
||||||
|
else if (age < (3 * 24 * 60 * 60))
|
||||||
|
e.className = e.className + " age-band1";
|
||||||
|
else if (age < (30 * 24 * 60 * 60))
|
||||||
|
e.className = e.className + " age-band2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(id) {
|
||||||
|
var e = document.getElementById(id);
|
||||||
|
|
||||||
|
if (e.style.display == "") {
|
||||||
|
e.style.display = "none"
|
||||||
|
} else if (e.style.display == "none") {
|
||||||
|
e.style.display = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,30 +1,37 @@
|
|||||||
|
|
||||||
/* CSS for syntax highlighting.
|
/* CSS for syntax highlighting.
|
||||||
* Generated by pygments (what we use for syntax highlighting):
|
* Generated by pygments (what we use for syntax highlighting).
|
||||||
*
|
*
|
||||||
* $ pygmentize -S default -f html -a .source_code
|
* Light mode: pygmentize -S default -f html -a .source_code
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pre { line-height: 125%; }
|
||||||
|
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
.source_code .hll { background-color: #ffffcc }
|
.source_code .hll { background-color: #ffffcc }
|
||||||
.source_code { background: #f8f8f8; }
|
.source_code { background: #f8f8f8; }
|
||||||
.source_code .c { color: #408080; font-style: italic } /* Comment */
|
.source_code .c { color: #3D7B7B; 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 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
.source_code .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
||||||
.source_code .cp { color: #BC7A00 } /* Comment.Preproc */
|
.source_code .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
||||||
.source_code .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
.source_code .cp { color: #9C6500 } /* Comment.Preproc */
|
||||||
.source_code .cs { color: #408080; font-style: italic } /* Comment.Special */
|
.source_code .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
||||||
|
.source_code .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
|
||||||
|
.source_code .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
|
||||||
.source_code .gd { color: #A00000 } /* Generic.Deleted */
|
.source_code .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: #FF0000 } /* Generic.Error */
|
.source_code .gr { color: #E40000 } /* 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: #00A000 } /* Generic.Inserted */
|
.source_code .gi { color: #008400 } /* Generic.Inserted */
|
||||||
.source_code .go { color: #808080 } /* Generic.Output */
|
.source_code .go { color: #717171 } /* 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: #0040D0 } /* Generic.Traceback */
|
.source_code .gt { color: #0044DD } /* 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 */
|
||||||
@ -33,38 +40,139 @@
|
|||||||
.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: #7D9029 } /* Name.Attribute */
|
.source_code .na { color: #687822 } /* 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: #999999; font-weight: bold } /* Name.Entity */
|
.source_code .ni { color: #717171; font-weight: bold } /* Name.Entity */
|
||||||
.source_code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
.source_code .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
||||||
.source_code .nf { color: #0000FF } /* Name.Function */
|
.source_code .nf { color: #0000FF } /* Name.Function */
|
||||||
.source_code .nl { color: #A0A000 } /* Name.Label */
|
.source_code .nl { color: #767600 } /* Name.Label */
|
||||||
.source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
.source_code .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: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
.source_code .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
|
||||||
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||||
.source_code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
.source_code .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
||||||
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
.source_code .sx { color: #008000 } /* Literal.String.Other */
|
||||||
.source_code .sr { color: #BB6688 } /* Literal.String.Regex */
|
.source_code .sr { color: #A45A77 } /* Literal.String.Regex */
|
||||||
.source_code .s1 { color: #BA2121 } /* Literal.String.Single */
|
.source_code .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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
166
utils.py
166
utils.py
@ -5,20 +5,44 @@ These are mostly used in templates, for presentation purposes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pygments
|
import pygments # type: ignore
|
||||||
from pygments import highlight
|
from pygments import highlight # type: ignore
|
||||||
from pygments import lexers
|
from pygments import lexers # type: ignore
|
||||||
from pygments.formatters import HtmlFormatter
|
from pygments.formatters import HtmlFormatter # type: ignore
|
||||||
|
|
||||||
|
_html_formatter = HtmlFormatter(
|
||||||
|
encoding="utf-8",
|
||||||
|
cssclass="source_code",
|
||||||
|
linenos="table",
|
||||||
|
anchorlinenos=True,
|
||||||
|
lineanchors="line",
|
||||||
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pygments = None
|
pygments = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import markdown # type: ignore
|
||||||
|
import markdown.treeprocessors # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
markdown = None
|
||||||
|
|
||||||
def shorten(s, width = 60):
|
import base64
|
||||||
|
import functools
|
||||||
|
import mimetypes
|
||||||
|
import string
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import git
|
||||||
|
|
||||||
|
|
||||||
|
def shorten(s: str, width=60):
|
||||||
if len(s) < 60:
|
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
|
||||||
@ -31,7 +55,7 @@ def can_colorize(s):
|
|||||||
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
||||||
start = 0
|
start = 0
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
pos = s.find('\n', start)
|
pos = s.find("\n", start)
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -41,30 +65,132 @@ def can_colorize(s):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def colorize_diff(s):
|
|
||||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
def can_markdown(repo: git.Repo, fname: str):
|
||||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
"""True if we can process file through markdown, False otherwise."""
|
||||||
cssclass = 'source_code')
|
if markdown is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not repo.info.embed_markdown:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return fname.endswith(".md")
|
||||||
|
|
||||||
|
|
||||||
|
def can_embed_image(repo, fname):
|
||||||
|
"""True if we can embed image file in HTML, False otherwise."""
|
||||||
|
if not repo.info.embed_images:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return ("." in fname) and (
|
||||||
|
fname.split(".")[-1].lower() in ["jpg", "jpeg", "png", "gif"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def colorize_diff(s: str) -> str:
|
||||||
|
lexer = lexers.DiffLexer(encoding="utf-8")
|
||||||
|
formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
|
||||||
|
|
||||||
return highlight(s, lexer, formatter)
|
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
|
||||||
|
|
||||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
return highlight(s, lexer, _html_formatter)
|
||||||
cssclass = 'source_code',
|
|
||||||
linenos = 'table')
|
|
||||||
|
|
||||||
return highlight(s, lexer, formatter)
|
|
||||||
|
|
||||||
|
def embed_image_blob(fname: str, image_data: bytes) -> str:
|
||||||
|
mimetype = mimetypes.guess_type(fname)[0]
|
||||||
|
b64img = base64.b64encode(image_data).decode("ascii")
|
||||||
|
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format(
|
||||||
|
mimetype, b64img
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def is_binary(b: bytes):
|
||||||
|
# Git considers a blob binary if NUL in first ~8KB, so do the same.
|
||||||
|
return b"\0" in b[:8192]
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def hexdump(s: bytes):
|
||||||
|
graph = string.ascii_letters + string.digits + string.punctuation + " "
|
||||||
|
b = s.decode("latin1")
|
||||||
|
offset = 0
|
||||||
|
while b:
|
||||||
|
t = b[:16]
|
||||||
|
hexvals = ["%.2x" % ord(c) for c in t]
|
||||||
|
text = "".join(c if c in graph else "." for c in t)
|
||||||
|
yield offset, " ".join(hexvals[:8]), " ".join(hexvals[8:]), text
|
||||||
|
offset += 16
|
||||||
|
b = b[16:]
|
||||||
|
|
||||||
|
|
||||||
|
if markdown:
|
||||||
|
|
||||||
|
class RewriteLocalLinks(markdown.treeprocessors.Treeprocessor):
|
||||||
|
"""Rewrites relative links to files, to match git-arr's links.
|
||||||
|
|
||||||
|
A link of "[example](a/file.md)" will be rewritten such that it links to
|
||||||
|
"a/f=file.md.html".
|
||||||
|
|
||||||
|
Note that we're already assuming a degree of sanity in the HTML, so we
|
||||||
|
don't re-check that the path is reasonable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self, root):
|
||||||
|
for child in root:
|
||||||
|
if child.tag == "a":
|
||||||
|
self.rewrite_href(child)
|
||||||
|
|
||||||
|
# Continue recursively.
|
||||||
|
self.run(child)
|
||||||
|
|
||||||
|
def rewrite_href(self, tag):
|
||||||
|
"""Rewrite an <a>'s href."""
|
||||||
|
target = tag.get("href")
|
||||||
|
if not target:
|
||||||
|
return
|
||||||
|
if "://" in target or target.startswith("/"):
|
||||||
|
return
|
||||||
|
|
||||||
|
head, tail = os.path.split(target)
|
||||||
|
new_target = os.path.join(head, "f=" + tail + ".html")
|
||||||
|
tag.set("href", new_target)
|
||||||
|
|
||||||
|
class RewriteLocalLinksExtension(markdown.Extension):
|
||||||
|
def extendMarkdown(self, md):
|
||||||
|
md.treeprocessors.register(
|
||||||
|
RewriteLocalLinks(), "RewriteLocalLinks", 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
_md_extensions = [
|
||||||
|
"markdown.extensions.fenced_code",
|
||||||
|
"markdown.extensions.tables",
|
||||||
|
RewriteLocalLinksExtension(),
|
||||||
|
]
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def markdown_blob(s: str) -> str:
|
||||||
|
return markdown.markdown(s, extensions=_md_extensions)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def markdown_blob(s: str) -> str:
|
||||||
|
raise RuntimeError("markdown_blob() called without markdown support")
|
||||||
|
|||||||
@ -1,46 +1,88 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
% if not dirname.raw:
|
% if not dirname.raw:
|
||||||
% relroot = './'
|
% reltree = './'
|
||||||
% else:
|
% else:
|
||||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||||
% end
|
% end
|
||||||
|
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||||
|
|
||||||
<title>git » {{repo.name}} »
|
<title>git » {{repo.name}} »
|
||||||
{{repo.branch}} » {{dirname.unicode}}/{{fname.unicode}}</title>
|
{{branch}} » {{dirname.raw}}{{fname.raw}}</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"
|
||||||
href="{{relroot}}../../../../../static/syntax.css"/>
|
href="{{relroot}}../../../../../static/syntax.css"/>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="tree">
|
<body class="tree">
|
||||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
<a href="{{reltree}}../">{{branch}}</a> »
|
||||||
<a href="{{relroot}}">tree</a>
|
<a href="{{reltree}}">tree</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||||
% base = smstr(relroot)
|
% base = smstr(reltree)
|
||||||
% for c in dirname.split('/'):
|
% for c in dirname.split('/'):
|
||||||
% if not c.raw: continue
|
% if not c.raw:
|
||||||
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
% continue
|
||||||
|
% end
|
||||||
|
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
||||||
% base += c + '/'
|
% base += c + '/'
|
||||||
% end
|
% end
|
||||||
<a href="">{{!fname.html}}</a>
|
<a href="">{{!fname.html}}</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
% if can_colorize(blob):
|
% if len(blob.raw_content) == 0:
|
||||||
{{!colorize_blob(fname.unicode, blob)}}
|
<table class="nice">
|
||||||
|
<tr>
|
||||||
|
<td>empty — 0 bytes</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
% elif can_embed_image(repo, fname.raw):
|
||||||
|
{{!embed_image_blob(fname.raw, blob.raw_content)}}
|
||||||
|
% elif is_binary(blob.raw_content):
|
||||||
|
<table class="nice blob-binary">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
binary — {{'{:,}'.format(len(blob.raw_content))}} bytes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% lim = 256
|
||||||
|
% for offset, hex1, hex2, text in hexdump(blob.raw_content[:lim]):
|
||||||
|
<tr>
|
||||||
|
<td class="offset">{{offset}}</td>
|
||||||
|
<td><pre>{{hex1}}</pre></td>
|
||||||
|
<td><pre>{{hex2}}</pre></td>
|
||||||
|
<td><pre>{{text}}</pre></td>
|
||||||
|
</tr>
|
||||||
|
% end
|
||||||
|
% if lim < len(blob.raw_content):
|
||||||
|
<tr class="etc">
|
||||||
|
<td></td>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
</tr>
|
||||||
|
% end
|
||||||
|
</table>
|
||||||
|
% elif can_markdown(repo, fname.raw):
|
||||||
|
<div class="markdown">
|
||||||
|
{{!markdown_blob(blob.utf8_content)}}
|
||||||
|
</div>
|
||||||
|
% elif can_colorize(blob.utf8_content):
|
||||||
|
<div class="colorized-src">
|
||||||
|
{{!colorize_blob(fname.raw, blob.utf8_content)}}
|
||||||
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<pre class="blob-body">
|
<pre class="blob-body">
|
||||||
{{blob}}
|
{{blob.utf8_content}}
|
||||||
</pre>
|
</pre>
|
||||||
% end
|
% end
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,27 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
<title>git » {{repo.name}} » {{repo.branch}}</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
% relroot = '../' * (len(branch.split('/')) - 1)
|
||||||
|
|
||||||
|
<title>git » {{repo.name}} » {{branch}}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{relroot}}../../../../static/git-arr.css"/>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="branch">
|
<body class="branch">
|
||||||
<h1><a href="../../../../">git</a> »
|
<h1><a href="{{relroot}}../../../../">git</a> »
|
||||||
<a href="../../">{{repo.name}}</a> »
|
<a href="{{relroot}}../../">{{repo.name}}</a> »
|
||||||
<a href="./">{{repo.branch}}</a>
|
<a href="./">{{branch}}</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a class="explicit" href="t/">Browse current source tree</a>
|
<a class="explicit" href="t/">Browse current source tree</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
% commits = repo.commits("refs/heads/" + repo.branch,
|
% commits = repo.commits("refs/heads/" + branch,
|
||||||
% limit = repo.info.commits_per_page,
|
% limit = repo.info.commits_per_page + 1,
|
||||||
% offset = repo.info.commits_per_page * offset)
|
% offset = repo.info.commits_per_page * offset)
|
||||||
% commits = list(commits)
|
% commits = list(commits)
|
||||||
|
|
||||||
@ -26,16 +29,21 @@
|
|||||||
% abort(404, "No more commits")
|
% abort(404, "No more commits")
|
||||||
% end
|
% end
|
||||||
|
|
||||||
|
% more = len(commits) > repo.info.commits_per_page
|
||||||
|
% if more:
|
||||||
|
% commits = commits[:-1]
|
||||||
|
% end
|
||||||
|
% more = more and offset + 1 < repo.info.max_pages
|
||||||
|
|
||||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
% include paginate more = more, offset = offset
|
||||||
|
|
||||||
% kwargs = dict(repo=repo, commits=commits,
|
% kwargs = dict(repo=repo, commits=commits,
|
||||||
% shorten=shorten, repo_root="../..")
|
% shorten=shorten, repo_root=relroot + "../..")
|
||||||
% include commit-list **kwargs
|
% include commit-list **kwargs
|
||||||
|
|
||||||
<p/>
|
<p/>
|
||||||
|
|
||||||
% include paginate nelem = len(commits), max_per_page = repo.info.commits_per_page, offset = offset
|
% include paginate more = more, offset = offset
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
% end
|
% end
|
||||||
% end
|
% end
|
||||||
|
|
||||||
<table class="nice commits">
|
<table class="nice commits" id="commits">
|
||||||
|
|
||||||
% refs = repo.refs()
|
% refs = repo.refs()
|
||||||
% if not defined("commits"):
|
% if not defined("commits"):
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
<title>git » {{repo.name}} » commit {{c.id[:7]}}</title>
|
<title>git » {{repo.name}} » commit {{c.id[:7]}}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
<link rel="stylesheet" type="text/css" href="../../../../static/git-arr.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../../static/syntax.css"/>
|
<link rel="stylesheet" type="text/css" href="../../../../static/syntax.css"/>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="commit">
|
<body class="commit">
|
||||||
@ -22,10 +22,10 @@
|
|||||||
<span class="date" title="{{c.author_date}}">
|
<span class="date" title="{{c.author_date}}">
|
||||||
{{c.author_date.utc}} UTC</span></td></tr>
|
{{c.author_date.utc}} UTC</span></td></tr>
|
||||||
<tr><td>committer</td>
|
<tr><td>committer</td>
|
||||||
<td><span class="name">{{c.author_name}}</span>
|
<td><span class="name">{{c.committer_name}}</span>
|
||||||
<span class="email"><{{c.author_email}}></span><br/>
|
<span class="email"><{{c.committer_email}}></span><br/>
|
||||||
<span class="date" title="{{c.author_date}}">
|
<span class="date" title="{{c.committer_date}}">
|
||||||
{{c.author_date.utc}} UTC</span></td></tr>
|
{{c.committer_date.utc}} UTC</span></td></tr>
|
||||||
|
|
||||||
% for p in c.parents:
|
% for p in c.parents:
|
||||||
<tr><td>parent</td>
|
<tr><td>parent</td>
|
||||||
@ -56,7 +56,9 @@
|
|||||||
<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,16 +1,17 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
<title>git</title>
|
<title>git</title>
|
||||||
<link rel="stylesheet" type="text/css" href="static/git-arr.css"/>
|
<link rel="stylesheet" type="text/css" href="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"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<script async src="static/git-arr.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="index">
|
<body class="index" onload="replace_timestamps()">
|
||||||
<h1>git</h1>
|
<h1>git</h1>
|
||||||
|
|
||||||
<table class="nice">
|
<table class="nice projects">
|
||||||
<tr>
|
<tr>
|
||||||
<th>project</th>
|
<th>project</th>
|
||||||
<th>description</th>
|
<th>description</th>
|
||||||
@ -18,8 +19,9 @@
|
|||||||
|
|
||||||
% for repo in sorted(repos.values(), key = lambda r: r.name):
|
% for repo in sorted(repos.values(), key = lambda r: r.name):
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
<td class="name"><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||||
<td><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
<td class="desc"><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
||||||
|
<td><span class="age">{{repo.last_commit_timestamp()}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
%end
|
%end
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<span class="inactive">← prev</span>
|
<span class="inactive">← prev</span>
|
||||||
% end
|
% end
|
||||||
<span class="sep">|</span>
|
<span class="sep">|</span>
|
||||||
% if nelem >= max_per_page:
|
% if more:
|
||||||
<a href="{{offset + 1}}.html">next →</a>
|
<a href="{{offset + 1}}.html">next →</a>
|
||||||
% else:
|
% else:
|
||||||
<span class="inactive">next →</span>
|
<span class="inactive">next →</span>
|
||||||
|
|||||||
8
views/patch.tpl
Normal file
8
views/patch.tpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
From: {{c.author_name}} <{{c.author_email}}>
|
||||||
|
Date: {{c.author_date}}
|
||||||
|
Subject: {{c.subject}}
|
||||||
|
|
||||||
|
{{c.body.strip()}}
|
||||||
|
---
|
||||||
|
|
||||||
|
{{c.diff.body}}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
<title>git » {{repo.name}}</title>
|
<title>git » {{repo.name}}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../static/git-arr.css"/>
|
<link rel="stylesheet" type="text/css" href="../../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"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<script async src="../../static/git-arr.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="summary">
|
<body class="summary">
|
||||||
@ -25,7 +26,7 @@
|
|||||||
% end
|
% end
|
||||||
% if repo.info.git_url:
|
% if repo.info.git_url:
|
||||||
<tr>
|
<tr>
|
||||||
<td class="category">repository</td>
|
<td class="category">git clone </td>
|
||||||
<td>{{! '<br/>'.join(repo.info.git_url.split())}}</td>
|
<td>{{! '<br/>'.join(repo.info.git_url.split())}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% end
|
% end
|
||||||
@ -35,19 +36,25 @@
|
|||||||
% end
|
% end
|
||||||
|
|
||||||
% if "master" in repo.branch_names():
|
% if "master" in repo.branch_names():
|
||||||
|
<div class="toggable-title" onclick="toggle('commits')">
|
||||||
|
<a href="b/master/">commits (master)</a>
|
||||||
|
</div>
|
||||||
% kwargs = dict(repo = repo, start_ref = "refs/heads/master",
|
% kwargs = dict(repo = repo, start_ref = "refs/heads/master",
|
||||||
% limit = repo.info.commits_in_summary,
|
% limit = repo.info.commits_in_summary,
|
||||||
% shorten = shorten, repo_root = ".", offset = 0)
|
% shorten = shorten, repo_root = ".", offset = 0)
|
||||||
% include commit-list **kwargs
|
% include commit-list **kwargs
|
||||||
|
<hr/>
|
||||||
|
<div class="toggable-title" onclick="toggle('ls')">
|
||||||
|
<a href="b/master/t/">tree (master)</a>
|
||||||
|
</div>
|
||||||
|
% kwargs = dict(repo = repo, tree=repo.tree("master"),
|
||||||
|
% treeroot="b/master/t", dirname=smstr.from_url(""))
|
||||||
|
% include tree-list **kwargs
|
||||||
|
<hr/>
|
||||||
% end
|
% end
|
||||||
|
|
||||||
<hr/>
|
<div class="toggable-title" onclick="toggle('branches')">branches</div>
|
||||||
|
<table class="nice toggable" id="branches">
|
||||||
<table class="nice">
|
|
||||||
<tr>
|
|
||||||
<th>branches</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
% for b in repo.branch_names():
|
% for b in repo.branch_names():
|
||||||
<tr>
|
<tr>
|
||||||
<td class="main"><a href="b/{{b}}/">{{b}}</a></td>
|
<td class="main"><a href="b/{{b}}/">{{b}}</a></td>
|
||||||
@ -63,11 +70,8 @@
|
|||||||
|
|
||||||
% tags = list(repo.tags())
|
% tags = list(repo.tags())
|
||||||
% if tags:
|
% if tags:
|
||||||
<table class="nice">
|
<div class="toggable-title" onclick="toggle('tags')">tags</div>
|
||||||
<tr>
|
<table class="nice toggable" id="tags">
|
||||||
<th>tags</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
% for name, obj_id in tags:
|
% for name, obj_id in tags:
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="c/{{obj_id}}/">{{name}}</a></td>
|
<td><a href="c/{{obj_id}}/">{{name}}</a></td>
|
||||||
|
|||||||
16
views/tree-list.html
Normal file
16
views/tree-list.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<table class="nice toggable ls" id="ls">
|
||||||
|
% key_func = lambda x: (x[0] != 'tree', x[1].raw)
|
||||||
|
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
||||||
|
<tr class="{{type}}">
|
||||||
|
% if type == "blob":
|
||||||
|
<td class="name"><a href="{{treeroot}}/f={{name.url}}.html">
|
||||||
|
{{!name.html}}</a></td>
|
||||||
|
<td class="size">{{size}}</td>
|
||||||
|
% elif type == "tree":
|
||||||
|
<td class="name">
|
||||||
|
<a class="explicit" href="{{treeroot}}/{{name.url}}/">
|
||||||
|
{{!name.html}}/</a></td>
|
||||||
|
% end
|
||||||
|
</tr>
|
||||||
|
% end
|
||||||
|
</table>
|
||||||
@ -1,54 +1,43 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
<!DOCTYPE html>
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
% if not dirname.raw:
|
% if not dirname.raw:
|
||||||
% relroot = './'
|
% reltree = './'
|
||||||
% else:
|
% else:
|
||||||
% relroot = '../' * (len(dirname.split('/')) - 1)
|
% reltree = '../' * (len(dirname.split('/')) - 1)
|
||||||
% end
|
% end
|
||||||
|
% relroot = reltree + '../' * (len(branch.split('/')) - 1)
|
||||||
|
|
||||||
<title>git » {{repo.name}} »
|
<title>git » {{repo.name}} »
|
||||||
{{repo.branch}} » {{dirname.unicode}}</title>
|
{{branch}} » {{dirname.raw if dirname.raw 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"/>
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="tree">
|
<body class="tree">
|
||||||
<h1><a href="{{relroot}}../../../../../">git</a> »
|
<h1><a href="{{relroot}}../../../../../">git</a> »
|
||||||
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
<a href="{{relroot}}../../../">{{repo.name}}</a> »
|
||||||
<a href="{{relroot}}../">{{repo.branch}}</a> »
|
<a href="{{reltree}}../">{{branch}}</a> »
|
||||||
<a href="{{relroot}}">tree</a>
|
<a href="{{reltree}}">tree</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{{relroot}}">[{{repo.branch}}]</a> /
|
<a href="{{reltree}}">[{{branch}}]</a> /
|
||||||
% base = smstr(relroot)
|
% base = smstr(reltree)
|
||||||
% for c in dirname.split('/'):
|
% for c in dirname.split('/'):
|
||||||
% if not c.raw: continue
|
% if not c.raw:
|
||||||
<a href="{{base.url}}{{c.url}}/">{{c.unicode}}</a> /
|
% continue
|
||||||
|
% end
|
||||||
|
<a href="{{base.url}}{{c.url}}/">{{c.raw}}</a> /
|
||||||
% base += c + '/'
|
% base += c + '/'
|
||||||
% end
|
% end
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<table class="nice ls">
|
% kwargs = dict(repo = repo, tree=tree, treeroot=".")
|
||||||
% key_func = lambda (t, n, s): (0 if t == 'tree' else 1, n.raw)
|
% include tree-list **kwargs
|
||||||
% for type, name, size in sorted(tree.ls(dirname.raw), key = key_func):
|
|
||||||
<tr class="{{type}}">
|
|
||||||
% if type == "blob":
|
|
||||||
<td class="name"><a href="./f={{name.url}}.html">
|
|
||||||
{{!name.html}}</a></td>
|
|
||||||
<td class="size">{{size}}</td>
|
|
||||||
% elif type == "tree":
|
|
||||||
<td class="name">
|
|
||||||
<a class="explicit" href="./{{name.url}}/">
|
|
||||||
{{!name.html}}/</a></td>
|
|
||||||
% end
|
|
||||||
</tr>
|
|
||||||
% end
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user