Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f65291ef1 | ||
|
|
f6a75820e8 | ||
|
|
e49c69da2e | ||
|
|
6764bfcfd6 | ||
|
|
54026b7585 | ||
|
|
a42d7da6a4 | ||
|
|
21522f8a3a | ||
|
|
f62ca211eb | ||
|
|
d3bf98ea00 |
23
git-arr
23
git-arr
@@ -55,6 +55,8 @@ def load_config(path):
|
||||
'web_url_file': 'web_url',
|
||||
'git_url': '',
|
||||
'git_url_file': 'cloneurl',
|
||||
'embed_markdown': 'yes',
|
||||
'embed_images': 'no',
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
@@ -115,6 +117,9 @@ def load_config(path):
|
||||
if not r.info.git_url and os.path.isfile(git_url_file):
|
||||
r.info.git_url = open(git_url_file).read()
|
||||
|
||||
r.info.embed_markdown = config.getboolean(s, 'embed_markdown')
|
||||
r.info.embed_images = config.getboolean(s, 'embed_images')
|
||||
|
||||
repos[r.name] = r
|
||||
|
||||
def find_git_dir(path):
|
||||
@@ -174,6 +179,10 @@ def with_utils(f):
|
||||
'can_colorize': utils.can_colorize,
|
||||
'colorize_diff': utils.colorize_diff,
|
||||
'colorize_blob': utils.colorize_blob,
|
||||
'can_markdown': utils.can_markdown,
|
||||
'markdown_blob': utils.markdown_blob,
|
||||
'can_embed_image': utils.can_embed_image,
|
||||
'embed_image_blob': utils.embed_image_blob,
|
||||
'abort': bottle.abort,
|
||||
'smstr': git.smstr,
|
||||
}
|
||||
@@ -260,6 +269,16 @@ def static(path):
|
||||
# Static HTML generation
|
||||
#
|
||||
|
||||
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, skip_index = False):
|
||||
"""Generate static html to the output directory."""
|
||||
def write_to(path, func_or_str, args = (), mtime = None):
|
||||
@@ -343,6 +362,8 @@ def generate(output, skip_index = False):
|
||||
read_f = lambda f: open(f).read()
|
||||
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
||||
os.stat(static_path + '/git-arr.css').st_mtime)
|
||||
write_to('static/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)
|
||||
|
||||
@@ -385,7 +406,7 @@ def generate(output, skip_index = False):
|
||||
# Some repos can have tags pointing to non-commits. This
|
||||
# happens in the Linux Kernel's v2.6.11, which points directly
|
||||
# to a tree. Ignore them.
|
||||
if e.status == 404:
|
||||
if is_404(e):
|
||||
print('404 in tag %s (%s)' % (tag_name, obj_id))
|
||||
else:
|
||||
raise
|
||||
|
||||
26
git.py
26
git.py
@@ -207,11 +207,13 @@ class Repo:
|
||||
"""Returns a GitCommand() on our path."""
|
||||
return GitCommand(self.path, cmd)
|
||||
|
||||
def for_each_ref(self, pattern = None, sort = None):
|
||||
def for_each_ref(self, pattern = None, sort = None, count = None):
|
||||
"""Returns a list of references."""
|
||||
cmd = self.cmd('for-each-ref')
|
||||
if sort:
|
||||
cmd.sort = sort
|
||||
if count:
|
||||
cmd.count = count
|
||||
if pattern:
|
||||
cmd.arg(pattern)
|
||||
|
||||
@@ -325,7 +327,7 @@ class Repo:
|
||||
ref = self.branch
|
||||
return Tree(self, ref)
|
||||
|
||||
def blob(self, path, ref = None):
|
||||
def blob(self, path, ref = None, raw = False):
|
||||
"""Returns the contents of the given path."""
|
||||
if not ref:
|
||||
ref = self.branch
|
||||
@@ -341,8 +343,21 @@ class Repo:
|
||||
if not head or head.strip().endswith('missing'):
|
||||
return None
|
||||
|
||||
# Raw option in case we need a binary blob and not a utf-8 encoded one.
|
||||
if raw:
|
||||
return out.fd.read()
|
||||
|
||||
return out.read()
|
||||
|
||||
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):
|
||||
"""A git commit."""
|
||||
@@ -397,7 +412,12 @@ class Commit (object):
|
||||
@staticmethod
|
||||
def from_str(repo, buf):
|
||||
"""Parses git rev-list output, returns a commit object."""
|
||||
header, raw_message = buf.split('\n\n', 1)
|
||||
if '\n\n' in buf:
|
||||
# Header, commit message
|
||||
header, raw_message = buf.split('\n\n', 1)
|
||||
else:
|
||||
# Header only, no commit message
|
||||
header, raw_message = buf.rstrip(), ' '
|
||||
|
||||
header_lines = header.split('\n')
|
||||
commit_id = header_lines.pop(0)
|
||||
|
||||
@@ -100,6 +100,26 @@ span.tag {
|
||||
background-color: #ffff88;
|
||||
}
|
||||
|
||||
/* Age of an object.
|
||||
* Note this is hidden by default as we rely on javascript to show it. */
|
||||
span.age {
|
||||
display: none;
|
||||
color: gray;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
span.age-band0 {
|
||||
color: darkgreen;
|
||||
}
|
||||
|
||||
span.age-band1 {
|
||||
color: green;
|
||||
}
|
||||
|
||||
span.age-band2 {
|
||||
color: seagreen;
|
||||
}
|
||||
|
||||
/* Commit message and diff. */
|
||||
pre.commit-message {
|
||||
font-size: large;
|
||||
|
||||
63
static/git-arr.js
Normal file
63
static/git-arr.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/* 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
39
utils.py
39
utils.py
@@ -12,6 +12,13 @@ try:
|
||||
except ImportError:
|
||||
pygments = None
|
||||
|
||||
try:
|
||||
import markdown
|
||||
except ImportError:
|
||||
markdown = None
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
|
||||
def shorten(s, width = 60):
|
||||
if len(s) < 60:
|
||||
@@ -41,6 +48,24 @@ def can_colorize(s):
|
||||
|
||||
return True
|
||||
|
||||
def can_markdown(repo, fname):
|
||||
"""True if we can process file through markdown, False otherwise."""
|
||||
if markdown is None:
|
||||
return False
|
||||
|
||||
if not repo.info.embed_markdown:
|
||||
return False
|
||||
|
||||
return fname.endswith(".md")
|
||||
|
||||
def can_embed_image(repo, fname):
|
||||
"""True if we can embed image file in HTML, False otherwise."""
|
||||
if not repo.info.embed_images:
|
||||
return False
|
||||
|
||||
return (('.' in fname) and
|
||||
(fname.split('.')[-1].lower() in [ 'jpg', 'jpeg', 'png', 'gif' ]))
|
||||
|
||||
def colorize_diff(s):
|
||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
@@ -68,3 +93,17 @@ def colorize_blob(fname, s):
|
||||
|
||||
return highlight(s, lexer, formatter)
|
||||
|
||||
def markdown_blob(s):
|
||||
return markdown.markdown(s)
|
||||
|
||||
def embed_image_blob(repo, dirname, fname):
|
||||
mimetype = mimetypes.guess_type(fname)[0]
|
||||
|
||||
# Unfortunately, bottle seems to require utf-8 encoded data.
|
||||
# We have to refetch the blob with raw=True, because the utf-8 encoded
|
||||
# version of the blob available in the bottle template discards binary data.
|
||||
raw_blob = repo.blob(dirname + fname, raw = True)
|
||||
|
||||
return '<img style="max-width:100%;" src="data:{0};base64,{1}" />'.format( \
|
||||
mimetype, base64.b64encode(raw_blob))
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@
|
||||
<a href="">{{!fname.html}}</a>
|
||||
</h3>
|
||||
|
||||
% if can_colorize(blob):
|
||||
% if can_embed_image(repo, fname.unicode):
|
||||
{{!embed_image_blob(repo, dirname.raw, fname.raw)}}
|
||||
% elif can_markdown(repo, fname.unicode):
|
||||
{{!markdown_blob(blob)}}
|
||||
% elif can_colorize(blob):
|
||||
{{!colorize_blob(fname.unicode, blob)}}
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
<span class="date" title="{{c.author_date}}">
|
||||
{{c.author_date.utc}} UTC</span></td></tr>
|
||||
<tr><td>committer</td>
|
||||
<td><span class="name">{{c.author_name}}</span>
|
||||
<span class="email"><{{c.author_email}}></span><br/>
|
||||
<span class="date" title="{{c.author_date}}">
|
||||
{{c.author_date.utc}} UTC</span></td></tr>
|
||||
<td><span class="name">{{c.committer_name}}</span>
|
||||
<span class="email"><{{c.committer_email}}></span><br/>
|
||||
<span class="date" title="{{c.committer_date}}">
|
||||
{{c.committer_date.utc}} UTC</span></td></tr>
|
||||
|
||||
% for p in c.parents:
|
||||
<tr><td>parent</td>
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
<title>git</title>
|
||||
<link rel="stylesheet" type="text/css" href="static/git-arr.css"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<script src="static/git-arr.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="index">
|
||||
<body class="index" onload="replace_timestamps()">
|
||||
<h1>git</h1>
|
||||
|
||||
<table class="nice">
|
||||
@@ -20,6 +21,7 @@
|
||||
<tr>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
|
||||
<td><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
|
||||
<td><span class="age">{{repo.last_commit_timestamp()}}</span></td>
|
||||
</tr>
|
||||
%end
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user