Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f5f3c4aa5 | ||
|
|
c72278c97c | ||
|
|
18e8599bfa | ||
|
|
c303c30755 | ||
|
|
9ec2bde5c4 | ||
|
|
36db9cc0ee | ||
|
|
bad8c52ef2 | ||
|
|
62da3ebc08 | ||
|
|
ba3b2132f5 | ||
|
|
1c729578b2 |
15
README
15
README
@@ -19,6 +19,13 @@ 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.
|
||||
|
||||
@@ -41,9 +48,9 @@ use, by running:
|
||||
That can be useful when making changes to the software itself.
|
||||
|
||||
|
||||
Where to report bugs
|
||||
--------------------
|
||||
Contact
|
||||
-------
|
||||
|
||||
If you want to report bugs, or have any questions or comments, just let me
|
||||
know at albertito@blitiri.com.ar.
|
||||
If you want to report bugs, send patches, or have any questions or comments,
|
||||
just let me know at albertito@blitiri.com.ar.
|
||||
|
||||
|
||||
83
git-arr
83
git-arr
@@ -5,6 +5,7 @@ git-arr: A git web html generator.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import optparse
|
||||
@@ -20,6 +21,18 @@ import git
|
||||
import utils
|
||||
|
||||
|
||||
# Tell bottle where to find the views.
|
||||
# Note this assumes they live next to the executable, and that is not a good
|
||||
# assumption; but it's good enough for now.
|
||||
bottle.TEMPLATE_PATH.insert(
|
||||
0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/views/')
|
||||
|
||||
# The path to our static files.
|
||||
# Note this assumes they live next to the executable, and that is not a good
|
||||
# assumption; but it's good enough for now.
|
||||
static_path = os.path.abspath(os.path.dirname(sys.argv[0])) + '/static/'
|
||||
|
||||
|
||||
# The list of repositories is a global variable for convenience. It will be
|
||||
# populated by load_config().
|
||||
repos = {}
|
||||
@@ -41,7 +54,7 @@ def load_config(path):
|
||||
'web_url': '',
|
||||
'web_url_file': 'web_url',
|
||||
'git_url': '',
|
||||
'git_url_file': 'git_url',
|
||||
'git_url_file': 'cloneurl',
|
||||
}
|
||||
|
||||
config = configparser.SafeConfigParser(defaults)
|
||||
@@ -49,14 +62,10 @@ def load_config(path):
|
||||
|
||||
# Do a first pass for general sanity checking and recursive expansion.
|
||||
for s in config.sections():
|
||||
if not config.has_option(s, 'path'):
|
||||
raise configparser.NoOptionError(
|
||||
'%s is missing the mandatory path' % s)
|
||||
|
||||
if config.getboolean(s, 'recursive'):
|
||||
for path in os.listdir(config.get(s, 'path')):
|
||||
fullpath = config.get(s, 'path') + '/' + path
|
||||
if not os.path.exists(fullpath + '/HEAD'):
|
||||
fullpath = find_git_dir(config.get(s, 'path') + '/' + path)
|
||||
if not fullpath:
|
||||
continue
|
||||
|
||||
if os.path.exists(fullpath + '/disable_gitweb'):
|
||||
@@ -76,7 +85,13 @@ def load_config(path):
|
||||
config.remove_section(s)
|
||||
|
||||
for s in config.sections():
|
||||
fullpath = config.get(s, 'path')
|
||||
fullpath = find_git_dir(config.get(s, 'path'))
|
||||
if not fullpath:
|
||||
raise ValueError(
|
||||
'%s: path %s is not a valid git repository' % (
|
||||
s, config.get(s, 'path')))
|
||||
|
||||
config.set(s, 'path', fullpath)
|
||||
config.set(s, 'name', s)
|
||||
|
||||
desc = config.get(s, 'desc')
|
||||
@@ -102,6 +117,29 @@ def load_config(path):
|
||||
|
||||
repos[r.name] = r
|
||||
|
||||
def find_git_dir(path):
|
||||
"""Returns the path to the git directory for the given repository.
|
||||
|
||||
This function takes a path to a git repository, and returns the path to
|
||||
its git directory. If the repo is bare, it will be the same path;
|
||||
otherwise it will be path + '.git/'.
|
||||
|
||||
An empty string is returned if the given path is not a valid repository.
|
||||
"""
|
||||
def check(p):
|
||||
"""A dirty check for whether this is a git dir or not."""
|
||||
# Note silent stderr because we expect this to fail and don't want the
|
||||
# noise; and also we strip the final \n from the output.
|
||||
return git.run_git(p,
|
||||
['rev-parse', '--git-dir'],
|
||||
silent_stderr = True).read()[:-1]
|
||||
|
||||
for p in [ path, path + '/.git' ]:
|
||||
if check(p):
|
||||
return p
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def repo_filter(unused_conf):
|
||||
"""Bottle route filter for repos."""
|
||||
@@ -133,7 +171,7 @@ def with_utils(f):
|
||||
"""
|
||||
utilities = {
|
||||
'shorten': utils.shorten,
|
||||
'has_colorizer': utils.has_colorizer,
|
||||
'can_colorize': utils.can_colorize,
|
||||
'colorize_diff': utils.colorize_diff,
|
||||
'colorize_blob': utils.colorize_blob,
|
||||
'abort': bottle.abort,
|
||||
@@ -215,14 +253,14 @@ def blob(repo, bname, fname, dirname = ''):
|
||||
|
||||
@bottle.route('/static/<path:path>')
|
||||
def static(path):
|
||||
return bottle.static_file(path, root = './static/')
|
||||
return bottle.static_file(path, root = static_path)
|
||||
|
||||
|
||||
#
|
||||
# Static HTML generation
|
||||
#
|
||||
|
||||
def generate(output):
|
||||
def generate(output, skip_index = False):
|
||||
"""Generate static html to the output directory."""
|
||||
def write_to(path, func_or_str, args = (), mtime = None):
|
||||
path = output + '/' + path
|
||||
@@ -298,14 +336,15 @@ def generate(output):
|
||||
(str(r.name), str(bn), oname.raw),
|
||||
tree, (r, bn, oname.url), mtime)
|
||||
|
||||
write_to('index.html', index())
|
||||
if not skip_index:
|
||||
write_to('index.html', index())
|
||||
|
||||
# We can't call static() because it relies on HTTP headers.
|
||||
read_f = lambda f: open(f).read()
|
||||
write_to('static/git-arr.css', read_f, ['static/git-arr.css'],
|
||||
os.stat('static/git-arr.css').st_mtime)
|
||||
write_to('static/syntax.css', read_f, ['static/syntax.css'],
|
||||
os.stat('static/syntax.css').st_mtime)
|
||||
write_to('static/git-arr.css', read_f, [static_path + '/git-arr.css'],
|
||||
os.stat(static_path + '/git-arr.css').st_mtime)
|
||||
write_to('static/syntax.css', read_f, [static_path + '/syntax.css'],
|
||||
os.stat(static_path + '/syntax.css').st_mtime)
|
||||
|
||||
for r in sorted(repos.values(), key = lambda r: r.name):
|
||||
write_to('r/%s/index.html' % r.name, summary(r))
|
||||
@@ -359,6 +398,7 @@ def main():
|
||||
parser.add_option('-o', '--output', metavar = 'DIR',
|
||||
help = 'output directory (for generate)')
|
||||
parser.add_option('', '--only', metavar = 'REPO', action = 'append',
|
||||
default = [],
|
||||
help = 'generate/serve only this repository')
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
@@ -367,22 +407,25 @@ def main():
|
||||
|
||||
try:
|
||||
load_config(opts.config)
|
||||
except configparser.NoOptionError as e:
|
||||
except (configparser.NoOptionError, ValueError) as e:
|
||||
print('Error parsing config:', e)
|
||||
return
|
||||
|
||||
if not args:
|
||||
parser.error('Must specify an action (serve|generate)')
|
||||
|
||||
if opts.only:
|
||||
global repos
|
||||
repos = [ r for r in repos if r.name in opts.only ]
|
||||
for rname in list(repos.keys()):
|
||||
if rname not in opts.only:
|
||||
del repos[rname]
|
||||
|
||||
if args[0] == 'serve':
|
||||
bottle.run(host = 'localhost', port = 8008, reloader = True)
|
||||
elif args[0] == 'generate':
|
||||
if not opts.output:
|
||||
parser.error('Must specify --output')
|
||||
generate(output = opts.output)
|
||||
generate(output = opts.output,
|
||||
skip_index = len(opts.only) > 0)
|
||||
else:
|
||||
parser.error('Unknown action %s' % args[0])
|
||||
|
||||
|
||||
12
git.py
12
git.py
@@ -41,7 +41,7 @@ class EncodeWrapper:
|
||||
return s.decode(self.encoding, errors = self.errors)
|
||||
|
||||
|
||||
def run_git(repo_path, params, stdin = None):
|
||||
def run_git(repo_path, params, stdin = None, silent_stderr = False):
|
||||
"""Invokes git with the given parameters.
|
||||
|
||||
This function invokes git with the given parameters, and returns a
|
||||
@@ -49,11 +49,17 @@ def run_git(repo_path, params, stdin = None):
|
||||
"""
|
||||
params = [GIT_BIN, '--git-dir=%s' % repo_path] + list(params)
|
||||
|
||||
stderr = None
|
||||
if silent_stderr:
|
||||
stderr = subprocess.PIPE
|
||||
|
||||
if not stdin:
|
||||
p = subprocess.Popen(params, stdin = None, stdout = subprocess.PIPE)
|
||||
p = subprocess.Popen(params,
|
||||
stdin = None, stdout = subprocess.PIPE, stderr = stderr)
|
||||
else:
|
||||
p = subprocess.Popen(params,
|
||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE)
|
||||
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
|
||||
stderr = stderr)
|
||||
p.stdin.write(stdin)
|
||||
p.stdin.close()
|
||||
|
||||
|
||||
14
hooks/README
Normal file
14
hooks/README
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
You can use the post-receive hook to automatically generate the repository
|
||||
view after a push.
|
||||
|
||||
To do so, configure in your target repository the following options:
|
||||
|
||||
$ git config hooks.git-arr-config /path/to/site.conf
|
||||
$ git config hooks.git-arr-output /var/www/git/
|
||||
|
||||
# Only if the git-arr executable is not on your $PATH.
|
||||
$ git config hooks.git-arr-path /path/to/git-arr
|
||||
|
||||
Then copy the post-receive file to the "hooks" directory in your repository.
|
||||
|
||||
58
hooks/post-receive
Executable file
58
hooks/post-receive
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# git-arr post-receive hook
|
||||
#
|
||||
# This is a script intended to be used as a post-receive hook, which updates
|
||||
# its git-arr view.
|
||||
#
|
||||
# You should place it /path/to/your/repository.git/hooks/.
|
||||
|
||||
# Config
|
||||
# --------
|
||||
#
|
||||
# hooks.git-arr-config
|
||||
# The git-arr configuration file to use. Mandatory.
|
||||
# Example: /srv/git-arr/site.conf
|
||||
#
|
||||
# hooks.git-arr-output
|
||||
# Directory for the generated output. Mandatory.
|
||||
# Example: /srv/www/git/
|
||||
#
|
||||
# hooks.git-arr-path
|
||||
# The path to the git-arr executable. Optional, defaults to "git-arr".
|
||||
#
|
||||
# hooks.git-arr-repo-name
|
||||
# The git-arr repository name. Optional, defaults to the path name.
|
||||
|
||||
git_arr_config="$(git config --path hooks.git-arr-config)"
|
||||
git_arr_output="$(git config --path hooks.git-arr-output)"
|
||||
|
||||
git_arr_path="$(git config --path hooks.git-arr-path 2> /dev/null)"
|
||||
git_arr_repo_name="$(git config hooks.git-arr-repo-name 2> /dev/null)"
|
||||
|
||||
if [ -z "$git_arr_config" -o -z "$git_arr_output" ]; then
|
||||
echo "Error: missing config options."
|
||||
echo "Both hooks.git-arr-config and hooks.git-arr-output must be set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$git_arr_path" ]; then
|
||||
git_arr_path=git-arr
|
||||
fi
|
||||
|
||||
if [ -z "$git_arr_repo_name" ]; then
|
||||
PARENT_DIR=$(cd $(dirname "$0")/..; echo "$PWD")
|
||||
git_arr_repo_name=$(basename "$PARENT_DIR")
|
||||
fi
|
||||
|
||||
echo "Running git-arr"
|
||||
$git_arr_path --config "$git_arr_config" generate \
|
||||
--output "$git_arr_output" \
|
||||
--only "$git_arr_repo_name" > /dev/null
|
||||
RESULT=$?
|
||||
|
||||
if [ $RESULT -ne 0 ]; then
|
||||
echo "Error running git-arr"
|
||||
exit $RESULT
|
||||
fi
|
||||
|
||||
@@ -38,8 +38,8 @@ path = /srv/git/repo/
|
||||
|
||||
# File name to get the git URLs from (optional).
|
||||
# If git_url is not set, attempt to get its value from this file.
|
||||
# Default: "git_url"
|
||||
#git_url_file = git_url
|
||||
# Default: "cloneurl" (same as gitweb).
|
||||
#git_url_file = cloneurl
|
||||
|
||||
# Do we look for repositories within this path? (optional).
|
||||
# This option enables a recursive, 1 level search for repositories within the
|
||||
|
||||
35
utils.py
35
utils.py
@@ -18,8 +18,28 @@ def shorten(s, width = 60):
|
||||
return s
|
||||
return s[:57] + "..."
|
||||
|
||||
def has_colorizer():
|
||||
return pygments is not None
|
||||
def can_colorize(s):
|
||||
"""True if we can colorize the string, False otherwise."""
|
||||
if pygments is None:
|
||||
return False
|
||||
|
||||
# Pygments can take a huge amount of time with long files, or with very
|
||||
# long lines; these are heuristics to try to avoid those situations.
|
||||
if len(s) > (512 * 1024):
|
||||
return False
|
||||
|
||||
# If any of the first 5 lines is over 300 characters long, don't colorize.
|
||||
start = 0
|
||||
for i in range(5):
|
||||
pos = s.find('\n', start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
if pos - start > 300:
|
||||
return False
|
||||
start = pos + 1
|
||||
|
||||
return True
|
||||
|
||||
def colorize_diff(s):
|
||||
lexer = lexers.DiffLexer(encoding = 'utf-8')
|
||||
@@ -30,9 +50,18 @@ def colorize_diff(s):
|
||||
|
||||
def colorize_blob(fname, s):
|
||||
try:
|
||||
lexer = lexers.guess_lexer_for_filename(fname, s)
|
||||
lexer = lexers.guess_lexer_for_filename(fname, s, encoding = 'utf-8')
|
||||
except lexers.ClassNotFound:
|
||||
# Only try to guess lexers if the file starts with a shebang,
|
||||
# otherwise it's likely a text file and guess_lexer() is prone to
|
||||
# make mistakes with those.
|
||||
lexer = lexers.TextLexer(encoding = 'utf-8')
|
||||
if s.startswith('#!'):
|
||||
try:
|
||||
lexer = lexers.guess_lexer(s[:80], encoding = 'utf-8')
|
||||
except lexers.ClassNotFound:
|
||||
pass
|
||||
|
||||
formatter = HtmlFormatter(encoding = 'utf-8',
|
||||
cssclass = 'source_code',
|
||||
linenos = 'table')
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<a href="">{{!fname.html}}</a>
|
||||
</h3>
|
||||
|
||||
% if has_colorizer():
|
||||
% if can_colorize(blob):
|
||||
{{!colorize_blob(fname.unicode, blob)}}
|
||||
% else:
|
||||
<pre class="blob-body">
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<hr/>
|
||||
|
||||
% if has_colorizer():
|
||||
% if can_colorize(c.diff.body):
|
||||
{{!colorize_diff(c.diff.body)}}
|
||||
% else:
|
||||
<pre class="diff-body">
|
||||
|
||||
Reference in New Issue
Block a user