klaus-1.4.0/0000755000076500000240000000000013464301406012210 5ustar jstaff00000000000000klaus-1.4.0/MANIFEST.in0000644000076500000240000000012513371602040013737 0ustar jstaff00000000000000recursive-include klaus/static * recursive-include klaus/templates * include klaus.1 klaus-1.4.0/PKG-INFO0000644000076500000240000000134013464301406013303 0ustar jstaff00000000000000Metadata-Version: 1.1 Name: klaus Version: 1.4.0 Summary: The first Git web viewer that Just Works™. Home-page: https://github.com/jonashaag/klaus Author: Jonas Haag Author-email: jonas@lophus.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Software Development :: Version Control Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 klaus-1.4.0/README.rst0000644000076500000240000000570113371602040013675 0ustar jstaff00000000000000|travis-badge| |gitter-badge| .. |travis-badge| image:: https://travis-ci.org/jonashaag/klaus.svg?branch=master :target: https://travis-ci.org/jonashaag/klaus .. |gitter-badge| image:: https://badges.gitter.im/Join%20Chat.svg :alt: Join the chat at https://gitter.im/jonashaag/klaus :target: https://gitter.im/jonashaag/klaus?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge klaus: a simple, easy-to-set-up Git web viewer that Just Works™. ================================================================ (If it doesn't Just Work for you, please file a bug.) * Super easy to set up -- no configuration required * Supports Python 2 and Python 3 * Syntax highlighting * Git Smart HTTP support * Code navigation using Exuberant ctags :Demo: http://klausdemo.lophus.org :Mailing list: http://groups.google.com/group/klaus-users :On PyPI: http://pypi.python.org/pypi/klaus/ :Wiki: https://github.com/jonashaag/klaus/wiki :License: ISC (BSD) Contributing ------------ Please do it! I'm equally happy with bug reports/feature ideas and code contributions. If you have any questions/issues, I'm happy to help! For starters, `here are a few ideas what to work on. `_ :-) |img1|_ |img2|_ |img3|_ .. |img1| image:: https://i.imgur.com/2XhZIgw.png .. |img2| image:: https://i.imgur.com/6LjC8Cl.png .. |img3| image:: https://i.imgur.com/EYJdQwv.png .. _img1: https://i.imgur.com/MV3uFvw.png .. _img2: https://i.imgur.com/9HEZ3ro.png .. _img3: https://i.imgur.com/kx2HaTq.png Installation ------------ :: pip install klaus (Optional dependencies: see `Markup rendering `_ in the wiki.) Usage ----- See also: `Klaus wiki `_ Using the ``klaus`` script ^^^^^^^^^^^^^^^^^^^^^^^^^^ **NOTE:** This is intended for testing/low-traffic local installations *only*! The `klaus` script uses wsgiref_ internally which doesn't scale *at all* (in fact it's single-threaded and non-asynchronous). To run klaus using the default options:: klaus [repo1 [repo2 ...]] For more options, see:: klaus --help Using a real server ^^^^^^^^^^^^^^^^^^^ The ``klaus`` module contains a ``make_app`` function which returns a WSGI app. An example WSGI helper script is provided with klaus (see ``klaus/contrib/wsgi.py``), configuration being read from environment variables. Use it like this (uWSGI example):: uwsgi -w klaus.contrib.wsgi \ --env KLAUS_SITE_NAME="Klaus Demo" \ --env KLAUS_REPOS="/path/to/repo1 /path/to/repo2 ..." \ ... Gunicorn example:: gunicorn --env KLAUS_SITE_NAME="Klaus Demo" \ --env KLAUS_REPOS="/path/to/repo1 /path/to/repo2 ..." \ klaus.contrib.wsgi See also `deployment section in the wiki `_. .. _wsgiref: http://docs.python.org/library/wsgiref.html klaus-1.4.0/bin/0000755000076500000240000000000013464301406012760 5ustar jstaff00000000000000klaus-1.4.0/bin/klaus0000755000076500000240000001000312726757501014032 0ustar jstaff00000000000000#!/usr/bin/env python # coding: utf-8 from __future__ import print_function import sys import os import argparse import webbrowser from dulwich.errors import NotGitRepository from dulwich.repo import Repo from klaus import make_app, KLAUS_VERSION from klaus.utils import force_unicode def git_repository(path): path = os.path.abspath(path) if not os.path.exists(path): raise argparse.ArgumentTypeError('%r: No such directory' % path) try: Repo(path) except NotGitRepository: raise argparse.ArgumentTypeError('%r: Not a Git repository' % path) return path def make_parser(): parser = argparse.ArgumentParser() parser.add_argument('--host', help="default: 127.0.0.1", default='127.0.0.1') parser.add_argument('--port', help="default: 8080", default=8080, type=int) parser.add_argument('--site-name', help="site name showed in header. default: your hostname") parser.add_argument('--version', help='print version number', action='store_true') parser.add_argument('-b', '--browser', help="open klaus in a browser on server start", default=False, action='store_true') parser.add_argument('-B', '--with-browser', help="specify which browser to use with --browser", metavar='BROWSER', default=None) parser.add_argument('--ctags', help="enable ctags for which revisions? default: none. " "WARNING: Don't use 'ALL' for public servers!", choices=['none', 'tags-and-branches', 'ALL'], default='none') parser.add_argument('repos', help='repositories to serve', metavar='DIR', nargs='*', type=git_repository) grp = parser.add_argument_group("Git Smart HTTP") grp.add_argument('--smarthttp', help="enable Git Smart HTTP serving", action='store_true') grp.add_argument('--htdigest', help="use credentials from FILE", metavar="FILE", type=argparse.FileType('r')) grp = parser.add_argument_group("Development flags", "DO NOT USE IN PRODUCTION!") grp.add_argument('--debug', help="Enable Werkzeug debugger and reloader", action='store_true') return parser def main(): args = make_parser().parse_args() if args.version: print(KLAUS_VERSION) return 0 if args.htdigest and not args.smarthttp: print("ERROR: --htdigest option has no effect without --smarthttp enabled", file=sys.stderr) return 1 if not args.repos: print("WARNING: No repositories supplied -- syntax is 'klaus dir1 dir2...'.", file=sys.stderr) if not args.site_name: args.site_name = '%s:%d' % (args.host, args.port) if args.ctags != 'none': from klaus.ctagsutils import check_have_exuberant_ctags if not check_have_exuberant_ctags(): print("ERROR: Exuberant ctags not installed (or 'ctags' binary isn't *Exuberant* ctags)", file=sys.stderr) return 1 try: import ctags except ImportError: raise ImportError("Please install 'python-ctags3' to enable ctags support.") app = make_app( args.repos, force_unicode(args.site_name or args.host), args.smarthttp, args.htdigest, ctags_policy=args.ctags, ) if args.browser: _open_browser(args) app.run(args.host, args.port, args.debug) def _open_browser(args): # Open a web browser onto the server URL. Technically we're jumping the # gun a little here since the server is not yet running, but there's no # clean way to run a function after the server has started without # losing the simplicity of the code. In the Real World (TM) it'll take # longer for the browser to start than it will for us to start # serving, so we'll be OK. if args.with_browser is None: opener = webbrowser.open else: opener = webbrowser.get(args.with_browser).open opener('http://%s:%s' % (args.host, args.port)) if __name__ == '__main__': exit(main()) klaus-1.4.0/klaus/0000755000076500000240000000000013464301406013327 5ustar jstaff00000000000000klaus-1.4.0/klaus/__init__.py0000644000076500000240000001772213464301027015450 0ustar jstaff00000000000000import jinja2 import flask import httpauth import dulwich.web from klaus import views, utils from klaus.repo import FancyRepo KLAUS_VERSION = utils.guess_git_revision() or '1.4.0' class Klaus(flask.Flask): jinja_options = { 'extensions': ['jinja2.ext.autoescape'], 'undefined': jinja2.StrictUndefined } def __init__(self, repo_paths, site_name, use_smarthttp, ctags_policy='none'): """(See `make_app` for parameter descriptions.)""" repo_objs = [FancyRepo(path) for path in repo_paths] self.repos = dict((repo.name, repo) for repo in repo_objs) self.site_name = site_name self.use_smarthttp = use_smarthttp self.ctags_policy = ctags_policy flask.Flask.__init__(self, __name__) self.setup_routes() def create_jinja_environment(self): """Called by Flask.__init__""" env = super(Klaus, self).create_jinja_environment() for func in [ 'force_unicode', 'timesince', 'shorten_sha1', 'shorten_message', 'extract_author_name', 'formattimestamp', ]: env.filters[func] = getattr(utils, func) env.globals['KLAUS_VERSION'] = KLAUS_VERSION env.globals['USE_SMARTHTTP'] = self.use_smarthttp env.globals['SITE_NAME'] = self.site_name return env def setup_routes(self): for endpoint, rule in [ ('repo_list', '/'), ('robots_txt', '/robots.txt/'), ('blob', '//blob/'), ('blob', '//blob//'), ('blame', '//blame/'), ('blame', '//blame//'), ('raw', '//raw//'), ('raw', '//raw//'), ('submodule', '//submodule//'), ('submodule', '//submodule//'), ('commit', '//commit//'), ('patch', '//commit/.diff'), ('patch', '//commit/.patch'), ('index', '//'), ('index', '//'), ('history', '//tree//'), ('history', '//tree//'), ('download', '//tarball//'), ]: self.add_url_rule(rule, view_func=getattr(views, endpoint)) def should_use_ctags(self, git_repo, git_commit): if self.ctags_policy == 'none': return False elif self.ctags_policy == 'ALL': return True elif self.ctags_policy == 'tags-and-branches': return git_commit.id in git_repo.get_tag_and_branch_shas() else: raise ValueError("Unknown ctags policy %r" % self.ctags_policy) def make_app(repo_paths, site_name, use_smarthttp=False, htdigest_file=None, require_browser_auth=False, disable_push=False, unauthenticated_push=False, ctags_policy='none'): """ Returns a WSGI app with all the features (smarthttp, authentication) already patched in. :param repo_paths: List of paths of repositories to serve. :param site_name: Name of the Web site (e.g. "John Doe's Git Repositories") :param use_smarthttp: Enable Git Smart HTTP mode, which makes it possible to pull from the served repositories. If `htdigest_file` is set as well, also allow to push for authenticated users. :param require_browser_auth: Require HTTP authentication according to the credentials in `htdigest_file` for ALL access to the Web interface. Requires the `htdigest_file` option to be set. :param disable_push: Disable push support. This is required in case both `use_smarthttp` and `require_browser_auth` (and thus `htdigest_file`) are set, but push should not be supported. :param htdigest_file: A *file-like* object that contains the HTTP auth credentials. :param unauthenticated_push: Allow push'ing without authentication. DANGER ZONE! :param ctags_policy: The ctags policy to use, may be one of: - 'none': never use ctags - 'tags-and-branches': use ctags for revisions that are the HEAD of a tag or branc - 'ALL': use ctags for all revisions, may result in high server load! """ if unauthenticated_push: if not use_smarthttp: raise ValueError("'unauthenticated_push' set without 'use_smarthttp'") if disable_push: raise ValueError("'unauthenticated_push' set with 'disable_push'") if require_browser_auth: raise ValueError("Incompatible options 'unauthenticated_push' and 'require_browser_auth'") if htdigest_file and not (require_browser_auth or use_smarthttp): raise ValueError("'htdigest_file' set without 'use_smarthttp' or 'require_browser_auth'") app = Klaus( repo_paths, site_name, use_smarthttp, ctags_policy, ) app.wsgi_app = utils.ProxyFix(app.wsgi_app) if use_smarthttp: # `path -> Repo` mapping for Dulwich's web support dulwich_backend = dulwich.server.DictBackend( dict(('/'+name, repo) for name, repo in app.repos.items()) ) # Dulwich takes care of all Git related requests/URLs # and passes through everything else to klaus dulwich_wrapped_app = dulwich.web.make_wsgi_chain( backend=dulwich_backend, fallback_app=app.wsgi_app, ) dulwich_wrapped_app = utils.ProxyFix(dulwich_wrapped_app) # `receive-pack` is requested by the "client" on a push # (the "server" is asked to *receive* packs), i.e. we need to secure # it using authentication or deny access completely to make the repo # read-only. # # Git first sends requests to //info/refs?service=git-receive-pack. # If this request is responded to using HTTP 401 Unauthorized, the user # is prompted for username and password. If we keep responding 401, Git # interprets this as an authentication failure. (We can't respond 403 # because this results in horrible, unhelpful Git error messages.) # # Git will never call //git-receive-pack if authentication # failed for /info/refs, but since it's used to upload stuff to the server # we must secure it anyway for security reasons. PATTERN = r'^/[^/]+/(info/refs\?service=git-receive-pack|git-receive-pack)$' if unauthenticated_push: # DANGER ZONE: Don't require authentication for push'ing app.wsgi_app = dulwich_wrapped_app elif htdigest_file and not disable_push: # .htdigest file given. Use it to read the push-er credentials from. if require_browser_auth: # No need to secure push'ing if we already require HTTP auth # for all of the Web interface. app.wsgi_app = dulwich_wrapped_app else: # Web interface isn't already secured. Require authentication for push'ing. app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, wsgi_app=dulwich_wrapped_app, routes=[PATTERN], ) else: # No .htdigest file given. Disable push-ing. Semantically we should # use HTTP 403 here but since that results in freaky error messages # (see above) we keep asking for authentication (401) instead. # Git will print a nice error message after a few tries. app.wsgi_app = httpauth.AlwaysFailingAuthMiddleware( wsgi_app=dulwich_wrapped_app, routes=[PATTERN], ) if require_browser_auth: app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, wsgi_app=app.wsgi_app ) return app klaus-1.4.0/klaus/contrib/0000755000076500000240000000000013464301406014767 5ustar jstaff00000000000000klaus-1.4.0/klaus/contrib/__init__.py0000644000076500000240000000000012430471212017061 0ustar jstaff00000000000000klaus-1.4.0/klaus/contrib/app_args.py0000644000076500000240000000143013256244636017145 0ustar jstaff00000000000000import os from distutils.util import strtobool def get_args_from_env(): repos = os.environ.get('KLAUS_REPOS', []) if repos: repos = repos.split() args = ( repos, os.environ.get('KLAUS_SITE_NAME', 'unnamed site') ) kwargs = dict( htdigest_file=os.environ.get('KLAUS_HTDIGEST_FILE'), use_smarthttp=strtobool(os.environ.get('KLAUS_USE_SMARTHTTP', '0')), require_browser_auth=strtobool( os.environ.get('KLAUS_REQUIRE_BROWSER_AUTH', '0')), disable_push=strtobool(os.environ.get('KLAUS_DISABLE_PUSH', '0')), unauthenticated_push=strtobool( os.environ.get('KLAUS_UNAUTHENTICATED_PUSH', '0')), ctags_policy=os.environ.get('KLAUS_CTAGS_POLICY', 'none') ) return args, kwargs klaus-1.4.0/klaus/contrib/wsgi.py0000644000076500000240000000047513042513007016312 0ustar jstaff00000000000000from klaus import make_app from .app_args import get_args_from_env args, kwargs = get_args_from_env() if kwargs['htdigest_file']: with open(kwargs['htdigest_file']) as file: kwargs['htdigest_file'] = file application = make_app(*args, **kwargs) else: application = make_app(*args, **kwargs) klaus-1.4.0/klaus/contrib/wsgi_autoreload.py0000644000076500000240000000141513256246150020535 0ustar jstaff00000000000000import os import warnings import io from .app_args import get_args_from_env from .wsgi_autoreloading import make_autoreloading_app if 'KLAUS_REPOS' in os.environ: warnings.warn("use KLAUS_REPOS_ROOT instead of KLAUS_REPOS for the autoreloader apps", DeprecationWarning) args, kwargs = get_args_from_env() repos_root = os.environ.get('KLAUS_REPOS_ROOT') or os.environ['KLAUS_REPOS'] args = (repos_root,) + args[1:] if kwargs['htdigest_file']: # Cache the contents of the htdigest file, the application will not read # the file like object until later when called. with io.open(kwargs['htdigest_file'], encoding='utf-8') as htdigest_file: kwargs['htdigest_file'] = io.StringIO(htdigest_file.read()) application = make_autoreloading_app(*args, **kwargs) klaus-1.4.0/klaus/contrib/wsgi_autoreloading.py0000644000076500000240000000273213256246300021233 0ustar jstaff00000000000000from __future__ import print_function import os import time import threading from klaus import make_app # Shared state between poller and application wrapper class _: #: the real WSGI app inner_app = None should_reload = True def poll_for_changes(interval, dir): """ Polls `dir` for changes every `interval` seconds and sets `should_reload` accordingly. """ old_contents = os.listdir(dir) while 1: time.sleep(interval) if _.should_reload: # klaus application has not seen our change yet continue new_contents = os.listdir(dir) if new_contents != old_contents: # Directory contents changed => should_reload old_contents = new_contents _.should_reload = True def make_autoreloading_app(repos_root, *args, **kwargs): def app(environ, start_response): if _.should_reload: # Refresh inner application with new repo list print("Reloading repository list...") _.inner_app = make_app( [os.path.join(repos_root, x) for x in os.listdir(repos_root)], *args, **kwargs ) _.should_reload = False return _.inner_app(environ, start_response) # Background thread that polls the directory for changes poller_thread = threading.Thread(target=(lambda: poll_for_changes(10, repos_root))) poller_thread.daemon = True poller_thread.start() return app klaus-1.4.0/klaus/ctagscache.py0000644000076500000240000001640013117766364016004 0ustar jstaff00000000000000"""A cache for tagsfiles generated by the 'ctags' command line tool. We don't want to run the 'ctags' command line tool on each request as it may take a lot of time. The following steps are necessary in order to create a ctags tagsfile that be read by Pygments: 1. Clone the repository to a temporary location and check out the branch/commit the user is browsing, unless the branch is already checked out. (*) 2. Run 'ctags -R' on the temporary repository checkout. 3. Delete the temporary repository checkout. To avoid going through these steps on each request, we cache the tagsfile generated in step 2. The cache is on-disk and non-persistent, i.e. cleared whenever the Python interpreter running klaus is shut down. For large projects, the ctags tagsfiles may grow to sizes of multiple MiB, so we have to set an upper limit on the size of the cache. Since tagsfiles are represented as uncompressed ASCII files, we can increase the number of tagsfiles we can cache by using compression. Of course, 'python-ctags', which is used by Pygments to read the tagsfiles, can't deal with compressed tagsfiles, so we have to uncompress them before actually using them. To avoid decompressing tagsfiles on each request, we keep the tagsfiles that are most likely to be used (**) in uncompressed form. (*) We always create a clone in the current implementation; this could be optimized in the future. (**) "most likely": currently implemented as "most recently used" """ import os import shutil import tempfile import threading import gzip import atexit from dulwich.lru_cache import LRUSizeCache from klaus.ctagsutils import create_tagsfile, delete_tagsfile # Good compression while taking only 10% more time than level 1 COMPRESSION_LEVEL = 4 def compress_tagsfile(uncompressed_tagsfile_path): """Compress an uncompressed tagsfile. :return: path to the compressed version of the tagsfile """ _, compressed_tagsfile_path = tempfile.mkstemp() with open(uncompressed_tagsfile_path, 'rb') as uncompressed: with gzip.open(compressed_tagsfile_path, 'wb', COMPRESSION_LEVEL) as compressed: shutil.copyfileobj(uncompressed, compressed) return compressed_tagsfile_path def uncompress_tagsfile(compressed_tagsfile_path): """Uncompress an compressed tagsfile. :return: path to the uncompressed version of the tagsfile """ _, uncompressed_tagsfile_path = tempfile.mkstemp() with gzip.open(compressed_tagsfile_path, 'rb') as compressed: with open(uncompressed_tagsfile_path, 'wb') as uncompressed: shutil.copyfileobj(compressed, uncompressed) return uncompressed_tagsfile_path MiB = 1024 * 1024 class CTagsCache(object): """A ctags cache. Both uncompressed and compressed entries are kept in temporary files created by `tempfile.mkstemp` which are deleted from disk when the Python interpreter is shut down. :param uncompressed_max_bytes: Maximum size of the uncompressed cache sector :param compressed_max_bytes: Maximum size of the compressed cache sector The lifecycle of a cache entry is as follows. - When first created, a tagsfile is put into the uncompressed cache sector. - When free space is required for other uncompressed tagsfiles, it may be moved to the compressed cache sector. Gzip is used to compress the tagsfile. - When free space is required for other compressed tagsfiles, it may be evicted from the cache entirely. - When the tagsfile is requested and it's in the compressed cache sector, it is moved back to the uncompressed sector prior to using it. """ def __init__(self, uncompressed_max_bytes=30*MiB, compressed_max_bytes=20*MiB): self.uncompressed_max_bytes = uncompressed_max_bytes self.compressed_max_bytes = compressed_max_bytes # Note: We use dulwich's LRU cache to store the tagsfile paths here, # but we could easily replace it by any other (LRU) cache implementation. self._uncompressed_cache = LRUSizeCache(uncompressed_max_bytes, compute_size=os.path.getsize) self._compressed_cache = LRUSizeCache(compressed_max_bytes, compute_size=os.path.getsize) self._clearing = False self._lock = threading.Lock() atexit.register(self.clear) def __del__(self): self.clear() def clear(self): """Clear both the uncompressed and compressed caches.""" # Don't waste time moving tagsfiles from uncompressed to compressed cache, # but remove them directly instead: self._clearing = True self._uncompressed_cache.clear() self._compressed_cache.clear() self._clearing = False def get_tagsfile(self, git_repo_path, git_rev): """Get the ctags tagsfile for the given Git repository and revision. - If the tagsfile is still in cache, and in uncompressed form, return it without any further cost. - If the tagsfile is still in cache, but in compressed form, uncompress it, put it into uncompressed space, and return the uncompressed version. - If the tagsfile isn't in cache at all, create it, put it into uncompressed cache and return the newly created version. """ # Always require full SHAs assert len(git_rev) == 40 # Avoiding race conditions, The Sledgehammer Way with self._lock: if git_rev in self._uncompressed_cache: return self._uncompressed_cache[git_rev] if git_rev in self._compressed_cache: compressed_tagsfile_path = self._compressed_cache[git_rev] uncompressed_tagsfile_path = uncompress_tagsfile(compressed_tagsfile_path) self._compressed_cache._remove_node(self._compressed_cache._cache[git_rev]) else: # Not in cache. uncompressed_tagsfile_path = create_tagsfile(git_repo_path, git_rev) self._uncompressed_cache.add(git_rev, uncompressed_tagsfile_path, self._clear_uncompressed_entry) return uncompressed_tagsfile_path def _clear_uncompressed_entry(self, git_rev, uncompressed_tagsfile_path): """Called by LRUSizeCache whenever an entry is to be evicted from uncompressed cache. Most of the times this happens when space is needed in uncompressed cache, in which case we move the tagsfile to compressed cache. When clearing the cache, we don't bother moving entries to uncompressed space; we delete them directly instead. """ if not self._clearing: # If we're clearing the whole cache, don't waste time moving tagsfiles # from uncompressed to compressed cache, but remove them directly instead. self._compressed_cache.add(git_rev, compress_tagsfile(uncompressed_tagsfile_path), self._clear_compressed_entry) delete_tagsfile(uncompressed_tagsfile_path) def _clear_compressed_entry(self, git_rev, compressed_tagsfile_path): """Called by LRUSizeCache whenever an entry to be evicted from compressed cache. This happens when space is needed for new compressed tagsfiles. We delete the evictee from the cache entirely. """ delete_tagsfile(compressed_tagsfile_path) klaus-1.4.0/klaus/ctagsutils.py0000644000076500000240000000245213117766352016100 0ustar jstaff00000000000000import os import subprocess import shutil import tempfile import subprocess def check_have_exuberant_ctags(): """Check that the 'ctags' binary is *Exuberant* ctags (not etags etc)""" try: return b"Exuberant" in subprocess.check_output(["ctags", "--version"], stderr=subprocess.PIPE) except subprocess.CalledProcessError: return False def create_tagsfile(git_repo_path, git_rev): """Create a ctags tagsfile for the given Git repository and revision. This creates a temporary clone of the repository, checks out the revision, runs 'ctags -R' and deletes the temporary clone. :return: path to the generated tagsfile """ assert check_have_exuberant_ctags(), "'ctags' binary is missing or not *Exuberant* ctags" _, target_tagsfile = tempfile.mkstemp() checkout_tmpdir = tempfile.mkdtemp() try: subprocess.check_call(["git", "clone", "-q", "--shared", git_repo_path, checkout_tmpdir]) subprocess.check_call(["git", "checkout", "-q", git_rev], cwd=checkout_tmpdir) subprocess.check_call(["ctags", "--fields=+l", "-Rno", target_tagsfile], cwd=checkout_tmpdir) finally: shutil.rmtree(checkout_tmpdir) return target_tagsfile def delete_tagsfile(tagsfile_path): """Delete a tagsfile.""" os.remove(tagsfile_path) klaus-1.4.0/klaus/diff.py0000644000076500000240000000465013256222524014620 0ustar jstaff00000000000000# -*- coding: utf-8 -*- """ lodgeit.lib.diff ~~~~~~~~~~~~~~~~ Render a nice diff between two things. :copyright: 2007 by Armin Ronacher. :license: BSD """ from difflib import SequenceMatcher from klaus.utils import escape_html as e def highlight_line(old_line, new_line): """Highlight inline changes in both lines.""" start = 0 limit = min(len(old_line), len(new_line)) while start < limit and old_line[start] == new_line[start]: start += 1 end = -1 limit -= start while -end <= limit and old_line[end] == new_line[end]: end -= 1 end += 1 if start or end: def do(l, tag): last = end + len(l) return b''.join( [l[:start], b'<', tag, b'>', l[start:last], b'', l[last:]]) old_line = do(old_line, b'del') new_line = do(new_line, b'ins') return old_line, new_line def render_diff(a, b, n=3): """Parse the diff an return data for the template.""" actions = [] chunks = [] for group in SequenceMatcher(None, a, b).get_grouped_opcodes(n): old_line, old_end, new_line, new_end = group[0][1], group[-1][2], group[0][3], group[-1][4] lines = [] def add_line(old_lineno, new_lineno, action, line): actions.append(action) lines.append({ 'old_lineno': old_lineno, 'new_lineno': new_lineno, 'action': action, 'line': line, 'no_newline': not line.endswith(b'\n') }) chunks.append(lines) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for c, line in enumerate(a[i1:i2]): add_line(i1+c, j1+c, 'unmod', e(line)) elif tag == 'insert': for c, line in enumerate(b[j1:j2]): add_line(None, j1+c, 'add', e(line)) elif tag == 'delete': for c, line in enumerate(a[i1:i2]): add_line(i1+c, None, 'del', e(line)) elif tag == 'replace': for c, line in enumerate(a[i1:i2]): add_line(i1+c, None, 'del', e(line)) for c, line in enumerate(b[j1:j2]): add_line(None, j1+c, 'add', e(line)) else: raise AssertionError('unknown tag %s' % tag) return actions.count('add'), actions.count('del'), chunks klaus-1.4.0/klaus/highlighting.py0000644000076500000240000001066013042513007016343 0ustar jstaff00000000000000from six.moves import filter from pygments import highlight from pygments.lexers import get_lexer_by_name, get_lexer_for_filename, \ guess_lexer, ClassNotFound, TextLexer from pygments.formatters import HtmlFormatter from klaus import markup CTAGS_SUPPORTED_LANGUAGES = ( "Asm Awk Basic C C# C++ Cobol DosBatch Eiffel Erlang Fortran HTML Java " "JavaScript Lisp Lua Make Makefile MatLab OCaml PHP Pascal Perl Python " "REXX Ruby SML SQL Scheme Sh Tcl Tex VHDL Verilog Vim" # Not supported by Pygments: Asp Ant BETA Flex SLang Vera YACC ).split() PYGMENTS_CTAGS_LANGUAGE_MAP = dict((get_lexer_by_name(l).name, l) for l in CTAGS_SUPPORTED_LANGUAGES) class KlausDefaultFormatter(HtmlFormatter): def __init__(self, language, ctags, **kwargs): HtmlFormatter.__init__(self, linenos='table', lineanchors='L', linespans='L', anchorlinenos=True, **kwargs) self.language = language if ctags: # Use Pygments' ctags system but provide our own CTags instance self.tagsfile = True # some trueish object self._ctags = ctags def _format_lines(self, tokensource): for tag, line in HtmlFormatter._format_lines(self, tokensource): if tag == 1: # sourcecode line line = '%s' % line yield tag, line def _lookup_ctag(self, token): matches = list(self._get_all_ctags_matches(token)) best_matches = list(self.get_best_ctags_matches(matches)) if not best_matches: return None, None else: return (best_matches[0]['file'].decode("utf-8"), best_matches[0]['lineNumber']) def _get_all_ctags_matches(self, token): FIELDS = ('file', 'lineNumber', 'kind', b'language') from ctags import TagEntry entry = TagEntry() # target "buffer" for ctags if self._ctags.find(entry, token.encode("utf-8"), 0): yield dict((k, entry[k]) for k in FIELDS) while self._ctags.findNext(entry): yield dict((k, entry[k]) for k in FIELDS) def get_best_ctags_matches(self, matches): if self.language is None: return matches else: return filter(lambda match: match[b'language'] == self.language.encode("utf-8"), matches) class KlausPythonFormatter(KlausDefaultFormatter): def get_best_ctags_matches(self, matches): # The first ctags match may be an import, which ctags sees as a # definition of the tag -- even though it might very well have found # the "real" definition of the tag. Import matches aren't very helpful: # In the best case, we are brought to the line where the tag is imported # in the same file. But it may also bring us to some completely unrelated # import of the tag in some other file. We change the tag lookup mechanics # so that non-import matches are always preferred over import matches. return filter( lambda match: match['kind'] != b'i', super(KlausPythonFormatter, self).get_best_ctags_matches(matches) ) def highlight_or_render(code, filename, render_markup=True, ctags=None, ctags_baseurl=None): """Render code using Pygments, markup (markdown, rst, ...) using the corresponding renderer, if available. :param code: the program code to highlight, str :param filename: name of the source file the code is taken from, str :param render_markup: whether to render markup if possible, bool :param ctags: tagsfile obj used for source code hyperlinks, ``ctags.CTags`` :param ctags_baseurl: base url used for source code hyperlinks, str """ if render_markup and markup.can_render(filename): return markup.render(filename, code) try: lexer = get_lexer_for_filename(filename, code) except ClassNotFound: try: lexer = guess_lexer(code) except ClassNotFound: lexer = TextLexer() formatter_cls = { 'Python': KlausPythonFormatter, }.get(lexer.name, KlausDefaultFormatter) if ctags: ctags_urlscheme = ctags_baseurl + "%(path)s%(fname)s%(fext)s" else: ctags_urlscheme = None formatter = formatter_cls( language=PYGMENTS_CTAGS_LANGUAGE_MAP.get(lexer.name), ctags=ctags, tagurlformat=ctags_urlscheme, ) return highlight(code, lexer, formatter) klaus-1.4.0/klaus/markup.py0000644000076500000240000000252513312123304015173 0ustar jstaff00000000000000import os LANGUAGES = [] def get_renderer(filename): _, ext = os.path.splitext(filename) for extensions, renderer in LANGUAGES: if ext in extensions: return renderer def can_render(filename): return get_renderer(filename) is not None def render(filename, content=None): if content is None: content = open(filename).read() return get_renderer(filename)(content) def _load_markdown(): try: import markdown except ImportError: return def render_markdown(content): return markdown.markdown(content, extensions=['toc', 'extra']) LANGUAGES.append((['.md', '.mkdn', '.mdwn', '.markdown'], render_markdown)) def _load_restructured_text(): try: from docutils.core import publish_parts from docutils.writers.html4css1 import Writer except ImportError: return def render_rest(content): # start by h2 and ignore invalid directives and so on # (most likely from Sphinx) settings = {'initial_header_level': 2, 'report_level': 0} return publish_parts(content, writer=Writer(), settings_overrides=settings).get('html_body') LANGUAGES.append((['.rst', '.rest'], render_rest)) for loader in [_load_markdown, _load_restructured_text]: loader() klaus-1.4.0/klaus/repo.py0000644000076500000240000002373513464300444014661 0ustar jstaff00000000000000import os import io import stat import subprocess from dulwich.objects import S_ISGITLINK from dulwich.object_store import tree_lookup_path from dulwich.objects import Blob from dulwich.errors import NotTreeError import dulwich, dulwich.patch from klaus.utils import force_unicode, parent_directory, encode_for_git, decode_from_git from klaus.diff import render_diff class FancyRepo(dulwich.repo.Repo): """A wrapper around Dulwich's Repo that adds some helper methods.""" # TODO: factor out stuff into dulwich @property def name(self): """Get repository name from path. 1. /x/y.git -> /x/y and /x/y/.git/ -> /x/y// 2. /x/y/ -> /x/y 3. /x/y -> y """ path = self.path.rstrip(os.sep).split(os.sep)[-1] if path.endswith('.git'): path = path[:-4] return path def get_last_updated_at(self): """Get datetime of last commit to this repository.""" refs = [self[ref_hash] for ref_hash in self.get_refs().values()] refs.sort(key=lambda obj:getattr(obj, 'commit_time', float('-inf')), reverse=True) for ref in refs: # Find the latest ref that has a commit_time; tags do not # have a commit time if hasattr(ref, "commit_time"): return ref.commit_time return None @property def cloneurl(self): """Retrieve the gitweb notion of the public clone URL of this repo.""" f = self.get_named_file('cloneurl') if f is not None: return f.read() c = self.get_config() try: return force_unicode(c.get(b'gitweb', b'url')) except KeyError: return None def get_description(self): """Like Dulwich's `get_description`, but returns None if the file contains Git's default text "Unnamed repository[...]". """ description = super(FancyRepo, self).get_description() if description: description = force_unicode(description) if not description.startswith("Unnamed repository;"): return force_unicode(description) def get_commit(self, rev): """Get commit object identified by `rev` (SHA or branch or tag name).""" for prefix in ['refs/heads/', 'refs/tags/', '']: key = prefix + rev try: obj = self[encode_for_git(key)] if isinstance(obj, dulwich.objects.Tag): obj = self[obj.object[1]] return obj except KeyError: pass raise KeyError(rev) def get_default_branch(self): """Tries to guess the default repo branch name.""" for candidate in ['master', 'trunk', 'default', 'gh-pages']: try: self.get_commit(candidate) return candidate except KeyError: pass try: return self.get_branch_names()[0] except IndexError: return None def get_ref_names_ordered_by_last_commit(self, prefix, exclude=None): """Return a list of ref names that begin with `prefix`, ordered by the time they have been committed to last. """ def get_commit_time(refname): obj = self[refs[refname]] if isinstance(obj, dulwich.objects.Tag): return obj.tag_time return obj.commit_time refs = self.refs.as_dict(encode_for_git(prefix)) if exclude: refs.pop(prefix + exclude, None) sorted_names = sorted(refs.keys(), key=get_commit_time, reverse=True) return [decode_from_git(ref) for ref in sorted_names] def get_branch_names(self, exclude=None): """Return a list of branch names of this repo, ordered by the time they have been committed to last. """ return self.get_ref_names_ordered_by_last_commit('refs/heads', exclude) def get_tag_names(self): """Return a list of tag names of this repo, ordered by creation time.""" return self.get_ref_names_ordered_by_last_commit('refs/tags') def get_tag_and_branch_shas(self): """Return a list of SHAs of all tags and branches.""" tag_shas = self.refs.as_dict(b'refs/tags/').values() branch_shas = self.refs.as_dict(b'refs/heads/').values() return set(tag_shas) | set(branch_shas) def history(self, commit, path=None, max_commits=None, skip=0): """Return a list of all commits that affected `path`, starting at branch or commit `commit`. `skip` can be used for pagination, `max_commits` to limit the number of commits returned. Similar to `git log [branch/commit] [--skip skip] [-n max_commits]`. """ # XXX The pure-Python/dulwich code is very slow compared to `git log` # at the time of this writing (mid-2012). # For instance, `git log .tx` in the Django root directory takes # about 0.15s on my machine whereas the history() method needs 5s. # Therefore we use `git log` here until dulwich gets faster. # For the pure-Python implementation, see the 'purepy-hist' branch. cmd = ['git', 'log', '--format=%H'] if skip: cmd.append('--skip=%d' % skip) if max_commits: cmd.append('--max-count=%d' % max_commits) cmd.append(decode_from_git(commit.id)) if path: cmd.extend(['--', path]) output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path)) sha1_sums = output.strip().split(b'\n') return [self[sha1] for sha1 in sha1_sums] def blame(self, commit, path): """Return a 'git blame' list for the file at `path`: For each line in the file, the list contains the commit that last changed that line. """ # XXX see comment in `.history()` cmd = ['git', 'blame', '-ls', '--root', decode_from_git(commit.id), '--', path] output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path)) sha1_sums = [line[:40] for line in output.strip().split(b'\n')] return [None if self[sha1] is None else decode_from_git(self[sha1].id) for sha1 in sha1_sums] def get_blob_or_tree(self, commit, path): """Return the Git tree or blob object for `path` at `commit`.""" try: (mode, oid) = tree_lookup_path(self.__getitem__, commit.tree, encode_for_git(path)) except NotTreeError: # Some part of the path was a file where a folder was expected. # Example: path="/path/to/foo.txt" but "to" is a file in "/path". raise KeyError return self[oid] def listdir(self, commit, path): """Return a list of submodules, directories and files in given directory: Lists of (link name, target path) tuples. """ submodules, dirs, files = [], [], [] for entry_rel in self.get_blob_or_tree(commit, path).items(): # entry_rel: Entry('foo.txt', ...) # entry_abs: Entry('spam/eggs/foo.txt', ...) entry_abs = entry_rel.in_path(encode_for_git(path)) path_str = decode_from_git(entry_abs.path) item = (os.path.basename(path_str), path_str) if S_ISGITLINK(entry_abs.mode): submodules.append(item) elif stat.S_ISDIR(entry_abs.mode): dirs.append(item) else: files.append(item) keyfunc = lambda tpl: tpl[0].lower() submodules.sort(key=keyfunc) files.sort(key=keyfunc) dirs.sort(key=keyfunc) if path: dirs.insert(0, ('..', parent_directory(path))) return {'submodules': submodules, 'dirs' : dirs, 'files' : files} def commit_diff(self, commit): """Return the list of changes introduced by `commit`.""" from klaus.utils import guess_is_binary if commit.parents: parent_tree = self[commit.parents[0]].tree else: parent_tree = None summary = {'nfiles': 0, 'nadditions': 0, 'ndeletions': 0} file_changes = [] # the changes in detail dulwich_changes = self.object_store.tree_changes(parent_tree, commit.tree) for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in dulwich_changes: summary['nfiles'] += 1 try: oldblob = self.object_store[oldsha] if oldsha else Blob.from_string(b'') newblob = self.object_store[newsha] if newsha else Blob.from_string(b'') except KeyError: # newsha/oldsha are probably related to submodules. # Dulwich will handle that. pass # Check for binary files -- can't show diffs for these if guess_is_binary(newblob) or \ guess_is_binary(oldblob): file_changes.append({ 'is_binary': True, 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': None }) continue additions, deletions, chunks = render_diff( oldblob.splitlines(), newblob.splitlines()) change = { 'is_binary': False, 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': chunks, 'additions': additions, 'deletions': deletions, } summary['nadditions'] += additions summary['ndeletions'] += deletions file_changes.append(change) return summary, file_changes def raw_commit_diff(self, commit): if commit.parents: parent_tree = self[commit.parents[0]].tree else: parent_tree = None bytesio = io.BytesIO() dulwich.patch.write_tree_diff(bytesio, self.object_store, parent_tree, commit.tree) return bytesio.getvalue() klaus-1.4.0/klaus/static/0000755000076500000240000000000013464301406014616 5ustar jstaff00000000000000klaus-1.4.0/klaus/static/favicon.png0000644000076500000240000000547412430471212016756 0ustar jstaff00000000000000PNG  IHDR>asBIT|d pHYsmmsH(tEXtSoftwarewww.inkscape.org< IDATxM$eO =vO/d &dj(YN9q(eA  WM%MWAvxzvSꞪ}KT̈́1fOMt"@/=1y%Yt:bADn3 n,{˓M!V98|,¡zq}Y8/yGx>%8ӥ |߿V\xԭ7V `5|sq^'}WqZX WqV}GM'%pFc̅50‡R(`nn񢫀yB%(ss/W$@%ARJ/T$5θJ(Ƙ jǚM8+ Fwvo4WI0`<߬$OvoEԩ$Gs{WJ{FQ/K`S`8`;$HNc.Z-sAg%y2*gGP<#Q/*"AxO2mc>y+l\%EbIhƷ;,t$;@jjѥ`wm ^2+r_&/#2ӿVj ]!+AfdVK(^QgD2 5j>Ziqq1UkDR @)[TuI[F$U P"aV]pGjX(rZ٨T$&@_ἙF6 FBb]+mR<`N,\$ҞWazpswh+۟CU# he|![*{/$K+#6$ׁZ\j7xƘ=+SKͅ6Zp]DSBJF0|Z10RFo-jB++{[$KDwy'3;xbbֆȔʱN&/ H[!WTbmiEv۪A䪴Due<<@=: m۶-@]\bQ}, ã@"n2;Xd*8P޸x>;Ƙ=ob727k׮!XS}Tˤ'd:o[ |7NG͉+\I+3-q&ɖi܃z4[/|X@%AbA4~c#"xNSo6{#"i6t&;;ξɅgi60WzhƩVӼ z ѳ@văh>A$P+;faAy>ĘPIP>1'TQ‡fUQÇUq‡1VqÇ1'X.>D'n,$|H0P`woŋ.Gy5%i`aNS/"E/bcM$,2<8΁gOM4Y{hY)K B qoq>qnPRlWQEoK41z? ?򒰂I[R(5z>&@vĉ;:VҢ"H j-I3|HqUpl p>|H;|HyYxX_o(H/v"|`_$P+l-ׂ‡6I8S'zY īuZVSlDC[d-mIV,< tt4499 5=("c1a>9T7MgG9l~FESm|ქSķn m?z)rrxJ-vv=&vF& mZ>@u> ?E  ChhC%(bP0M >PpK"ܐCrKP@9%(KP\)|(P >H(e J&S%%AÇ ŐC|%p!|(JR Jb v%p9|pT#@2 6Cp>l`Y CɍP}>ոt3Zv}{Td7@@^@0 cGFIENDB`klaus-1.4.0/klaus/static/klaus.css0000644000076500000240000002235713371602040016453 0ustar jstaff00000000000000@charset "utf-8"; body, header, #content { overflow: auto; } /* Reset */ body { margin: 0; padding: 0; font-family: sans-serif; } a, a:visited { color: #003278; text-decoration: none; } a:hover { text-decoration: underline; } table { border-spacing: 0; border-collapse: collapse; } h2 > span:last-of-type { font-size: 60%; } h2 > code:last-of-type { font-size: 60%; margin-left: 2%; } .clearfloat { clear: both; } .hastooltip { cursor: help; } .separated-by-dots > span:not(:first-child):before { content: '·'; margin: 0 3px 0 5px; } .slash { color: #666; margin: 0 -0.2em; } .history ul, .repolist, .tree ul, .branch-selector ul { list-style-type: none; padding-left: 0; } /* Header */ header { font-size: 90%; padding: 0.5%; border-bottom: 3px solid #e0e0e0; } header a { padding: 0.5% 0; } header .breadcrumbs > span:before { content: ' » '; color: #666; } header .slash { margin: 0 -2px; } /* Branch/tag selector */ .branch-selector { position: absolute; top: 2px; right: 2px; font-size: 90%; background-color: #fefefe; } .branch-selector > * { background-color: #fcfcfc; position: relative; } .branch-selector > span { border: 1px solid #f1f1f1; padding: 4px 5px; float: right; } .branch-selector > span:after { content: "☟"; margin-left: 5px; } .branch-selector > span:hover { background-color: #fefefe; cursor: pointer; } .branch-selector div { z-index: 1; clear: both; display: none; } .branch-selector ul { margin: 0; } .branch-selector ul + ul { border-top: 1px solid #e0e0e0; } .branch-selector li a { display: block; padding: 4px 5px; border-bottom: 1px solid #f1f1f1; } .branch-selector li:first-child a { border-top: 1px solid #f1f1f1; } .branch-selector li a:hover { background-color: #fefefe; } .branch-selector li:last-child a { border: 0; } .branch-selector:hover { border: 1px solid #ccc; } .branch-selector:hover > span { border: 0; background-color: inherit; } .branch-selector:hover div { display: block; } /* Footer */ footer { clear: both; font-size: 80%; float: right; color: #666; padding: 50px 5px 5px 0; } footer a { color: inherit; border-bottom: 1px dotted #666; } footer a:hover { text-decoration: none; } /* Container */ #content { padding: 5px 1.5%; } #content > div:nth-of-type(1), #content > div:nth-of-type(2) { float: left; } #content > div:nth-of-type(1) { width: 24%; } #content > div:nth-of-type(2) { width: 72%; margin-left: 1.5%; } /* Pagination */ .pagination { float: right; margin: 0; font-size: 90%; } .pagination > * { border: 1px solid; padding: 2px 10px; text-align: center; } .pagination .n { font-size: 90%; padding: 1px 5px; position: relative; top: 1px; } .pagination > a { opacity: 0.6; border-color: #6491bf; } .pagination > a:hover { opacity: 1; text-decoration: none; border-color: #4D6FA0; } .pagination span { color: #999; border-color: #ccc; } /* Repo List */ .repolist { margin-left: 2em; font-size: 120%; } .repolist li { margin-bottom: 10px; } .repolist li a .last-updated { color: #737373; font-size: 60%; margin-left: 1px; } .repolist li a .description { color: black; font-size: 75%; margin-left: 1px; } .repolist li a:hover { text-decoration: none; } .repolist li a:hover .name { text-decoration: underline; } /* Base styles for history and commit views */ .commit { display: block; margin-bottom: 2px; padding: 8px 10px; background-color: #f9f9f9; border: 1px solid #e0e0e0; } .commit:hover { text-decoration: none; } .commit > span { display: block; } .commit .line1 { font-family: monospace; padding-bottom: 2px; line-height: 1.3; } .commit .line1 span { white-space: pre-wrap; text-overflow: hidden; } .commit:hover .line1 { text-decoration: underline; color: #aaa; } .commit:hover .line1 span { color: black; } .commit .line2 { position: relative; top: 5px; left: 1px; } .commit .line2 > span:first-child { float: left; } .commit .line2 > span:nth-child(2) { float: right; } .commit .line2 { color: #737373; font-size: 80%; } /* History View */ .history .pagination { margin-top: -2em; } a.commit { color: black !important; } .tree ul { font-family: monospace; border-top: 1px solid #e0e0e0; } .tree li { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-top: 0; } .tree li a { padding: 5px 7px 6px 7px; display: block; color: #001533; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .tree li a:before { margin-right: 5px; position: relative; top: 2px; opacity: 0.7; content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAPCAYAAADUFP50AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sGBhMmAbS/QqsAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAANBJREFUKM+Vkj2OgzAQhb8HSLupkKiQD8DPWbZMkSMgLsF9IlLmMpiKA9CncraIQGbXIPGqsec9faOx1TTNwxhz5YT6vr8lxphr13Wc1D1Zqnmecc4BIGl1LLUk4jgmTVMA1qBzDmvtxuhLEnVdr+fEb5ZleUj0lfgGn/hXh8SiKAKEF+/3F1EUhYkA4zhumlVVARfgBXzvjxoiSkK6/Bt9Q7TWHi7lM8HOVsNE7RMlMQxDkLRs078LEkPh3XfMsuzUZ1Xbts88z3/OhKZpuv8CNeMsq6Yg8OoAAAAASUVORK5CYII=); } .tree li a.dir:before { content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAPCAYAAADtc08vAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sGBhMiMxgE1i8AAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAYxJREFUKM+l0r1KXEEYxvH/bNS4K2RNCjsRBAlqITESI0QCuqBdWLwACSJaiZILiDcQkDRql8oUIqkkkRTRwiKVyvoRV5HFEFEX/DjomePsmfNauB4xrLAmD1PNML95eBnV0Fj/vjPRMcpfMcYAMP9jYXDzV3qSO1LSmegYfdvbV/DQ8zS+709oVzu7u78/FwQAHOeU9Y31gsjz5hYcx5lqbXsxdb23ld4eW15aGQmBaDRGZfzxXS1JvukBQCmFUoqZL9PDIWCMQWuX76tnpLIxisqjJC39SXmoM5thg1Q2xsd3XXjGFmWUlz1g6MPc0xIArV0A9o89dg7PiwJqqyoAiHieRzRaZPUCibiuGzb4J+B6Bv8F3LeBtQFeznLrH5RGAgQQEZRSiAgiEIhgrZCzAcYXLnxLzgrxirIbQGuXmvgFR2eGP0caRBEg5BciIAgieRjwrdwAB9lDnrW9Yjlzkr909boIBAiCApGwVWvdE+a+fkOvzX5STd0D86XV7a/vOzy7t/hzaXb85SVDycBfkNNgmgAAAABJRU5ErkJggg==); } /* Blob, Blame, Diff, Markup View */ .line { display: block; } .linenos { background-color: #f9f9f9; text-align: right; } .linenos a { color: #888; } .linenos a:hover { text-decoration: none; } .highlight-line, .highlight-line .line { background-color: #fefed0; } .linenos a { padding: 0 6px 0 6px; } .markup table, .markup img, .markup pre { border: 1px solid #e0e0e0; } .markup table { min-width: 100%; } .markup img { max-width: 100%; padding: 1px; } .markup pre { padding: 10px 12px; background-color: #f9f9f9; } /* Blob, Blame View */ .blobview table, .blameview table { min-width: 100%; } .blobview table, .blameview table { border: 1px solid #e0e0e0; } .blobview .code, .blameview .code { padding: 0; width: 100%; } .blobview .code .line, .blameview .code .line { padding: 0 5px 0 10px; } .blobview .code a, .blameview .code a { color: inherit; } .blobview .linenos, .blameview .linenos { border: 1px solid #e0e0e0; padding: 0; } /* Blob View */ .blobview .markup { border: 1px solid #e0e0e0; } .blobview .markup h1:first-child { margin-top: 8px; } .blobview .markup { padding: 0 10px; } /* Blame View */ .blameview .highlighttable { border-top: 0; border-bottom: 0; border-left: 0; } .blameview .linenos { border-top: 0; border-bottom: 0; border-left: 0; } .blameview .line-info a { padding: 0 6px 0 6px; } .blameview .line-info { background-color: #f9f9f9; } /* Commit View */ .full-commit { width: 100% !important; margin-top: 10px; } .full-commit .commit { padding: 15px 20px; } .full-commit .commit .line1 { padding-bottom: 5px; } .full-commit .commit:hover .line1 { text-decoration: none; } .full-commit .commit .line2 > span { float: left; } .full-commit .summary { color: #737373; font-size: 80%; margin-top: 25px; } .full-commit .summary .additions { color: #008800; } .full-commit .summary .deletions { color: #ee4444; } .full-commit .file.collapsed > table { display: none; } .diff { font-family: monospace; } .diff .filename { padding: 8px 10px; background-color: #f9f9f9; border: 1px solid #e0e0e0; margin-top: 25px; } .diff .filename del { color: #999; } .diff .filename .summary { float: left; margin: -4px 15px 0 -5px; font-size: 80%; } .diff .filename .summary .additions { color: green; } .diff .filename .summary .deletions{ color: red; } .diff .togglers { float: right; } .diff .togglers a { opacity: 0.5; } .diff .file:not(.collapsed) .togglers .expand { display: none; } .diff .file.collapsed .togglers .collapse { display: none; } .diff table, .diff .emptydiff { border: 1px solid #e0e0e0; border-top: 0; background-color: #fdfdfd; display: block; } .diff .emptydiff { padding: 7px 10px; } .diff td { padding: 0; border-left: 1px solid #e0e0e0; } .diff td .line { padding: 1px 10px; display: block; min-height: 1.2em; white-space: pre-wrap; } .diff .linenos { font-size: 85%; padding: 0; vertical-align: top; } .diff .linenos a { display: block; padding-top: 1px; padding-bottom: 1px; } .diff td + td + td { width: 100%; } .diff tr:first-of-type td { padding-top: 7px; } .diff tr:last-of-type td { padding-bottom: 7px; } .diff table .del { background-color: #ffdddd; } .diff table .add { background-color: #ddffdd; } .diff table .no-newline-marker { font-size: 50%; margin-left: 5px; color: red; } .diff table del { background-color: #ee9999; text-decoration: none; } .diff table ins { background-color: #99ee99; text-decoration: none; } .diff .sep > td { height: 1.2em; text-align: center; background-color: #f9f9f9; border: 1px solid #e0e0e0; } .diff .sep:hover > td { background-color: #f9f9f9; } klaus-1.4.0/klaus/static/klaus.js0000644000076500000240000000357012703151602016274 0ustar jstaff00000000000000var forEach = function(collection, func) { for(var i = 0; i < collection.length; ++i) { func(collection[i]); } } /* General collapse/expand/toggle framework. Used for hiding diffs in commits */ var toggler = { expand: function(elem) { elem.className = elem.className.replace("collapsed", ""); }, collapse: function(elem) { if (!/collapsed/.test(elem.className)) { elem.className += " collapsed"; } }, collapseAll: function(selector) { forEach(document.querySelectorAll(selector), toggler.collapse); }, expandAll: function(selector) { forEach(document.querySelectorAll(selector), toggler.expand); } }; /* Line highlighting logic for diffs */ var highlight_linenos = function(opts) { var links = document.querySelectorAll(opts.linksSelector), currentHash = location.hash; forEach(links, function(a) { var lineno = a.getAttribute('href').substr(1), associatedLine = document.getElementById(lineno); var highlight = function() { a.className = 'highlight-line'; associatedLine.className = 'highlight-line'; currentHighlight = a; } var unhighlight = function() { if (a.getAttribute('href') != location.hash) { a.className = ''; associatedLine.className = ''; } } a.onmouseover = associatedLine.onmouseover = highlight; a.onmouseout = associatedLine.onmouseout = unhighlight; // Initial highlight if (a.getAttribute('href') == location.hash) { highlight(); } }); window.onpopstate = function() { if (currentHash) { forEach(document.querySelectorAll('a[href="' + currentHash + '"]'), function(e) { e.onmouseout() }) } if (location.hash) { forEach(document.querySelectorAll('a[href="' + location.hash + '"]'), function(e) { e.onmouseover() }); currentHash = location.hash; } }; } klaus-1.4.0/klaus/static/pygments.css0000644000076500000240000000653612574570105017215 0ustar jstaff00000000000000/* This is the Pygments Trac theme */ .code .hll { background-color: #ffffcc } .code { background: #ffffff; } .code .c { color: #999988; font-style: italic } /* Comment */ .code .err { color: #a61717; background-color: #e3d2d2 } /* Error */ .code .k { font-weight: bold } /* Keyword */ .code .o { font-weight: bold } /* Operator */ .code .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .code .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ .code .c1 { color: #999988; font-style: italic } /* Comment.Single */ .code .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ .code .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .code .ge { font-style: italic } /* Generic.Emph */ .code .gr { color: #aa0000 } /* Generic.Error */ .code .gh { color: #999999 } /* Generic.Heading */ .code .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .code .go { color: #888888 } /* Generic.Output */ .code .gp { color: #555555 } /* Generic.Prompt */ .code .gs { font-weight: bold } /* Generic.Strong */ .code .gu { color: #aaaaaa } /* Generic.Subheading */ .code .gt { color: #aa0000 } /* Generic.Traceback */ .code .kc { font-weight: bold } /* Keyword.Constant */ .code .kd { font-weight: bold } /* Keyword.Declaration */ .code .kn { font-weight: bold } /* Keyword.Namespace */ .code .kp { font-weight: bold } /* Keyword.Pseudo */ .code .kr { font-weight: bold } /* Keyword.Reserved */ .code .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .code .m { color: #009999 } /* Literal.Number */ .code .s { color: #bb8844 } /* Literal.String */ .code .na { color: #008080 } /* Name.Attribute */ .code .nb { color: #999999 } /* Name.Builtin */ .code .nc { color: #445588; font-weight: bold } /* Name.Class */ .code .no { color: #008080 } /* Name.Constant */ .code .ni { color: #800080 } /* Name.Entity */ .code .ne { color: #990000; font-weight: bold } /* Name.Exception */ .code .nf { color: #990000; font-weight: bold } /* Name.Function */ .code .nn { color: #555555 } /* Name.Namespace */ .code .nt { color: #000080 } /* Name.Tag */ .code .nv { color: #008080 } /* Name.Variable */ .code .ow { font-weight: bold } /* Operator.Word */ .code .w { color: #bbbbbb } /* Text.Whitespace */ .code .mf { color: #009999 } /* Literal.Number.Float */ .code .mh { color: #009999 } /* Literal.Number.Hex */ .code .mi { color: #009999 } /* Literal.Number.Integer */ .code .mo { color: #009999 } /* Literal.Number.Oct */ .code .sb { color: #bb8844 } /* Literal.String.Backtick */ .code .sc { color: #bb8844 } /* Literal.String.Char */ .code .sd { color: #bb8844 } /* Literal.String.Doc */ .code .s2 { color: #bb8844 } /* Literal.String.Double */ .code .se { color: #bb8844 } /* Literal.String.Escape */ .code .sh { color: #bb8844 } /* Literal.String.Heredoc */ .code .si { color: #bb8844 } /* Literal.String.Interpol */ .code .sx { color: #bb8844 } /* Literal.String.Other */ .code .sr { color: #808000 } /* Literal.String.Regex */ .code .s1 { color: #bb8844 } /* Literal.String.Single */ .code .ss { color: #bb8844 } /* Literal.String.Symbol */ .code .bp { color: #999999 } /* Name.Builtin.Pseudo */ .code .vc { color: #008080 } /* Name.Variable.Class */ .code .vg { color: #008080 } /* Name.Variable.Global */ .code .vi { color: #008080 } /* Name.Variable.Instance */ .code .il { color: #009999 } /* Literal.Number.Integer.Long */ klaus-1.4.0/klaus/static/robots.txt0000644000076500000240000000031112703151602016656 0ustar jstaff00000000000000User-agent: * Allow: /*/blob/master/ Allow: /*/tree/master/ Allow: /*/raw/master/ Disallow: /*/tree/ Disallow: /*/blob/ Disallow: /*/raw/ Disallow: /*/blame/ Disallow: /*/commit/ Disallow: /*/tarball/ klaus-1.4.0/klaus/templates/0000755000076500000240000000000013464301406015325 5ustar jstaff00000000000000klaus-1.4.0/klaus/templates/base.html0000644000076500000240000000233613124765761017144 0ustar jstaff00000000000000{% extends 'skeleton.html' %} {% block title %} {{ repo.name }} ({{ rev|shorten_sha1 }}) {% endblock %} {% block breadcrumbs %} {{ repo.name }} / {{ rev|shorten_sha1 }} {% if subpaths %} {% for name, subpath in subpaths %} {% if loop.last %} {{ name|force_unicode }} {% else %} {{ name|force_unicode }} / {% endif %} {% endfor %} {% endif %} {% endblock %} {% block extra_header %}
{{ rev|shorten_sha1 }}
{% if tags %}
    {% for tag in tags %}
  • {{ tag }}
  • {% endfor %}
{% endif %}
{% endblock %} klaus-1.4.0/klaus/templates/blame_blob.html0000644000076500000240000000227413124765761020311 0ustar jstaff00000000000000{% extends 'base.html' %} {% block title %} {{ path }} - {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %}

