pax_global_header00006660000000000000000000000064126064472070014522gustar00rootroot0000000000000052 comment=cda99f5fe2bde2763cf53446a4e672966b67116a klaus-0.7.1/000077500000000000000000000000001260644720700126465ustar00rootroot00000000000000klaus-0.7.1/.gitignore000066400000000000000000000001211260644720700146300ustar00rootroot00000000000000*.pyc /bin/ env/ .idea *.swp tests/repos/build *.egg-info build/ dist/ .DS_Store klaus-0.7.1/.travis.yml000066400000000000000000000005001260644720700147520ustar00rootroot00000000000000sudo: false language: python python: - "2.6" - "2.7" addons: apt: packages: - exuberant-ctags install: - "pip install ." - "pip install requests" - "pip install python-ctags" script: - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - ./runtests.sh klaus-0.7.1/CHANGELOG.rst000066400000000000000000000116411260644720700146720ustar00rootroot00000000000000Changelog ========= 0.7.1 (Oct 11, 2015) -------------------- * Fix #136: wrong .diff URL generated if klaus is mounted under a prefix (John Ko) 0.7.0 (Oct 7, 2015) ------------------- * Add ctags support (see wiki) (Jonas Haag) * Append ".diff" or ".patch" to a commit URL and you'll be given a plaintext patch (like you can do at GitHub) (Jonas Haag) * Fix JavaScript line highlighter after window reload (Jonas Haag) 0.6.0 (Aug 6, 2015) -------------------- * Basic blame view (Martin Zimmermann, Jonas Haag) * Bug #133: Fix line highlighter (Jonas Haag) 0.5.0 (July 27, 2015) --------------------- * Experimental support for Python 3. (Jonas Haag) * #126: Show committer if different from author (Jonas Haag) * Bug #130: Fix highlighting for "No newline at the end of file" (Jonas Haag) 0.4.10 (June 28, 2015) ---------------------- * Add option to require HTTP authentication for all parts of the Web interface (Jonas Haag) * Add option to disable authentication entirely for Smart HTTP -- DANGER ZONE! (Jonas Haag) * Add some unit tests; Travis (Jonas Haag) * Bugs #116, #124, #128: Fix ``klaus.contrib.wsgi_autoreload`` (William Hughes, Yed Podtrzitko) * Bug #113: Fix filenames containing whitespace in diffs. (Jonas Haag) * Bug #115: In diffs, it now says "(new empty file)" rather than "(no changes)" when an empty file has been added. (Jonas Haag) * Bug #125: Fix tarball download on Python 2.6 (Dana Runge) 0.4.9 (April 13, 2015) ---------------------- * Add option to auto-launch a web-browser on startup (@rjw57) * Bug #104: "git" executable unnecessarily required to be available (@Mechazawa) 0.4.8 (June 22, 2014) --------------------- * Fix .tar.gz download if repository contains git submodule. (Jonas Haag) 0.4.7 (June 22, 2014) --------------------- * #87, #98: Add favicon (@lb1a) * #35, #95: Add default robots.txt file (@lb1a) * #93, #94, #101: Add "download as .tar.gz archive" feature. (@Mechazawa, Jonas Haag) * Bug #90: htdigest file handling broken in contrib.wsgi. (Philip Dexter) * Bug #99/#53: Misbehaving mimetype recognition (@Mechazawa) 0.4.6 (Mar 5, 2014) ------------------- * #89: Work around a bug in Dulwich 0.9.5: https://github.com/jelmer/dulwich/issues/144 (Klaus Alexander Seistrup, Jonas Haag) 0.4.5 (Mar 5, 2014) ------------------- * Bugfix release for bugfix release 0.4.4. (Daniel Krüger, Jonas Haag) 0.4.4 (Feb 21, 2014) ------------------- * Fix syntax highlighting in case multiple different file formats share the same file extension. Rely on Pygments to select the best matching lexer for us. (Gnewbee, Jonas Haag) 0.4.3 (Feb 20, 2014) -------------------- * Bug #86: Empty repo name if klaus is fed a ".git" directory. Now: name of parent directory, i.e. /foo/bar/.git has the name "bar". (David Wahlund) 0.4.2 (Jan 21, 2014) -------------------- * Bug #83: Wrong version of Dulwich dependency in ``setup.py`` 0.4.1 (Jan 17, 2014) -------------------- * Bug #82: Include ``contrib/*`` in the distribution as ``klaus.contrib.*``. 0.4 (Jan 16, 2014) ------------------ * NOTE TO CONTRIBUTORS -- HISTORY REWRITTEN: See 46bcec1a8e21d510f3af3c9e2d19bc388b20c753 * Moved ``klaus.wsgi`` to ``klaus.contrib.wsgi`` * New autoreloader (see ``klaus/contrib/wsgi_autoreload.py``) WSGI middleware that watches a directory for repository additions/deletions (i.e., no need to restart klaus anymore). Also see page in wiki. (Jonas Haag) * Commit view: - Wrap long lines (Brendan Molloy) - Add change summary and make file diffs toggleable (A. Svensson, Jonas Haag) - Speed up page rendering thanks to Javascript optimization (Martin Zimmermann, Jonas Haag) 0.3 (Jun 10, 2013) ------------------ * #57: Better "N minutes/hours/weeks ago" strings (Jonas Haag) * #59: Show download link for binary files / large files * #56: Markdown renderer: enable "TOC" and "extra" extensions (@ar4s, Jonas Haag) * Bug #61: Don't crash on repos without "master" branch (Jonas Haag) * Bug #60: Don't crash if "/blob/" URL is requested with non-file argument * Don't crash on completely empty repos (Jonas Haag) 0.2.3 (May 08, 2013) -------------------- * Fix an issue with the version/revision indicator bottom-right of the page (Jonas Haag) 0.2.2 (Apr 5, 2013) ------------------- * #49: Support for short descriptions using `.git/description` file (Ernest W. Durbin III) * Bug #53: Misbehaving mimetype recognition (Jonas Haag) 0.2.1 (Jan 29, 2013) -------------------- * Tags work again (Jonas Haag) * Apache/mod_wsgi deployment docs (Alex Marandon) * Bug #43: ``bin/klaus``: ``--site-name`` did only accept ASCII strings (Alex Marandon, Martin Zimmermann, Jonas Haag) * More robust routing (Jonas Haag) 0.2 (Dec 3, 2012) ----------------- * Rewrite/port to Flask/Werkzeug (Martin Zimmermann, Jonas Haag). * Git Smart HTTP support with HTTP authentication (Martin Zimmermann, Jonas Haag) * Tag selector (Jonas Haag) * Switch to ISC license 0.1 (unreleased) ---------------- BSD-licensed initial version, based on Nano "web framework" (Jonas Haag) klaus-0.7.1/LICENSE000066400000000000000000000021521260644720700136530ustar00rootroot00000000000000https://github.com/jonashaag/klaus Copyright (c) 2011-2013 Jonas Haag and contributors (see Git logs). Favicon: Git Logo by Jason Long , licensed under the Creative Commons Attribution 3.0 Unported License. Feature contributions --------------------- * Werkzeug port -- Martin Zimmermann * Git Smart HTTP support -- Martin Zimmermann Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. klaus-0.7.1/MANIFEST.in000066400000000000000000000001051260644720700144000ustar00rootroot00000000000000recursive-include klaus/static * recursive-include klaus/templates * klaus-0.7.1/README.rst000066400000000000000000000050611260644720700143370ustar00rootroot00000000000000.. image:: https://travis-ci.org/jonashaag/klaus.svg?branch=master :target: https://travis-ci.org/jonashaag/klaus 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.) :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. `_ :-) Features -------- * Super easy to set up -- no configuration required * Syntax highlighting * Git Smart HTTP support |img1|_ |img2|_ |img3|_ .. |img1| image:: http://i.imgur.com/2XhZIgw.png .. |img2| image:: http://i.imgur.com/6LjC8Cl.png .. |img3| image:: http://i.imgur.com/EYJdQwv.png .. _img1: http://i.imgur.com/MV3uFvw.png .. _img2: http://i.imgur.com/9HEZ3ro.png .. _img3: http://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-0.7.1/RELEASE_PROCESS000066400000000000000000000001671260644720700150530ustar00rootroot00000000000000* CHANGELOG.rst * Change version number in - setup.py - klaus/__init__.py * python setup.py sdist upload * git tag klaus-0.7.1/bin/000077500000000000000000000000001260644720700134165ustar00rootroot00000000000000klaus-0.7.1/bin/klaus000077500000000000000000000075021260644720700144670ustar00rootroot00000000000000#!/usr/bin/env python2 # coding: utf-8 import sys import os import argparse import webbrowser from dulwich.errors import NotGitRepository from dulwich.repo import Repo from klaus import make_app 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(epilog="Gemüse kaufen!") 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('-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.htdigest and not args.smarthttp: print >> sys.stderr, "ERROR: --htdigest option has no effect without --smarthttp enabled" return 1 if not args.repos: print >> sys.stderr, "WARNING: No repositories supplied -- syntax is 'klaus dir1 dir2...'." 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 >> sys.stderr, "ERROR: Exuberant ctags not installed (or 'ctags' binary isn't *Exuberant* ctags)" return 1 try: import ctags except ImportError: raise ImportError("Please install 'python-ctags' 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-0.7.1/klaus/000077500000000000000000000000001260644720700137655ustar00rootroot00000000000000klaus-0.7.1/klaus/__init__.py000066400000000000000000000174201260644720700161020ustar00rootroot00000000000000import 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 '0.7.1' 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', '/'), ('blob', '//blob//'), ('blob', '//blob//'), ('blame', '//blame//'), ('blame', '//blame//'), ('raw', '//raw//'), ('raw', '//raw//'), ('commit', '//commit//'), ('patch', '//commit/.diff'), ('patch', '//commit/.patch'), ('history', '//'), ('history', '//tree//'), ('history', '//tree//'), ('robots_txt', '/robots.txt/'), ('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.SubUri(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.SubUri(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-0.7.1/klaus/contrib/000077500000000000000000000000001260644720700154255ustar00rootroot00000000000000klaus-0.7.1/klaus/contrib/__init__.py000066400000000000000000000000001260644720700175240ustar00rootroot00000000000000klaus-0.7.1/klaus/contrib/wsgi.py000066400000000000000000000010101260644720700167400ustar00rootroot00000000000000import os from klaus import make_app if 'KLAUS_HTDIGEST_FILE' in os.environ: with open(os.environ['KLAUS_HTDIGEST_FILE']) as file: application = make_app( os.environ['KLAUS_REPOS'].split(), os.environ['KLAUS_SITE_NAME'], os.environ.get('KLAUS_USE_SMARTHTTP'), file, ) else: application = make_app( os.environ['KLAUS_REPOS'].split(), os.environ['KLAUS_SITE_NAME'], os.environ.get('KLAUS_USE_SMARTHTTP'), None, ) klaus-0.7.1/klaus/contrib/wsgi_autoreload.py000066400000000000000000000042461260644720700211750ustar00rootroot00000000000000from __future__ import print_function import os import time import threading import warnings 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 if 'KLAUS_REPOS' in os.environ: warnings.warn("use KLAUS_REPOS_ROOT instead of KLAUS_REPOS for the autoreloader apps", DeprecationWarning) if 'KLAUS_HTDIGEST_FILE' in os.environ: with open(os.environ['KLAUS_HTDIGEST_FILE']) as file: application = make_autoreloading_app( os.environ.get('KLAUS_REPOS_ROOT') or os.environ['KLAUS_REPOS'], os.environ['KLAUS_SITE_NAME'], os.environ.get('KLAUS_USE_SMARTHTTP'), file, ) else: application = make_autoreloading_app( os.environ.get('KLAUS_REPOS_ROOT') or os.environ['KLAUS_REPOS'], os.environ['KLAUS_SITE_NAME'], os.environ.get('KLAUS_USE_SMARTHTTP'), ) klaus-0.7.1/klaus/ctagscache.py000066400000000000000000000163151260644720700164320ustar00rootroot00000000000000"""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 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() 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-0.7.1/klaus/ctagsutils.py000066400000000000000000000024611260644720700165240ustar00rootroot00000000000000import os import subprocess import shutil import tempfile from klaus.utils import check_output def check_have_exuberant_ctags(): """Check that the 'ctags' binary is *Exuberant* ctags (not etags etc)""" try: return "Exuberant" in 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-0.7.1/klaus/diff.py000066400000000000000000000137241260644720700152560ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ lodgeit.lib.diff ~~~~~~~~~~~~~~~~ Render a nice diff between two things. :copyright: 2007 by Armin Ronacher. :license: BSD """ import re from cgi import escape def prepare_udiff(udiff, **kwargs): """Prepare an udiff for a template.""" return DiffRenderer(udiff).prepare(**kwargs) class DiffRenderer(object): """Give it a unified diff and it renders you a beautiful html diff :-) """ _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@') def __init__(self, udiff): """:param udiff: a text in udiff format""" self.lines = [escape(line) for line in udiff.splitlines()] def _extract_filename(self, line): """ Extract file name from unified diff line: --- a/foo/bar ==> foo/bar +++ b/foo/bar ==> foo/bar """ if line.startswith(("--- /dev/null", "+++ /dev/null")): return line[len("--- "):] else: return line[len("--- a/"):] def _highlight_line(self, line, next): """Highlight inline changes in both lines.""" start = 0 limit = min(len(line['line']), len(next['line'])) while start < limit and line['line'][start] == next['line'][start]: start += 1 end = -1 limit -= start while -end <= limit and line['line'][end] == next['line'][end]: end -= 1 end += 1 if start or end: def do(l): last = end + len(l['line']) if l['action'] == 'add': tag = 'ins' else: tag = 'del' l['line'] = u'%s<%s>%s%s' % ( l['line'][:start], tag, l['line'][start:last], tag, l['line'][last:] ) do(line) do(next) def prepare(self, want_header=True): """Parse the diff an return data for the template.""" in_header = True header = [] lineiter = iter(self.lines) files = [] try: line = next(lineiter) while 1: # continue until we found the old file if not line.startswith('--- '): if in_header: header.append(line) line = next(lineiter) continue if header and all(x.strip() for x in header): if want_header: files.append({'is_header': True, 'lines': header}) header = [] in_header = False chunks = [] files.append({ 'is_header': False, 'old_filename': self._extract_filename(line), 'new_filename': self._extract_filename(next(lineiter)), 'additions': 0, 'deletions': 0, 'chunks': chunks }) line = next(lineiter) while line: match = self._chunk_re.match(line) if not match: in_header = True break lines = [] chunks.append(lines) old_line, old_end, new_line, new_end = \ [int(x or 1) for x in match.groups()] old_line -= 1 new_line -= 1 old_end += old_line new_end += new_line line = next(lineiter) while old_line < old_end or new_line < new_end: if line: command, line = line[0], line[1:] else: command = ' ' affects_old = affects_new = False if command == '+': affects_new = True action = 'add' files[-1]['additions'] += 1 elif command == '-': affects_old = True action = 'del' files[-1]['deletions'] += 1 else: affects_old = affects_new = True action = 'unmod' old_line += affects_old new_line += affects_new lines.append({ 'old_lineno': affects_old and old_line or u'', 'new_lineno': affects_new and new_line or u'', 'action': action, 'line': line, 'no_newline': False, }) # Skip "no newline at end of file" markers line = next(lineiter) if line == r"\ No newline at end of file": lines[-1]['no_newline'] = True line = next(lineiter) except StopIteration: pass # highlight inline changes for file in files: if file['is_header']: continue for chunk in file['chunks']: lineiter = iter(chunk) try: while True: line = next(lineiter) if line['action'] != 'unmod': nextline = next(lineiter) if nextline['action'] == 'unmod' or \ nextline['action'] == line['action']: continue self._highlight_line(line, nextline) except StopIteration: pass return files klaus-0.7.1/klaus/highlighting.py000066400000000000000000000104651260644720700170120ustar00rootroot00000000000000from 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 = self.get_best_ctags_matches(matches) if not best_matches: return None, None else: return best_matches[0]['file'], best_matches[0]['lineNumber'] def _get_all_ctags_matches(self, token): FIELDS = ('file', 'lineNumber', 'kind', 'language') from ctags import TagEntry entry = TagEntry() # target "buffer" for ctags if self._ctags.find(entry, token, 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['language'] == self.language, 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'] != 'i', super(KlausPythonFormatter, self).get_best_ctags_matches(matches) ) def pygmentize(code, filename, render_markup, 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-0.7.1/klaus/markup.py000066400000000000000000000025221260644720700156370ustar00rootroot00000000000000import 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', '.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': 'quiet'} 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-0.7.1/klaus/repo.py000066400000000000000000000221301260644720700153020ustar00rootroot00000000000000import os import io import stat import dulwich, dulwich.patch from klaus.utils import check_output, force_unicode, parent_directory, encode_for_git, decode_from_git from klaus.diff import prepare_udiff 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 """ return self.path.replace(".git", "").rstrip(os.sep).split(os.sep)[-1] 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) if refs: return refs[0].commit_time 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('refs/tags/').values() branch_shas = self.refs.as_dict('refs/heads/').values() return tag_shas + 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(commit.id) if path: cmd.extend(['--', path]) output = 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', commit.id, '--', path] output = check_output(cmd, cwd=os.path.abspath(self.path)) sha1_sums = [line[:40] for line in output.strip().split(b'\n')] return [self[sha1] for sha1 in sha1_sums] def get_blob_or_tree(self, commit, path): """Return the Git tree or blob object for `path` at `commit`.""" tree_or_blob = self[commit.tree] # Still a tree here but may turn into # a blob somewhere in the loop. for part in path.strip('/').split('/'): if part: if isinstance(tree_or_blob, dulwich.objects.Blob): # Blobs don't have sub-files/folders. raise KeyError tree_or_blob = self[tree_or_blob[encode_for_git(part)][1]] return tree_or_blob def listdir(self, commit, path): """Return a list of directories and files in given directory.""" dirs, files = [], [] for entry in self.get_blob_or_tree(commit, path).items(): name, entry = entry.path, entry.in_path(encode_for_git(path)) if entry.mode & stat.S_IFDIR: dirs.append((name.lower(), name, entry.path)) else: files.append((name.lower(), name, entry.path)) files.sort() dirs.sort() if path: dirs.insert(0, (None, '..', parent_directory(path))) return {'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: # Check for binary files -- can't show diffs for these if newsha and guess_is_binary(self[newsha]) or \ oldsha and guess_is_binary(self[oldsha]): file_changes.append({ 'is_binary': True, 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': None }) continue except KeyError: # newsha/oldsha are probably related to submodules. # Dulwich will handle that. pass bytesio = io.BytesIO() dulwich.patch.write_object_diff(bytesio, self.object_store, (oldpath, oldmode, oldsha), (newpath, newmode, newsha)) files = prepare_udiff(decode_from_git(bytesio.getvalue()), want_header=False) if not files: # the diff module doesn't handle deletions/additions # of empty files correctly. file_changes.append({ 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': [], 'additions': 0, 'deletions': 0, }) else: change = files[0] summary['nadditions'] += change['additions'] summary['ndeletions'] += change['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-0.7.1/klaus/static/000077500000000000000000000000001260644720700152545ustar00rootroot00000000000000klaus-0.7.1/klaus/static/favicon.png000066400000000000000000000054741260644720700174210ustar00rootroot00000000000000PNG  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-0.7.1/klaus/static/klaus.css000066400000000000000000000222651260644720700171140ustar00rootroot00000000000000@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 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; } /* 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 img { max-width: 100%; padding: 1px; } .blobview img, .blobview .markup { border: 1px solid #e0e0e0; } .blobview .markup h1:first-child { margin-top: 8px; } .blobview .markup { padding: 0 10px; } .blobview .markup pre { padding: 10px 12px; background-color: #f9f9f9; border: 1px solid #e0e0e0; } /* 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-0.7.1/klaus/static/klaus.js000066400000000000000000000035701260644720700167360ustar00rootroot00000000000000var 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-0.7.1/klaus/static/pygments.css000066400000000000000000000065361260644720700176460ustar00rootroot00000000000000/* 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-0.7.1/klaus/static/robots.txt000066400000000000000000000002121260644720700173200ustar00rootroot00000000000000User-agent: * Allow: /*/blob/master/ Allow: /*/tree/master/ Allow: /*/raw/master/ Disallow: /*/tree/ Disallow: /*/blob/ Disallow: /*/raw/ klaus-0.7.1/klaus/tarutils.py000066400000000000000000000054771260644720700162230ustar00rootroot00000000000000import os import stat import tarfile from io import BytesIO from contextlib import closing from klaus.utils import decode_from_git class ListBytesIO(object): """Turn a list of bytestrings into a file-like object. This is similar to creating a `BytesIO` from a concatenation of the bytestring list, but saves memory by NOT creating one giant bytestring first:: BytesIO(b''.join(list_of_bytestrings)) =~= ListBytesIO(list_of_bytestrings) """ def __init__(self, contents): self.contents = contents self.pos = (0, 0) def read(self, maxbytes=None): if maxbytes < 0: maxbytes = float('inf') buf = [] chunk, cursor = self.pos while chunk < len(self.contents): if maxbytes < len(self.contents[chunk]) - cursor: buf.append(self.contents[chunk][cursor:cursor+maxbytes]) cursor += maxbytes self.pos = (chunk, cursor) break else: buf.append(self.contents[chunk][cursor:]) maxbytes -= len(self.contents[chunk]) - cursor chunk += 1 cursor = 0 self.pos = (chunk, cursor) return b''.join(buf) def tar_stream(repo, tree, mtime, format=''): """Return a generator that lazily assembles a .tar.gz archive, yielding it in pieces (bytestrings). To obtain the complete .tar.gz binary file, simply concatenate these chunks. 'repo' and 'tree' are the dulwich Repo and Tree objects the archive shall be created from. 'mtime' is a UNIX timestamp that is assigned as the modification time of all files in the resulting .tar.gz archive. """ buf = BytesIO() with closing(tarfile.open(None, "w:%s" % format, buf)) as tar: for entry_abspath, entry in walk_tree(repo, tree): try: blob = repo[entry.sha] except KeyError: # Entry probably refers to a submodule, which we don't yet support. continue data = ListBytesIO(blob.chunked) info = tarfile.TarInfo() info.name = entry_abspath info.size = blob.raw_length() info.mode = entry.mode info.mtime = mtime tar.addfile(info, data) yield buf.getvalue() buf.truncate(0) buf.seek(0) yield buf.getvalue() def walk_tree(repo, tree, root=''): """Recursively walk a dulwich Tree, yielding tuples of (absolute path, TreeEntry) along the way. """ for entry in tree.iteritems(): entry_abspath = os.path.join(root, decode_from_git(entry.path)) if stat.S_ISDIR(entry.mode): for _ in walk_tree(repo, repo[entry.sha], entry_abspath): yield _ else: yield (entry_abspath, entry) klaus-0.7.1/klaus/templates/000077500000000000000000000000001260644720700157635ustar00rootroot00000000000000klaus-0.7.1/klaus/templates/base.html000066400000000000000000000023401260644720700175620ustar00rootroot00000000000000{% 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-0.7.1/klaus/templates/blame_blob.html000066400000000000000000000023021260644720700207240ustar00rootroot00000000000000{% 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.id | shorten_sha1 }}
                {%- endif -%}
              {%- endfor -%}
            