{{ filename|force_unicode }} @{{ rev|shorten_sha1 }}

{% if not can_render %} (Can't show blame: File is binary or too large) {% else %}
              {%- for commit in line_commits -%}
                {%- if commit == None %}
 
                {%- else %}
{{ commit | shorten_sha1 }}
                {%- endif -%}
              {%- endfor -%}
            
{% autoescape false %} {{ rendered_code }} {% endautoescape %}
{% endif %}
{% endblock %} klaus-1.4.0/klaus/templates/history.html0000644000076500000240000000036413042513007017711 0ustar jstaff00000000000000{% extends 'base.html' %} {% block title %} History of {% if path %}{{ path }} - {% endif %} {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %}
{% include 'history.inc.html' %}
{% endblock %} klaus-1.4.0/klaus/templates/history.inc.html0000644000076500000240000000377113456060655020505 0ustar jstaff00000000000000{% macro pagination() %}
{% endmacro %}

{% if subpaths %} History of {% for name, subpath in subpaths %} {{ name }} {% if not loop.last %} / {% endif %} {% endfor %} {% else %} Commit History {% endif %} @{{ rev }} {% if USE_SMARTHTTP %} git clone {{ url_for('index', repo=repo.name, _external=True) }} {% endif %} {% if repo.cloneurl %} git clone {{ repo.cloneurl }} {% endif %}

{{ pagination() }}
{{ pagination() }} klaus-1.4.0/klaus/templates/index.html0000644000076500000240000000061413042513007017315 0ustar jstaff00000000000000{% extends 'base.html' %} {% block title %} {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %}
{% if rendered_code %} {% autoescape false %} {% if is_markup %}
{{ rendered_code }}
{% else %} {{ rendered_code }} {% endif %} {% endautoescape %}
{% endif %} {% include 'history.inc.html' %}
{% endblock %} klaus-1.4.0/klaus/templates/repo_list.html0000644000076500000240000000161113464300507020213 0ustar jstaff00000000000000{% extends 'skeleton.html' %} {% block title %}Repository list{% endblock %} {% block content %}

Repositories (order by name)

{% endblock %} klaus-1.4.0/klaus/templates/skeleton.html0000644000076500000240000000166113116247124020043 0ustar jstaff00000000000000 {% if base_href %} {% endif %} {% block title %}{% endblock %} - {{ SITE_NAME }}
{{ SITE_NAME }} {% block breadcrumbs %}{% endblock %} {% block extra_header %}{% endblock %}
{% block content %}{% endblock %}
powered by klaus {{ KLAUS_VERSION }}, a simple Git viewer by Jonas Haag
klaus-1.4.0/klaus/templates/submodule.html0000644000076500000240000000074213124275505020220 0ustar jstaff00000000000000{% extends 'base.html' %} {% block title %} {{ path }} - {{ super() }} {% endblock %} {% block content %}

{{ path|force_unicode }} @{{ rev|shorten_sha1 }}

The path at {{ path|force_unicode }} contains a submodule, revision {{ submodule_rev }}. {% if submodule_url %} It can be checked out from {{ submodule_url }}. {% endif %}

{% endblock %} klaus-1.4.0/klaus/templates/tree.inc.html0000644000076500000240000000141313456064531017727 0ustar jstaff00000000000000

Tree @{{ rev|shorten_sha1 }} (Download .tar.gz)

    {% for name, fullpath in root_tree.dirs %}
  • {{ name }}
  • {% endfor %} {% for name, fullpath in root_tree.submodules %}
  • {{ name }}
  • {% endfor %} {% for name, fullpath in root_tree.files %}
  • {{ name }}
  • {% endfor %}
klaus-1.4.0/klaus/templates/view_blob.html0000644000076500000240000000314612572123357020175 0ustar jstaff00000000000000{% extends 'base.html' %} {% block title %} {{ path }} - {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %} {% set raw_url = url_for('raw', repo=repo.name, rev=rev, path=path) %} {% macro not_shown(reason) %}
({{ reason }} not shown — Download file)
{% endmacro %}

{{ filename|force_unicode }} @{{ rev|shorten_sha1 }} — {% if is_markup %} {% if render_markup %} view markup {% else %} view rendered {% endif %} · {% endif %} raw · history {% if not is_binary and not too_large %} · blame {% endif %}

{% if is_binary %} {% if is_image %} {% else %} {{ not_shown("Binary data") }} {% endif %} {% elif too_large %} {{ not_shown("Large file") }} {% else %} {% autoescape false %} {% if is_markup and render_markup %}
{{ rendered_code }}
{% else %} {{ rendered_code }} {% endif %} {% endautoescape %} {% endif %}
{% endblock %} klaus-1.4.0/klaus/templates/view_commit.html0000644000076500000240000001344413456060655020554 0ustar jstaff00000000000000{% extends 'base.html' %} {% block extra_header %}{% endblock %} {# hide branch selector #} {% block title %} Commit {{ rev }} - {{ repo.name }} {% endblock %} {% block content %} {% set summary, file_changes = repo.commit_diff(commit) %}
{{ commit.message|force_unicode }} {% if commit.author != commit.committer %} {{ commit.author|force_unicode|extract_author_name }} authored {{ commit.author_time|timesince }} {{ commit.committer|force_unicode|extract_author_name }} committed {{ commit.commit_time|timesince }} {% else %} {{ commit.committer|force_unicode|extract_author_name }} {{ commit.commit_time|timesince }} {% endif %}
{{ summary.nfiles }} changed file(s) with {{ summary.nadditions }} addition(s) and {{ summary.ndeletions }} deletion(s). Raw diff Collapse all Expand all
{% for file in file_changes %}
{% set fileno = loop.index0 %}
{% if not file.get('is_binary') %}
+{{ file.additions }}
-{{ file.deletions }}
{% endif %} {# TODO dulwich doesn't do rename recognition {% if file.old_filename != file.new_filename %} {{ file.old_filename }} → {% endif %}#} {% if file.new_filename == '/dev/null' %} {{ file.old_filename|force_unicode }} {% else %} {{ file.new_filename|force_unicode }} {% endif %} less more
{% if file.get('is_binary') %}
Binary diff not shown
{% else %} {% for chunk in file.chunks %} {%- for line in chunk -%} {#- left column: linenos -#} {%- if line.old_lineno is not none -%} {%- if line.new_lineno is not none -%} {%- else -%} {%- endif -%} {%- else %} {% endif %} {#- right column: code -#} {%- if line.old_lineno -%} {%- set line_id = "%s-L-%s"|format(fileno, line.old_lineno) -%} {%- else -%} {%- set line_id = "%s-R-%s"|format(fileno, line.new_lineno) -%} {%- endif -%} {%- endfor -%} {# lines #} {% if not loop.last %} {% endif %} {% else %} {% if file.old_filename == '/dev/null' %}
(New empty file)
{% elif file.new_filename == '/dev/null' %}
(Empty file)
{% else %} {# This case happens if a file has undergone only mode changes. In the future, if we have rename recognition, it may also happen if the file has been renamed without having its content changed. Currently, renames are always reported by dulwich as a file deletion and addition. #}
(No changes)
{% endif %} {%- endfor -%} {# chunks #}
{{ line.old_lineno }}{{ line.new_lineno }} {{ line.new_lineno }} {#- lineno anchors -#} {#- the actual line of code -#} {% autoescape false %}{{ line.line|force_unicode }}{% endautoescape %}{% if line.no_newline %}{% endif %}
{% endif %}
{% endfor %}
{% endblock %} klaus-1.4.0/klaus/utils.py0000644000076500000240000002013013456064476015053 0ustar jstaff00000000000000# encoding: utf-8 import binascii import os import re import time import datetime import mimetypes import locale import warnings import subprocess import six try: import chardet except ImportError: chardet = None from werkzeug.contrib.fixers import ProxyFix as WerkzeugProxyFix from humanize import naturaltime class ProxyFix(WerkzeugProxyFix): """This middleware can be applied to add HTTP (reverse) proxy support to a WSGI application (klaus), making it possible to: * Mount it under a sub-URL (http://example.com/git/...) * Use a different HTTP scheme (HTTP vs. HTTPS) * Make it appear under a different domain altogether It sets `REMOTE_ADDR`, `HTTP_HOST` and `wsgi.url_scheme` from `X-Forwarded-*` headers. It also sets `SCRIPT_NAME` from the `X-Script-Name` header. For instance if you have klaus mounted under /git/ and your site uses SSL (but your proxy doesn't), make the proxy pass :: X-Script-Name = '/git' X-Forwarded-Proto = 'https' ... If you have more than one proxy server in front of your app, set `num_proxies` accordingly. Do not use this middleware in non-proxy setups for security reasons. The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and `werkzeug.proxy_fix.orig_http_host`. :param app: the WSGI application :param num_proxies: the number of proxy servers in front of the app. """ def __call__(self, environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME') if script_name is not None: if script_name.endswith('/'): warnings.warn( "'X-Script-Name' header should not end in '/' (found: %r). " "Please fix your proxy's configuration." % script_name) script_name = script_name.rstrip('/') environ['SCRIPT_NAME'] = script_name return super(ProxyFix, self).__call__(environ, start_response) class SubUri(object): """WSGI middleware to tweak the WSGI environ so that it's possible to serve the wrapped app (klaus) under a sub-URL and/or to use a different HTTP scheme (http:// vs. https://) for proxy communication. This is done by making your proxy pass appropriate HTTP_X_SCRIPT_NAME and HTTP_X_SCHEME headers. For instance if you have klaus mounted under /git/ and your site uses SSL (but your proxy doesn't), make it pass :: X-Script-Name = '/git' X-Scheme = 'https' Snippet stolen from http://flask.pocoo.org/snippets/35/ """ def __init__(self, app): warnings.warn( "'klaus.utils.SubUri' is deprecated and will be removed. " "Please upgrade your code to use 'klaus.utils.ProxyFix' instead.", DeprecationWarning ) self.app = app def __call__(self, environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: environ['SCRIPT_NAME'] = script_name.rstrip('/') if script_name and environ['PATH_INFO'].startswith(script_name): # strip `script_name` from PATH_INFO environ['PATH_INFO'] = environ['PATH_INFO'][len(script_name):] if 'HTTP_X_SCHEME' in environ: environ['wsgi.url_scheme'] = environ['HTTP_X_SCHEME'] return self.app(environ, start_response) def timesince(when, now=time.time): """Return the difference between `when` and `now` in human readable form.""" return naturaltime(now() - when) def formattimestamp(timestamp): return datetime.datetime.fromtimestamp(timestamp).strftime('%b %d, %Y %H:%M:%S') def guess_is_binary(dulwich_blob): return any(b'\0' in chunk for chunk in dulwich_blob.chunked) def guess_is_image(filename): mime, _ = mimetypes.guess_type(filename) if mime is None: return False return mime.startswith('image/') def encode_for_git(s): # XXX This assumes everything to be UTF-8 encoded return s.encode('utf8') def decode_from_git(b): # XXX This assumes everything to be UTF-8 encoded return b.decode('utf8') def force_unicode(s): """Do all kinds of magic to turn `s` into unicode""" # It's already unicode, don't do anything: if isinstance(s, six.text_type): return s last_exc = None # Try some default encodings: try: return s.decode('utf-8') except UnicodeDecodeError as exc: last_exc = exc try: return s.decode(locale.getpreferredencoding()) except UnicodeDecodeError: pass if chardet is not None: # Try chardet, if available encoding = chardet.detect(s)['encoding'] if encoding is not None: return s.decode(encoding) raise last_exc # Give up. def extract_author_name(email): """Extract the name from an email address -- >>> extract_author_name("John ") "John" -- or return the address if none is given. >>> extract_author_name("noname@example.com") "noname@example.com" """ match = re.match('^(.*?)<.*?>$', email) if match: return match.group(1).strip() return email def is_hex_prefix(s): if len(s) % 2: s += '0' try: binascii.unhexlify(s) return True except binascii.Error: return False def shorten_sha1(sha1): if 20 <= len(sha1) <= 40 and is_hex_prefix(sha1): sha1 = sha1[:7] return sha1 def parent_directory(path): return os.path.split(path)[0] def subpaths(path): """Yield a `(last part, subpath)` tuple for all possible sub-paths of `path`. >>> list(subpaths("foo/bar/spam")) [('foo', 'foo'), ('bar', 'foo/bar'), ('spam', 'foo/bar/spam')] """ seen = [] for part in path.split('/'): seen.append(part) yield part, '/'.join(seen) def shorten_message(msg): return msg.split('\n')[0] def replace_dupes(ls, replacement): """Replace items in `ls` that are equal to their predecessors with `replacement`. >>> ls = [1, 2, 2, 3, 2, 2, 2] >>> replace_dupes(x, 'x') >>> ls [1, 2, 'x', 3, 2, 'x', 'x'] """ last = object() for i, elem in enumerate(ls): if last == elem: ls[i] = replacement else: last = elem def guess_git_revision(): """Try to guess whether this instance of klaus is run directly from a klaus git checkout. If it is, guess and return the currently checked-out commit SHA. If it's not (installed using pip, setup.py or the like), return None. This is used to display the "powered by klaus $VERSION" footer on each page, $VERSION being either the SHA guessed by this function or the latest release number. """ git_dir = os.path.join(os.path.dirname(__file__), '..', '.git') try: return force_unicode(subprocess.check_output( ['git', 'log', '--format=%h', '-n', '1'], cwd=git_dir ).strip()) except OSError: # Either the git executable couldn't be found in the OS's PATH # or no ".git" directory exists, i.e. this is no "bleeding-edge" installation. return None def sanitize_branch_name(name, chars='./', repl='-'): for char in chars: name = name.replace(char, repl) return name def escape_html(s): return s.replace(b'&', b'&').replace(b'<', b'<') \ .replace(b'>', b'>').replace(b'"', b'"') def tarball_basename(repo_name, rev): """Determine the name for a tarball.""" rev = sanitize_branch_name(rev, chars='/') if rev.startswith(repo_name + '-'): # If the rev is a tag name that already starts with the repo name, # skip it. return rev elif len(rev) >= 2 and rev[0] == 'v' and not rev[1].isalpha(): # If the rev is a tag name prefixed by a 'v', skip the 'v'. # So, v-1.0 -> 1.0, v1.0 -> 1.0, but vanilla -> vanilla. return "%s-%s" % (repo_name, rev[1:]) elif len(rev) == 40 and is_hex_prefix(rev): # If the rev is a commit hash, simply use that. return "%s@%s" % (repo_name, rev) else: return "%s-%s" % (repo_name, rev) klaus-1.4.0/klaus/views.py0000644000076500000240000003673013464300457015054 0ustar jstaff00000000000000from io import BytesIO import os import sys from flask import request, render_template, current_app, url_for from flask.views import View from werkzeug.wrappers import Response from werkzeug.exceptions import NotFound import dulwich.objects import dulwich.archive import dulwich.config from dulwich.object_store import tree_lookup_path try: import ctags except ImportError: ctags = None else: from klaus import ctagscache CTAGS_CACHE = ctagscache.CTagsCache() from klaus import markup from klaus.highlighting import highlight_or_render from klaus.utils import parent_directory, subpaths, force_unicode, guess_is_binary, \ guess_is_image, replace_dupes, sanitize_branch_name, encode_for_git README_FILENAMES = [b'README', b'README.md', b'README.mkdn', b'README.mdwn', b'README.markdown', b'README.rst'] def repo_list(): """Show a list of all repos and can be sorted by last update.""" if 'by-name' in request.args: sort_key = lambda repo: repo.name else: sort_key = lambda repo: (-(repo.get_last_updated_at() or -1), repo.name) repos = sorted(current_app.repos.values(), key=sort_key) return render_template('repo_list.html', repos=repos, base_href=None) def robots_txt(): """Serve the robots.txt file to manage the indexing of the site by search engines.""" return current_app.send_static_file('robots.txt') def _get_repo_and_rev(repo, rev=None, path=None): if path and rev: rev += "/" + path.rstrip("/") try: repo = current_app.repos[repo] except KeyError: raise NotFound("No such repository %r" % repo) if rev is None: rev = repo.get_default_branch() if rev is None: raise NotFound("Empty repository") i = len(rev) while i > 0: try: commit = repo.get_commit(rev[:i]) path = rev[i:].strip("/") rev = rev[:i] except (KeyError, IOError): i = rev.rfind("/", 0, i) else: break else: raise NotFound("No such commit %r" % rev) return repo, rev, path, commit def _get_submodule(repo, commit, path): """Retrieve submodule URL and path.""" submodule_blob = repo.get_blob_or_tree(commit, '.gitmodules') config = dulwich.config.ConfigFile.from_file( BytesIO(submodule_blob.as_raw_string())) key = (b'submodule', path) submodule_url = config.get(key, b'url') submodule_path = config.get(key, b'path') return (submodule_url, submodule_path) class BaseRepoView(View): """Base for all views with a repo context. The arguments `repo`, `rev`, `path` (see `dispatch_request`) define the repository, branch/commit and directory/file context, respectively -- that is, they specify what (and in what state) is being displayed in all the derived views. For example: The 'history' view is the `git log` equivalent, i.e. if `path` is "/foo/bar", only commits related to "/foo/bar" are displayed, and if `rev` is "master", the history of the "master" branch is displayed. """ def __init__(self, view_name): self.view_name = view_name self.context = {} def dispatch_request(self, repo, rev=None, path=''): """Dispatch repository, revision (if any) and path (if any). To retain compatibility with :func:`url_for`, view routing uses two arguments: rev and path, although a single path is sufficient (from Git's point of view, '/foo/bar/baz' may be a branch '/foo/bar' containing baz, or a branch '/foo' containing 'bar/baz', but never both [1]. Hence, rebuild rev and path to a single path argument, which is then later split into rev and path again, but revision now may contain slashes. [1] https://github.com/jonashaag/klaus/issues/36#issuecomment-23990266 """ self.make_template_context(repo, rev, path.strip('/')) return self.get_response() def get_response(self): return render_template(self.template_name, **self.context) def make_template_context(self, repo, rev, path): repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) try: blob_or_tree = repo.get_blob_or_tree(commit, path) except KeyError: raise NotFound("File not found") self.context = { 'view': self.view_name, 'repo': repo, 'rev': rev, 'commit': commit, 'branches': repo.get_branch_names(exclude=rev), 'tags': repo.get_tag_names(), 'path': path, 'blob_or_tree': blob_or_tree, 'subpaths': list(subpaths(path)) if path else None, 'base_href': None, } class CommitView(BaseRepoView): template_name = 'view_commit.html' class PatchView(BaseRepoView): def get_response(self): return Response( self.context['repo'].raw_commit_diff(self.context['commit']), mimetype='text/plain', ) class TreeViewMixin(object): """The logic required for displaying the current directory in the sidebar.""" def make_template_context(self, *args): super(TreeViewMixin, self).make_template_context(*args) self.context['root_tree'] = self.listdir() def listdir(self): """Return a list of directories and files in the current path of the selected commit.""" root_directory = self.get_root_directory() return self.context['repo'].listdir( self.context['commit'], root_directory ) def get_root_directory(self): root_directory = self.context['path'] if isinstance(self.context['blob_or_tree'], dulwich.objects.Blob): # 'path' is a file (not folder) name root_directory = parent_directory(root_directory) return root_directory class HistoryView(TreeViewMixin, BaseRepoView): """Show commits of a branch + path, just like `git log`. With pagination.""" template_name = 'history.html' def make_template_context(self, *args): super(HistoryView, self).make_template_context(*args) try: page = int(request.args.get('page')) except (TypeError, ValueError): page = 0 self.context['page'] = page history_length = 30 if page: skip = (self.context['page']-1) * 30 + 10 if page > 7: self.context['previous_pages'] = [0, 1, 2, None] + list(range(page))[-3:] else: self.context['previous_pages'] = range(page) else: skip = 0 history = self.context['repo'].history( self.context['commit'], self.context['path'], history_length + 1, skip ) if len(history) == history_length + 1: # At least one more commit for next page left more_commits = True # We don't want show the additional commit on this page history.pop() else: more_commits = False self.context.update({ 'history': history, 'more_commits': more_commits, }) class IndexView(TreeViewMixin, BaseRepoView): """Show commits of a branch, just like `git log`. Also, README, if available.""" template_name = 'index.html' def _get_readme(self): tree = self.context['repo'][self.context['commit'].tree] for name in README_FILENAMES: if name in tree: readme_data = self.context['repo'][tree[name][1]].data readme_filename = name return (readme_filename, readme_data) else: raise KeyError def make_template_context(self, *args): super(IndexView, self).make_template_context(*args) self.context['base_href'] = url_for( 'blob', repo=self.context['repo'].name, rev=self.context['rev'], path='' ) self.context['page'] = 0 history_length = 10 history = self.context['repo'].history( self.context['commit'], self.context['path'], history_length + 1, skip=0, ) if len(history) == history_length + 1: # At least one more commit for next page left more_commits = True # We don't want show the additional commit on this page history.pop() else: more_commits = False self.context.update({ 'history': history, 'more_commits': more_commits, }) try: (readme_filename, readme_data) = self._get_readme() except KeyError: self.context.update({ 'is_markup': None, 'rendered_code': None, }) else: readme_filename = force_unicode(readme_filename) readme_data = force_unicode(readme_data) self.context.update({ 'is_markup': markup.can_render(readme_filename), 'rendered_code': highlight_or_render(readme_data, readme_filename) }) class BaseBlobView(BaseRepoView): def make_template_context(self, *args): super(BaseBlobView, self).make_template_context(*args) if not isinstance(self.context['blob_or_tree'], dulwich.objects.Blob): raise NotFound("Not a blob") self.context['filename'] = os.path.basename(self.context['path']) class SubmoduleView(BaseRepoView): """Show an information page about a submodule.""" template_name = 'submodule.html' def make_template_context(self, repo, rev, path): repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) try: submodule_rev = tree_lookup_path( repo.__getitem__, commit.tree, encode_for_git(path))[1] except KeyError: raise NotFound("Parent path for submodule missing") try: (submodule_url, submodule_path) = _get_submodule( repo, commit, encode_for_git(path)) except KeyError: submodule_url = None submodule_path = None # TODO(jelmer): Rather than printing an information page, # redirect to the page in klaus for the repository at # submodule_path, revision submodule_rev. self.context = { 'view': self.view_name, 'repo': repo, 'rev': rev, 'commit': commit, 'branches': repo.get_branch_names(exclude=rev), 'tags': repo.get_tag_names(), 'path': path, 'subpaths': list(subpaths(path)) if path else None, 'submodule_url': force_unicode(submodule_url), 'submodule_path': force_unicode(submodule_path), 'submodule_rev': force_unicode(submodule_rev), 'base_href': None, } class BaseFileView(TreeViewMixin, BaseBlobView): """Base for FileView and BlameView.""" def render_code(self, render_markup): should_use_ctags = current_app.should_use_ctags(self.context['repo'], self.context['commit']) if should_use_ctags: if ctags is None: raise ImportError("Ctags enabled but python-ctags not installed") ctags_base_url = url_for( self.view_name, repo=self.context['repo'].name, rev=self.context['rev'], path='' ) ctags_tagsfile = CTAGS_CACHE.get_tagsfile( self.context['repo'].path, self.context['commit'].id ) ctags_args = { 'ctags': ctags.CTags(ctags_tagsfile.encode(sys.getfilesystemencoding())), 'ctags_baseurl': ctags_base_url, } else: ctags_args = {} return highlight_or_render( force_unicode(self.context['blob_or_tree'].data), self.context['filename'], render_markup, **ctags_args ) def make_template_context(self, *args): super(BaseFileView, self).make_template_context(*args) self.context.update({ 'can_render': True, 'is_binary': False, 'too_large': False, 'is_markup': False, }) binary = guess_is_binary(self.context['blob_or_tree']) too_large = sum(map(len, self.context['blob_or_tree'].chunked)) > 100*1024 if binary: self.context.update({ 'can_render': False, 'is_binary': True, 'is_image': guess_is_image(self.context['filename']), }) elif too_large: self.context.update({ 'can_render': False, 'too_large': True, }) class FileView(BaseFileView): """Shows a file rendered using ``pygmentize``.""" template_name = 'view_blob.html' def make_template_context(self, *args): super(FileView, self).make_template_context(*args) if self.context['can_render']: render_markup = 'markup' not in request.args self.context.update({ 'is_markup': markup.can_render(self.context['filename']), 'render_markup': render_markup, 'rendered_code': self.render_code(render_markup), }) class BlameView(BaseFileView): template_name = 'blame_blob.html' def make_template_context(self, *args): super(BlameView, self).make_template_context(*args) if self.context['can_render']: line_commits = self.context['repo'].blame(self.context['commit'], self.context['path']) replace_dupes(line_commits, None) self.context.update({ 'rendered_code': self.render_code(render_markup=False), 'line_commits': line_commits, }) class RawView(BaseBlobView): """Show a single file in raw for (as if it were a normal filesystem file served through a static file server). """ def get_response(self): # Explicitly set an empty mimetype. This should work well for most # browsers as they do file type recognition anyway. # The correct way would be to implement proper file type recognition here. return Response(self.context['blob_or_tree'].chunked, mimetype='') class DownloadView(BaseRepoView): """Download a repo as a tar.gz file.""" def get_response(self): basename = "%s@%s" % (self.context['repo'].name, sanitize_branch_name(self.context['rev'])) tarname = basename + ".tar.gz" headers = { 'Content-Disposition': "attachment; filename=%s" % tarname, 'Cache-Control': "no-store", # Disables browser caching } tar_stream = dulwich.archive.tar_stream( self.context['repo'], self.context['blob_or_tree'], self.context['commit'].commit_time, format="gz", prefix=encode_for_git(basename), ) return Response( tar_stream, mimetype="application/x-tgz", headers=headers ) history = HistoryView.as_view('history', 'history') index = IndexView.as_view('index', 'index') commit = CommitView.as_view('commit', 'commit') patch = PatchView.as_view('patch', 'patch') blame = BlameView.as_view('blame', 'blame') blob = FileView.as_view('blob', 'blob') raw = RawView.as_view('raw', 'raw') download = DownloadView.as_view('download', 'download') submodule = SubmoduleView.as_view('submodule', 'submodule') klaus-1.4.0/klaus.10000644000076500000240000000263312703151602013411 0ustar jstaff00000000000000.TH KLAUS "1" "December 2015" "klaus 4e82832" "User Commands" .SH NAME klaus \- easy to set up Git web viewer .SH SYNOPSIS .B klaus [\fIOPTION\fR]... [\fIDIR\fR]... .SH DESCRIPTION Klaus is a simple and easy-to-set-up Git web viewer that Just Works\(tm. .PP Note that the klaus binary just starts a test instance. The klaus script uses wsgiref internally which doesn't scale at all - it's single-threaded and non-asynchronous. .PP It supports syntax highlighting and Git Smart HTTP. .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-\-host\fR HOST default: 127.0.0.1 .TP \fB\-\-port\fR PORT default: 8080 .TP \fB\-\-site\-name\fR SITE_NAME site name showed in header. default: your hostname .TP \fB\-\-version\fR print version number .TP \fB\-b\fR, \fB\-\-browser\fR open klaus in a browser on server start .TP \fB\-B\fR BROWSER, \fB\-\-with\-browser\fR BROWSER specify which browser to use with \fB\-\-browser\fR .TP \fB\-\-ctags\fR {none,tags\-and\-branches,ALL} enable ctags for which revisions? default: none. WARNING: Don't use 'ALL' for public servers! .SS "Git Smart HTTP:" .TP \fB\-\-smarthttp\fR enable Git Smart HTTP serving .TP \fB\-\-htdigest\fR FILE use credentials from FILE .SS "Development flags:" .IP DO NOT USE IN PRODUCTION! .TP \fB\-\-debug\fR Enable Werkzeug debugger and reloader .SH AUTHORS Copyright \(co 2011-2015 Jonas Haag and contributors (see Git logs). klaus-1.4.0/klaus.egg-info/0000755000076500000240000000000013464301406015021 5ustar jstaff00000000000000klaus-1.4.0/klaus.egg-info/PKG-INFO0000644000076500000240000000134013464301406016114 0ustar jstaff00000000000000Metadata-Version: 1.1 Name: klaus Version: 1.4.0 Summary: The first Git web viewer that Just Works™. Home-page: https://github.com/jonashaag/klaus Author: Jonas Haag Author-email: jonas@lophus.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Software Development :: Version Control Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 klaus-1.4.0/klaus.egg-info/SOURCES.txt0000644000076500000240000000171313464301406016707 0ustar jstaff00000000000000MANIFEST.in README.rst klaus.1 setup.py bin/klaus klaus/__init__.py klaus/ctagscache.py klaus/ctagsutils.py klaus/diff.py klaus/highlighting.py klaus/markup.py klaus/repo.py klaus/utils.py klaus/views.py klaus.egg-info/PKG-INFO klaus.egg-info/SOURCES.txt klaus.egg-info/dependency_links.txt klaus.egg-info/not-zip-safe klaus.egg-info/requires.txt klaus.egg-info/top_level.txt klaus/contrib/__init__.py klaus/contrib/app_args.py klaus/contrib/wsgi.py klaus/contrib/wsgi_autoreload.py klaus/contrib/wsgi_autoreloading.py klaus/static/favicon.png klaus/static/klaus.css klaus/static/klaus.js klaus/static/pygments.css klaus/static/robots.txt klaus/templates/base.html klaus/templates/blame_blob.html klaus/templates/history.html klaus/templates/history.inc.html klaus/templates/index.html klaus/templates/repo_list.html klaus/templates/skeleton.html klaus/templates/submodule.html klaus/templates/tree.inc.html klaus/templates/view_blob.html klaus/templates/view_commit.htmlklaus-1.4.0/klaus.egg-info/dependency_links.txt0000644000076500000240000000000113464301406021067 0ustar jstaff00000000000000 klaus-1.4.0/klaus.egg-info/not-zip-safe0000644000076500000240000000000112542627235017256 0ustar jstaff00000000000000 klaus-1.4.0/klaus.egg-info/requires.txt0000644000076500000240000000006513464301406017422 0ustar jstaff00000000000000six flask pygments dulwich>=0.19.3 httpauth humanize klaus-1.4.0/klaus.egg-info/top_level.txt0000644000076500000240000000000613464301406017547 0ustar jstaff00000000000000klaus klaus-1.4.0/setup.cfg0000644000076500000240000000004613464301406014031 0ustar jstaff00000000000000[egg_info] tag_build = tag_date = 0 klaus-1.4.0/setup.py0000644000076500000240000000264013464301021013715 0ustar jstaff00000000000000# encoding: utf-8 from setuptools import setup def install_data_files_hack(): # This is a clever hack to circumvent distutil's data_files # policy "install once, find never". Definitely a TODO! # -- https://groups.google.com/group/comp.lang.python/msg/2105ee4d9e8042cb from distutils.command.install import INSTALL_SCHEMES for scheme in INSTALL_SCHEMES.values(): scheme['data'] = scheme['purelib'] install_data_files_hack() requires = ['six', 'flask', 'pygments', 'dulwich>=0.19.3', 'httpauth', 'humanize'] setup( name='klaus', version='1.4.0', author='Jonas Haag', author_email='jonas@lophus.org', packages=['klaus', 'klaus.contrib'], scripts=['bin/klaus'], include_package_data=True, zip_safe=False, url='https://github.com/jonashaag/klaus', description='The first Git web viewer that Just Works™.', long_description=__doc__, classifiers=[ "Development Status :: 5 - Production/Stable", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Version Control", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: ISC License (ISCL)", "Programming Language :: Python", "Programming Language :: Python :: 2.7", ], install_requires=requires, )