{% autoescape false %} {{ rendered_code }} {% endautoescape %}
{% endif %}
{% endblock %} klaus-0.7.1/klaus/templates/history.html000066400000000000000000000041101260644720700203460ustar00rootroot00000000000000{% extends 'base.html' %} {% block title %} History of {% if path %}{{ path }} - {% endif %} {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %} {% 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('history', repo=repo.name, _external=True) }} {% endif %}

{{ pagination() }}
{{ pagination() }}
{% endblock %} klaus-0.7.1/klaus/templates/repo_list.html000066400000000000000000000016311260644720700206520ustar00rootroot00000000000000{% extends 'skeleton.html' %} {% block title %}Repository list{% endblock %} {% block content %}

Repositories (order by last update)

{% endblock %} klaus-0.7.1/klaus/templates/skeleton.html000066400000000000000000000015311260644720700204750ustar00rootroot00000000000000 {% 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-0.7.1/klaus/templates/tree.inc.html000066400000000000000000000011561260644720700203630ustar00rootroot00000000000000

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

klaus-0.7.1/klaus/templates/view_blob.html000066400000000000000000000031461260644720700206250ustar00rootroot00000000000000{% 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-0.7.1/klaus/templates/view_commit.html000066400000000000000000000137461260644720700212060ustar00rootroot00000000000000{% extends 'base.html' %} {% block extra_header %}{% endblock %} {# no branch selector on commits #} {% 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 -%} {%- if line.new_lineno -%} {%- else -%} {%- endif -%} {%- else %} {%- if line.old_lineno -%} {%- else -%} {%- endif -%} {% 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 }}{{ 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-0.7.1/klaus/utils.py000066400000000000000000000130261260644720700155010ustar00rootroot00000000000000# encoding: utf-8 import os import re import time import datetime import mimetypes import locale import six try: import chardet except ImportError: chardet = None from humanize import naturaltime 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): 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 # Try some default encodings: try: return s.decode('utf-8') except UnicodeDecodeError as exc: pass 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 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 shorten_sha1(sha1): if re.match(r'[a-z\d]{20,40}', 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 try: from subprocess import check_output except ImportError: # Python < 2.7 fallback, stolen from the 2.7 stdlib def check_output(*popenargs, **kwargs): from subprocess import Popen, PIPE, CalledProcessError if 'stdout' in kwargs: raise ValueError('stdout argument not allowed, it will be overridden.') process = Popen(stdout=PIPE, *popenargs, **kwargs) output, _ = process.communicate() retcode = process.poll() if retcode: cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] raise CalledProcessError(retcode, cmd, output=output) return output 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 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 klaus-0.7.1/klaus/views.py000066400000000000000000000241161260644720700155000ustar00rootroot00000000000000import os 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 from dulwich.objects import Blob try: import ctags from klaus import ctagscache except ImportError: ctags = None from klaus import markup, tarutils from klaus.highlighting import pygmentize from klaus.utils import parent_directory, subpaths, force_unicode, guess_is_binary, \ guess_is_image, replace_dupes if ctags: CTAGS_CACHE = ctagscache.CTagsCache() def repo_list(): """Show a list of all repos and can be sorted by last update.""" if 'by-last-update' in request.args: sort_key = lambda repo: repo.get_last_updated_at() reverse = True else: sort_key = lambda repo: repo.name reverse = False repos = sorted(current_app.repos.values(), key=sort_key, reverse=reverse) return render_template('repo_list.html', repos=repos) def robots_txt(): """Serve the robots.txt file to manage the indexing of the site by search enginges.""" return current_app.send_static_file('robots.txt') 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=''): 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): 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") try: commit = repo.get_commit(rev) except KeyError: raise NotFound("No such commit %r" % rev) 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, } 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'], 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 if page: history_length = 30 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: history_length = 10 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 BaseBlobView(BaseRepoView): def make_template_context(self, *args): super(BaseBlobView, self).make_template_context(*args) if not isinstance(self.context['blob_or_tree'], Blob): raise NotFound("Not a blob") self.context['filename'] = os.path.basename(self.context['path']) 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: 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), 'ctags_baseurl': ctags_base_url, } else: ctags_args = {} return pygmentize( 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): tarname = "%s@%s.tar.gz" % (self.context['repo'].name, self.context['rev']) headers = { 'Content-Disposition': "attachment; filename=%s" % tarname, 'Cache-Control': "no-store", # Disables browser caching } tar_stream = tarutils.tar_stream( self.context['repo'], self.context['blob_or_tree'], self.context['commit'].commit_time, format="gz" ) return Response( tar_stream, mimetype="application/x-tgz", headers=headers ) history = HistoryView.as_view('history', 'history') 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') klaus-0.7.1/runtests.sh000077500000000000000000000004011260644720700150670ustar00rootroot00000000000000#!/bin/sh -e ( cd tests/repos/ rm -rf build for maker in scripts/*; do builddir="build/`basename $maker`" mkdir -p $builddir ( cd $builddir; ../../$maker ) done ) tests="$1" if [ -z "$tests" ]; then tests="tests/" fi py.test $tests -v klaus-0.7.1/setup.py000066400000000000000000000031131260644720700143560ustar00rootroot00000000000000# encoding: utf-8 import glob 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.9.6', 'httpauth', 'humanize'] try: import argparse # not available for Python 2.6 except ImportError: requires.append('argparse') setup( name='klaus', version='0.7.1', 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.6", "Programming Language :: Python :: 2.7", ], install_requires=requires, ) klaus-0.7.1/tests/000077500000000000000000000000001260644720700140105ustar00rootroot00000000000000klaus-0.7.1/tests/__init__.py000066400000000000000000000000001260644720700161070ustar00rootroot00000000000000klaus-0.7.1/tests/credentials.htdigest000066400000000000000000000000571260644720700200440ustar00rootroot00000000000000testuser:test:41e45fc24eaddd7545c4819e49d1198b klaus-0.7.1/tests/repos/000077500000000000000000000000001260644720700151405ustar00rootroot00000000000000klaus-0.7.1/tests/repos/scripts/000077500000000000000000000000001260644720700166275ustar00rootroot00000000000000klaus-0.7.1/tests/repos/scripts/dont-render000077500000000000000000000003731260644720700210010ustar00rootroot00000000000000#!/bin/bash -e git init # Create binary echo -e 'abc \x0 def' > binary git add binary # Create image echo -e 'abc \x0 def' > image.jpg git add image.jpg # Create too large file yes | head -n 102400 > toolarge git add toolarge git commit -am first klaus-0.7.1/tests/repos/scripts/no-newline-at-end-of-file000077500000000000000000000002251260644720700233140ustar00rootroot00000000000000#!/bin/bash -e git init echo 1 > test echo -n 2 >> test git add test git commit -m old echo 1 > test echo 2 >> test git add test git commit -m new klaus-0.7.1/tests/repos/scripts/test_repo000077500000000000000000000003721260644720700205630ustar00rootroot00000000000000#!/bin/bash -e git init echo "int a;" > test.c echo "function test() {}" > test.js git add test.c git add test.js git commit -m "Add some code" git commit --allow-empty -m "Empty commit 1" git tag tag1 git commit --allow-empty -m "Empty commit 2" klaus-0.7.1/tests/test_blame.py000066400000000000000000000011061260644720700164770ustar00rootroot00000000000000import requests from .utils import * def test_dont_show_blame_link(): with serve(): for file in ["binary", "image.jpg", "toolarge"]: response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/" + file).content assert "blame" not in response def test_dont_render_blame(): """Don't render blame even if someone navigated to the blame site by accident.""" with serve(): for file in ["binary", "image.jpg", "toolarge"]: response = requests.get(TEST_REPO_DONT_RENDER_URL + "blame/HEAD/" + file).content assert "Can't show blame" in response klaus-0.7.1/tests/test_ctags.py000066400000000000000000000001351260644720700165210ustar00rootroot00000000000000from io import BytesIO import requests import tarfile import contextlib from .utils import * klaus-0.7.1/tests/test_make_app.py000066400000000000000000000122221260644720700171750ustar00rootroot00000000000000import os import re import subprocess import tempfile import shutil import klaus import pytest import requests import requests.auth from .utils import * def test_htdigest_file_without_smarthttp_or_require_browser_auth(): with pytest.raises(ValueError): klaus.make_app([], None, htdigest_file=object()) def test_unauthenticated_push_and_require_browser_auth(): with pytest.raises(ValueError): klaus.make_app([], None, use_smarthttp=True, unauthenticated_push=True, require_browser_auth=True) def test_unauthenticated_push_without_use_smarthttp(): with pytest.raises(ValueError): klaus.make_app([], None, unauthenticated_push=True) def test_unauthenticated_push_with_disable_push(): with pytest.raises(ValueError): klaus.make_app([], None, unauthenticated_push=True, disable_push=True) def options_test(make_app_args, expected_permissions): def test(): with serve(**make_app_args): for check, permitted in expected_permissions.items(): if check in globals(): checks = [check] elif check.endswith('auth'): checks = ['can_%s' % check] else: checks = ['can_%s_unauth' % check, 'can_%s_auth' % check] for check in checks: assert globals()[check]() == permitted return test test_nosmart_noauth = options_test( {}, {'reach': True, 'clone': False, 'push': False} ) test_smart_noauth = options_test( {'use_smarthttp': True}, {'reach': True, 'clone': True, 'push': False} ) test_smart_push = options_test( {'use_smarthttp': True, 'htdigest_file': open(HTDIGEST_FILE)}, {'reach': True, 'clone': True, 'push_auth': True, 'push_unauth': False} ) test_unauthenticated_push = options_test( {'use_smarthttp': True, 'unauthenticated_push': True}, {'reach': True, 'clone': True, 'push': True} ) test_nosmart_auth = options_test( {'require_browser_auth': True, 'htdigest_file': open(HTDIGEST_FILE)}, {'reach_auth': True, 'reach_unauth': False, 'clone': False, 'push': False} ) test_smart_auth = options_test( {'require_browser_auth': True, 'use_smarthttp': True, 'htdigest_file': open(HTDIGEST_FILE)}, {'reach_auth': True, 'reach_unauth': False, 'clone_auth': True, 'clone_unauth': False, 'push_unauth': False, 'push_auth': True} ) test_smart_auth_disable_push = options_test( {'require_browser_auth': True, 'use_smarthttp': True, 'disable_push': True, 'htdigest_file': open(HTDIGEST_FILE)}, {'reach_auth': True, 'reach_unauth': False, 'clone_auth': True, 'clone_unauth': False, 'push': False} ) test_ctags_disabled = options_test( {}, {'ctags_tags_and_branches': False, 'ctags_all': False} ) test_ctags_tags_and_branches = options_test( {'ctags_policy': 'tags-and-branches'}, {'ctags_tags_and_branches': True, 'ctags_all': False} ) test_ctags_all = options_test( {'ctags_policy': 'ALL'}, {'ctags_tags_and_branches': True, 'ctags_all': True} ) # Reach def can_reach_unauth(): return _check_http200(_GET_unauth, "test_repo") def can_reach_auth(): return _check_http200(_GET_auth, "test_repo") # Clone def can_clone_unauth(): return _can_clone(_GET_unauth, UNAUTH_TEST_REPO_URL) def can_clone_auth(): return _can_clone(_GET_auth, AUTH_TEST_REPO_URL) def _can_clone(http_get, url): tmp = tempfile.mkdtemp() try: return any([ b"git clone" in http_get(TEST_REPO_URL).content, _check_http200(http_get, TEST_REPO_URL + "info/refs?service=git-upload-pack"), subprocess.call(["git", "clone", url, tmp]) == 0, ]) finally: shutil.rmtree(tmp, ignore_errors=True) # Push def can_push_unauth(): return _can_push(_GET_unauth, UNAUTH_TEST_REPO_URL) def can_push_auth(): return _can_push(_GET_auth, AUTH_TEST_REPO_URL) def _can_push(http_get, url): return any([ _check_http200(http_get, TEST_REPO_URL + "info/refs?service=git-receive-pack"), _check_http200(http_get, TEST_REPO_URL + "git-receive-pack"), subprocess.call(["git", "push", url, "master"], cwd=TEST_REPO) == 0, ]) # Ctags def ctags_tags_and_branches(): return all( _ctags_enabled(ref, f) for ref in ["master", "tag1"] for f in ["test.c", "test.js"] ) def ctags_all(): all_refs = re.findall('href=".+/commit/([a-z0-9]{40})/">', requests.get(UNAUTH_TEST_REPO_URL).content) assert len(all_refs) == 3 return all( _ctags_enabled(ref, f) for ref in all_refs for f in ["test.c", "test.js"] ) def _ctags_enabled(ref, filename): response = requests.get(UNAUTH_TEST_REPO_URL + "blob/%s/%s" % (ref, filename)) href = '' % (TEST_REPO_URL, ref, filename) return href in response.content def _GET_unauth(url=""): return requests.get(UNAUTH_TEST_SERVER + url, auth=requests.auth.HTTPDigestAuth("invalid", "password")) def _GET_auth(url=""): return requests.get(AUTH_TEST_SERVER + url, auth=requests.auth.HTTPDigestAuth("testuser", "testpassword")) def _check_http200(http_get, url): try: return http_get(url).status_code == 200 except: return False klaus-0.7.1/tests/test_views.py000066400000000000000000000024341260644720700165610ustar00rootroot00000000000000from io import BytesIO import requests import tarfile import contextlib from .utils import * def test_download(): with serve(): response = requests.get(UNAUTH_TEST_REPO_URL + "tarball/master", stream=True) response_body = BytesIO(response.raw.read()) tarball = tarfile.TarFile.gzopen("test.tar.gz", fileobj=response_body) with contextlib.closing(tarball): assert tarball.extractfile('test.c').read() == b'int a;\n' def test_no_newline_at_end_of_file(): with serve(): response = requests.get(TEST_REPO_NO_NEWLINE_URL + "commit/HEAD/").content assert "No newline at end of file" in response assert "2" in response assert "2" in response def test_dont_render_binary(): with serve(): response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/binary").content assert "Binary data not shown" in response def test_render_image(): with serve(): response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/image.jpg").content assert '.*?', '', html) urls.update(AHREF_RE.findall(html)) else: if '--failfast' in sys.argv: print url, status exit(1) errors[status].add(url) def print_stats(): import pprint print len(seen) pprint.pprint(dict(errors)) print {url: sum(times)/len(times) for url, times in durations.iteritems()} atexit.register(print_stats) main()