pax_global_header00006660000000000000000000000064131272127520014514gustar00rootroot0000000000000052 comment=43a3cfc6b65a95ddf03ffc0bd3097c5cad1d7ade klaus-1.2.1/000077500000000000000000000000001312721275200126345ustar00rootroot00000000000000klaus-1.2.1/.gitignore000066400000000000000000000001211312721275200146160ustar00rootroot00000000000000*.pyc /bin/ env/ .idea *.swp tests/repos/build *.egg-info build/ dist/ .DS_Store klaus-1.2.1/.travis.yml000066400000000000000000000004731312721275200147510ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.4" - "3.5" addons: apt: packages: - exuberant-ctags install: - "pip install ." - "pip install -r test_requirements.txt" script: - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - ./runtests.sh klaus-1.2.1/CHANGELOG.rst000066400000000000000000000154361312721275200146660ustar00rootroot00000000000000Changelog ========= 1.2.1 (Jul 5, 2017) ------------------- - SECURITY ISSUE, PLEASE UPDATE: Fix #200: Missing HTML escaping in diff view - #189: Submodule info page instead of server error (Jelmer Vernooij) - #187, #191, #165: Bug fixes (Chris St. Pierre, Aleksey Rybalkin) 1.2.0 (Jun 13, 2017) -------------------- * #177: Fix relative links in READMEs (etc.) (Jelmer Vernooij) * #36: Allow for branch names with ``/``, e.g. ``feature/foobar`` (Martin Zimmermann, Chris St. Pierre) * #184: Drop support for Python 2.6 (Jelmer Vernooij) * Refactor diff generating code (Jelmer Vernooij) * Fix temporary files not being deleted (Jonas Haag) 1.1.0 (Feb 1, 2017) ------------------- * Display README on repository landing page (Jelmer Vernooij) * Make all options configurable using environment variables (Jimmy Petersson) * #122: Support `.git/cloneurl` and `gitweb.url` settings (Jelmer Vernooij) * Support ".mdwn" markdown file extension (Jelmer Vernooij) * #166: Set device viewport (Jonas Haag) * Fix autoreloader with Python (Jimmy Petersson) * #169: Fix htdigest with autoreloader (Jimmy Petersson) 1.0.1 (May 24, 2016) --------------------- * Full support for Python 3 (Louis Sautier, Jonas Haag) 0.9.1 (Apr 14, 2016) -------------------- * #155: Do not change SCRIPT_NAME if HTTP_X_SCRIPT_NAME isn't set (Louis Sautier) 0.8.0 (Feb 2, 2016) ------------------- * #140, #145: Deprecate ``klaus.utils.SubUri`` in favor of the new ``klaus.utils.ProxyFix``, which correctly handles ``SCRIPT_NAME``. For details on how to use the new ``ProxyFix``, see `Klaus behind a reverse proxy `_. (Jelmer Vernooij, Jonas Haag) * Add man page. (Jelmer Vernooij) * Add ``--version`` command line option (Jelmer Vernooij) * Improve error message when ctags is enabled but not installed (Jonas Haag) * Add a few missing entries to the default robots.txt (Jonas Haag) 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-1.2.1/LICENSE000066400000000000000000000021521312721275200136410ustar00rootroot00000000000000https://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-1.2.1/MANIFEST.in000066400000000000000000000001051312721275200143660ustar00rootroot00000000000000recursive-include klaus/static * recursive-include klaus/templates * klaus-1.2.1/README.rst000066400000000000000000000056731312721275200143360ustar00rootroot00000000000000|travis-badge| |gitter-badge| .. |travis-badge| image:: https://travis-ci.org/jonashaag/klaus.svg?branch=master :target: https://travis-ci.org/jonashaag/klaus .. |gitter-badge| image:: https://badges.gitter.im/Join%20Chat.svg :alt: Join the chat at https://gitter.im/jonashaag/klaus :target: https://gitter.im/jonashaag/klaus?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge klaus: a simple, easy-to-set-up Git web viewer that Just Works™. ================================================================ (If it doesn't Just Work for you, please file a bug.) * Super easy to set up -- no configuration required * Supports Python 2 and Python 3 * Syntax highlighting * Git Smart HTTP support * Code navigation using Exuberant ctags :Demo: http://klausdemo.lophus.org :Mailing list: http://groups.google.com/group/klaus-users :On PyPI: http://pypi.python.org/pypi/klaus/ :Wiki: https://github.com/jonashaag/klaus/wiki :License: ISC (BSD) Contributing ------------ Please do it! I'm equally happy with bug reports/feature ideas and code contributions. If you have any questions/issues, I'm happy to help! For starters, `here are a few ideas what to work on. `_ :-) |img1|_ |img2|_ |img3|_ .. |img1| image:: 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-1.2.1/RELEASE_PROCESS000066400000000000000000000001671312721275200150410ustar00rootroot00000000000000* CHANGELOG.rst * Change version number in - setup.py - klaus/__init__.py * python setup.py sdist upload * git tag klaus-1.2.1/bin/000077500000000000000000000000001312721275200134045ustar00rootroot00000000000000klaus-1.2.1/bin/klaus000077500000000000000000000100031312721275200144430ustar00rootroot00000000000000#!/usr/bin/env python # coding: utf-8 from __future__ import print_function import sys import os import argparse import webbrowser from dulwich.errors import NotGitRepository from dulwich.repo import Repo from klaus import make_app, KLAUS_VERSION from klaus.utils import force_unicode def git_repository(path): path = os.path.abspath(path) if not os.path.exists(path): raise argparse.ArgumentTypeError('%r: No such directory' % path) try: Repo(path) except NotGitRepository: raise argparse.ArgumentTypeError('%r: Not a Git repository' % path) return path def make_parser(): parser = argparse.ArgumentParser() parser.add_argument('--host', help="default: 127.0.0.1", default='127.0.0.1') parser.add_argument('--port', help="default: 8080", default=8080, type=int) parser.add_argument('--site-name', help="site name showed in header. default: your hostname") parser.add_argument('--version', help='print version number', action='store_true') parser.add_argument('-b', '--browser', help="open klaus in a browser on server start", default=False, action='store_true') parser.add_argument('-B', '--with-browser', help="specify which browser to use with --browser", metavar='BROWSER', default=None) parser.add_argument('--ctags', help="enable ctags for which revisions? default: none. " "WARNING: Don't use 'ALL' for public servers!", choices=['none', 'tags-and-branches', 'ALL'], default='none') parser.add_argument('repos', help='repositories to serve', metavar='DIR', nargs='*', type=git_repository) grp = parser.add_argument_group("Git Smart HTTP") grp.add_argument('--smarthttp', help="enable Git Smart HTTP serving", action='store_true') grp.add_argument('--htdigest', help="use credentials from FILE", metavar="FILE", type=argparse.FileType('r')) grp = parser.add_argument_group("Development flags", "DO NOT USE IN PRODUCTION!") grp.add_argument('--debug', help="Enable Werkzeug debugger and reloader", action='store_true') return parser def main(): args = make_parser().parse_args() if args.version: print(KLAUS_VERSION) return 0 if args.htdigest and not args.smarthttp: print("ERROR: --htdigest option has no effect without --smarthttp enabled", file=sys.stderr) return 1 if not args.repos: print("WARNING: No repositories supplied -- syntax is 'klaus dir1 dir2...'.", file=sys.stderr) if not args.site_name: args.site_name = '%s:%d' % (args.host, args.port) if args.ctags != 'none': from klaus.ctagsutils import check_have_exuberant_ctags if not check_have_exuberant_ctags(): print("ERROR: Exuberant ctags not installed (or 'ctags' binary isn't *Exuberant* ctags)", file=sys.stderr) return 1 try: import ctags except ImportError: raise ImportError("Please install 'python-ctags3' to enable ctags support.") app = make_app( args.repos, force_unicode(args.site_name or args.host), args.smarthttp, args.htdigest, ctags_policy=args.ctags, ) if args.browser: _open_browser(args) app.run(args.host, args.port, args.debug) def _open_browser(args): # Open a web browser onto the server URL. Technically we're jumping the # gun a little here since the server is not yet running, but there's no # clean way to run a function after the server has started without # losing the simplicity of the code. In the Real World (TM) it'll take # longer for the browser to start than it will for us to start # serving, so we'll be OK. if args.with_browser is None: opener = webbrowser.open else: opener = webbrowser.get(args.with_browser).open opener('http://%s:%s' % (args.host, args.port)) if __name__ == '__main__': exit(main()) klaus-1.2.1/klaus.1000066400000000000000000000026331312721275200140410ustar00rootroot00000000000000.TH KLAUS "1" "December 2015" "klaus 4e82832" "User Commands" .SH NAME klaus \- easy to set up Git web viewer .SH SYNOPSIS .B klaus [\fIOPTION\fR]... [\fIDIR\fR]... .SH DESCRIPTION Klaus is a simple and easy-to-set-up Git web viewer that Just Works\(tm. .PP Note that the klaus binary just starts a test instance. The klaus script uses wsgiref internally which doesn't scale at all - it's single-threaded and non-asynchronous. .PP It supports syntax highlighting and Git Smart HTTP. .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-\-host\fR HOST default: 127.0.0.1 .TP \fB\-\-port\fR PORT default: 8080 .TP \fB\-\-site\-name\fR SITE_NAME site name showed in header. default: your hostname .TP \fB\-\-version\fR print version number .TP \fB\-b\fR, \fB\-\-browser\fR open klaus in a browser on server start .TP \fB\-B\fR BROWSER, \fB\-\-with\-browser\fR BROWSER specify which browser to use with \fB\-\-browser\fR .TP \fB\-\-ctags\fR {none,tags\-and\-branches,ALL} enable ctags for which revisions? default: none. WARNING: Don't use 'ALL' for public servers! .SS "Git Smart HTTP:" .TP \fB\-\-smarthttp\fR enable Git Smart HTTP serving .TP \fB\-\-htdigest\fR FILE use credentials from FILE .SS "Development flags:" .IP DO NOT USE IN PRODUCTION! .TP \fB\-\-debug\fR Enable Werkzeug debugger and reloader .SH AUTHORS Copyright \(co 2011-2015 Jonas Haag and contributors (see Git logs). klaus-1.2.1/klaus/000077500000000000000000000000001312721275200137535ustar00rootroot00000000000000klaus-1.2.1/klaus/__init__.py000066400000000000000000000177221312721275200160750ustar00rootroot00000000000000import jinja2 import flask import httpauth import dulwich.web from klaus import views, utils from klaus.repo import FancyRepo KLAUS_VERSION = utils.guess_git_revision() or '1.2.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', '/'), ('robots_txt', '/robots.txt/'), ('blob', '//blob/'), ('blob', '//blob//'), ('blame', '//blame/'), ('blame', '//blame//'), ('raw', '//raw//'), ('raw', '//raw//'), ('submodule', '//submodule//'), ('submodule', '//submodule//'), ('commit', '//commit//'), ('patch', '//commit/.diff'), ('patch', '//commit/.patch'), ('index', '//'), ('index', '//'), ('history', '//tree//'), ('history', '//tree//'), ('download', '//tarball//'), ]: self.add_url_rule(rule, view_func=getattr(views, endpoint)) def should_use_ctags(self, git_repo, git_commit): if self.ctags_policy == 'none': return False elif self.ctags_policy == 'ALL': return True elif self.ctags_policy == 'tags-and-branches': return git_commit.id in git_repo.get_tag_and_branch_shas() else: raise ValueError("Unknown ctags policy %r" % self.ctags_policy) def make_app(repo_paths, site_name, use_smarthttp=False, htdigest_file=None, require_browser_auth=False, disable_push=False, unauthenticated_push=False, ctags_policy='none'): """ Returns a WSGI app with all the features (smarthttp, authentication) already patched in. :param repo_paths: List of paths of repositories to serve. :param site_name: Name of the Web site (e.g. "John Doe's Git Repositories") :param use_smarthttp: Enable Git Smart HTTP mode, which makes it possible to pull from the served repositories. If `htdigest_file` is set as well, also allow to push for authenticated users. :param require_browser_auth: Require HTTP authentication according to the credentials in `htdigest_file` for ALL access to the Web interface. Requires the `htdigest_file` option to be set. :param disable_push: Disable push support. This is required in case both `use_smarthttp` and `require_browser_auth` (and thus `htdigest_file`) are set, but push should not be supported. :param htdigest_file: A *file-like* object that contains the HTTP auth credentials. :param unauthenticated_push: Allow push'ing without authentication. DANGER ZONE! :param ctags_policy: The ctags policy to use, may be one of: - 'none': never use ctags - 'tags-and-branches': use ctags for revisions that are the HEAD of a tag or branc - 'ALL': use ctags for all revisions, may result in high server load! """ if unauthenticated_push: if not use_smarthttp: raise ValueError("'unauthenticated_push' set without 'use_smarthttp'") if disable_push: raise ValueError("'unauthenticated_push' set with 'disable_push'") if require_browser_auth: raise ValueError("Incompatible options 'unauthenticated_push' and 'require_browser_auth'") if htdigest_file and not (require_browser_auth or use_smarthttp): raise ValueError("'htdigest_file' set without 'use_smarthttp' or 'require_browser_auth'") app = Klaus( repo_paths, site_name, use_smarthttp, ctags_policy, ) app.wsgi_app = utils.ProxyFix(app.wsgi_app) if use_smarthttp: # `path -> Repo` mapping for Dulwich's web support dulwich_backend = dulwich.server.DictBackend( dict(('/'+name, repo) for name, repo in app.repos.items()) ) # Dulwich takes care of all Git related requests/URLs # and passes through everything else to klaus dulwich_wrapped_app = dulwich.web.make_wsgi_chain( backend=dulwich_backend, fallback_app=app.wsgi_app, ) dulwich_wrapped_app = utils.ProxyFix(dulwich_wrapped_app) # `receive-pack` is requested by the "client" on a push # (the "server" is asked to *receive* packs), i.e. we need to secure # it using authentication or deny access completely to make the repo # read-only. # # Git first sends requests to //info/refs?service=git-receive-pack. # If this request is responded to using HTTP 401 Unauthorized, the user # is prompted for username and password. If we keep responding 401, Git # interprets this as an authentication failure. (We can't respond 403 # because this results in horrible, unhelpful Git error messages.) # # Git will never call //git-receive-pack if authentication # failed for /info/refs, but since it's used to upload stuff to the server # we must secure it anyway for security reasons. PATTERN = r'^/[^/]+/(info/refs\?service=git-receive-pack|git-receive-pack)$' if unauthenticated_push: # DANGER ZONE: Don't require authentication for push'ing app.wsgi_app = dulwich_wrapped_app elif htdigest_file and not disable_push: # .htdigest file given. Use it to read the push-er credentials from. if require_browser_auth: # No need to secure push'ing if we already require HTTP auth # for all of the Web interface. app.wsgi_app = dulwich_wrapped_app else: # Web interface isn't already secured. Require authentication for push'ing. app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, wsgi_app=dulwich_wrapped_app, routes=[PATTERN], ) else: # No .htdigest file given. Disable push-ing. Semantically we should # use HTTP 403 here but since that results in freaky error messages # (see above) we keep asking for authentication (401) instead. # Git will print a nice error message after a few tries. app.wsgi_app = httpauth.AlwaysFailingAuthMiddleware( wsgi_app=dulwich_wrapped_app, routes=[PATTERN], ) if require_browser_auth: app.wsgi_app = httpauth.DigestFileHttpAuthMiddleware( htdigest_file, wsgi_app=app.wsgi_app ) return app klaus-1.2.1/klaus/contrib/000077500000000000000000000000001312721275200154135ustar00rootroot00000000000000klaus-1.2.1/klaus/contrib/__init__.py000066400000000000000000000000001312721275200175120ustar00rootroot00000000000000klaus-1.2.1/klaus/contrib/app_args.py000066400000000000000000000014041312721275200175600ustar00rootroot00000000000000import os from distutils.util import strtobool def get_args_from_env(): repos = os.environ.get('KLAUS_REPOS', []) if repos: repos = repos.split() args = ( repos, os.environ['KLAUS_SITE_NAME'] ) kwargs = dict( htdigest_file=os.environ.get('KLAUS_HTDIGEST_FILE'), use_smarthttp=strtobool(os.environ.get('KLAUS_USE_SMARTHTTP', '0')), require_browser_auth=strtobool( os.environ.get('KLAUS_REQUIRE_BROWSER_AUTH', '0')), disable_push=strtobool(os.environ.get('KLAUS_DISABLE_PUSH', '0')), unauthenticated_push=strtobool( os.environ.get('KLAUS_UNAUTHENTICATED_PUSH', '0')), ctags_policy=os.environ.get('KLAUS_CTAGS_POLICY', 'none') ) return args, kwargs klaus-1.2.1/klaus/contrib/wsgi.py000066400000000000000000000004751312721275200167440ustar00rootroot00000000000000from klaus import make_app from .app_args import get_args_from_env args, kwargs = get_args_from_env() if kwargs['htdigest_file']: with open(kwargs['htdigest_file']) as file: kwargs['htdigest_file'] = file application = make_app(*args, **kwargs) else: application = make_app(*args, **kwargs) klaus-1.2.1/klaus/contrib/wsgi_autoreload.py000066400000000000000000000042621312721275200211610ustar00rootroot00000000000000from __future__ import print_function import os import time import threading import warnings from io import open, StringIO from klaus import make_app from .app_args import get_args_from_env # 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) args, kwargs = get_args_from_env() repos_root = os.environ.get('KLAUS_REPOS_ROOT') or os.environ['KLAUS_REPOS'] args = (repos_root,) + args[1:] if kwargs['htdigest_file']: # Cache the contents of the htdigest file, the application will not read # the file like object until later when called. with open(kwargs['htdigest_file'], encoding='utf-8') as htdigest_file: kwargs['htdigest_file'] = StringIO(htdigest_file.read()) application = make_autoreloading_app(*args, **kwargs) klaus-1.2.1/klaus/ctagscache.py000066400000000000000000000164001312721275200164130ustar00rootroot00000000000000"""A cache for tagsfiles generated by the 'ctags' command line tool. We don't want to run the 'ctags' command line tool on each request as it may take a lot of time. The following steps are necessary in order to create a ctags tagsfile that be read by Pygments: 1. Clone the repository to a temporary location and check out the branch/commit the user is browsing, unless the branch is already checked out. (*) 2. Run 'ctags -R' on the temporary repository checkout. 3. Delete the temporary repository checkout. To avoid going through these steps on each request, we cache the tagsfile generated in step 2. The cache is on-disk and non-persistent, i.e. cleared whenever the Python interpreter running klaus is shut down. For large projects, the ctags tagsfiles may grow to sizes of multiple MiB, so we have to set an upper limit on the size of the cache. Since tagsfiles are represented as uncompressed ASCII files, we can increase the number of tagsfiles we can cache by using compression. Of course, 'python-ctags', which is used by Pygments to read the tagsfiles, can't deal with compressed tagsfiles, so we have to uncompress them before actually using them. To avoid decompressing tagsfiles on each request, we keep the tagsfiles that are most likely to be used (**) in uncompressed form. (*) We always create a clone in the current implementation; this could be optimized in the future. (**) "most likely": currently implemented as "most recently used" """ import os import shutil import tempfile import threading import gzip import atexit from dulwich.lru_cache import LRUSizeCache from klaus.ctagsutils import create_tagsfile, delete_tagsfile # Good compression while taking only 10% more time than level 1 COMPRESSION_LEVEL = 4 def compress_tagsfile(uncompressed_tagsfile_path): """Compress an uncompressed tagsfile. :return: path to the compressed version of the tagsfile """ _, compressed_tagsfile_path = tempfile.mkstemp() with open(uncompressed_tagsfile_path, 'rb') as uncompressed: with gzip.open(compressed_tagsfile_path, 'wb', COMPRESSION_LEVEL) as compressed: shutil.copyfileobj(uncompressed, compressed) return compressed_tagsfile_path def uncompress_tagsfile(compressed_tagsfile_path): """Uncompress an compressed tagsfile. :return: path to the uncompressed version of the tagsfile """ _, uncompressed_tagsfile_path = tempfile.mkstemp() with gzip.open(compressed_tagsfile_path, 'rb') as compressed: with open(uncompressed_tagsfile_path, 'wb') as uncompressed: shutil.copyfileobj(compressed, uncompressed) return uncompressed_tagsfile_path MiB = 1024 * 1024 class CTagsCache(object): """A ctags cache. Both uncompressed and compressed entries are kept in temporary files created by `tempfile.mkstemp` which are deleted from disk when the Python interpreter is shut down. :param uncompressed_max_bytes: Maximum size of the uncompressed cache sector :param compressed_max_bytes: Maximum size of the compressed cache sector The lifecycle of a cache entry is as follows. - When first created, a tagsfile is put into the uncompressed cache sector. - When free space is required for other uncompressed tagsfiles, it may be moved to the compressed cache sector. Gzip is used to compress the tagsfile. - When free space is required for other compressed tagsfiles, it may be evicted from the cache entirely. - When the tagsfile is requested and it's in the compressed cache sector, it is moved back to the uncompressed sector prior to using it. """ def __init__(self, uncompressed_max_bytes=30*MiB, compressed_max_bytes=20*MiB): self.uncompressed_max_bytes = uncompressed_max_bytes self.compressed_max_bytes = compressed_max_bytes # Note: We use dulwich's LRU cache to store the tagsfile paths here, # but we could easily replace it by any other (LRU) cache implementation. self._uncompressed_cache = LRUSizeCache(uncompressed_max_bytes, compute_size=os.path.getsize) self._compressed_cache = LRUSizeCache(compressed_max_bytes, compute_size=os.path.getsize) self._clearing = False self._lock = threading.Lock() atexit.register(self.clear) def __del__(self): self.clear() def clear(self): """Clear both the uncompressed and compressed caches.""" # Don't waste time moving tagsfiles from uncompressed to compressed cache, # but remove them directly instead: self._clearing = True self._uncompressed_cache.clear() self._compressed_cache.clear() self._clearing = False def get_tagsfile(self, git_repo_path, git_rev): """Get the ctags tagsfile for the given Git repository and revision. - If the tagsfile is still in cache, and in uncompressed form, return it without any further cost. - If the tagsfile is still in cache, but in compressed form, uncompress it, put it into uncompressed space, and return the uncompressed version. - If the tagsfile isn't in cache at all, create it, put it into uncompressed cache and return the newly created version. """ # Always require full SHAs assert len(git_rev) == 40 # Avoiding race conditions, The Sledgehammer Way with self._lock: if git_rev in self._uncompressed_cache: return self._uncompressed_cache[git_rev] if git_rev in self._compressed_cache: compressed_tagsfile_path = self._compressed_cache[git_rev] uncompressed_tagsfile_path = uncompress_tagsfile(compressed_tagsfile_path) self._compressed_cache._remove_node(self._compressed_cache._cache[git_rev]) else: # Not in cache. uncompressed_tagsfile_path = create_tagsfile(git_repo_path, git_rev) self._uncompressed_cache.add(git_rev, uncompressed_tagsfile_path, self._clear_uncompressed_entry) return uncompressed_tagsfile_path def _clear_uncompressed_entry(self, git_rev, uncompressed_tagsfile_path): """Called by LRUSizeCache whenever an entry is to be evicted from uncompressed cache. Most of the times this happens when space is needed in uncompressed cache, in which case we move the tagsfile to compressed cache. When clearing the cache, we don't bother moving entries to uncompressed space; we delete them directly instead. """ if not self._clearing: # If we're clearing the whole cache, don't waste time moving tagsfiles # from uncompressed to compressed cache, but remove them directly instead. self._compressed_cache.add(git_rev, compress_tagsfile(uncompressed_tagsfile_path), self._clear_compressed_entry) delete_tagsfile(uncompressed_tagsfile_path) def _clear_compressed_entry(self, git_rev, compressed_tagsfile_path): """Called by LRUSizeCache whenever an entry to be evicted from compressed cache. This happens when space is needed for new compressed tagsfiles. We delete the evictee from the cache entirely. """ delete_tagsfile(compressed_tagsfile_path) klaus-1.2.1/klaus/ctagsutils.py000066400000000000000000000024521312721275200165120ustar00rootroot00000000000000import os import subprocess import shutil import tempfile import subprocess def check_have_exuberant_ctags(): """Check that the 'ctags' binary is *Exuberant* ctags (not etags etc)""" try: return b"Exuberant" in subprocess.check_output(["ctags", "--version"], stderr=subprocess.PIPE) except subprocess.CalledProcessError: return False def create_tagsfile(git_repo_path, git_rev): """Create a ctags tagsfile for the given Git repository and revision. This creates a temporary clone of the repository, checks out the revision, runs 'ctags -R' and deletes the temporary clone. :return: path to the generated tagsfile """ assert check_have_exuberant_ctags(), "'ctags' binary is missing or not *Exuberant* ctags" _, target_tagsfile = tempfile.mkstemp() checkout_tmpdir = tempfile.mkdtemp() try: subprocess.check_call(["git", "clone", "-q", "--shared", git_repo_path, checkout_tmpdir]) subprocess.check_call(["git", "checkout", "-q", git_rev], cwd=checkout_tmpdir) subprocess.check_call(["ctags", "--fields=+l", "-Rno", target_tagsfile], cwd=checkout_tmpdir) finally: shutil.rmtree(checkout_tmpdir) return target_tagsfile def delete_tagsfile(tagsfile_path): """Delete a tagsfile.""" os.remove(tagsfile_path) klaus-1.2.1/klaus/diff.py000066400000000000000000000055051312721275200152420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ lodgeit.lib.diff ~~~~~~~~~~~~~~~~ Render a nice diff between two things. :copyright: 2007 by Armin Ronacher. :license: BSD """ from difflib import SequenceMatcher from klaus.utils import escape_html as e def highlight_line(old_line, new_line): """Highlight inline changes in both lines.""" old_line = old_line.rstrip(b'\n') new_line = new_line.rstrip(b'\n') start = 0 limit = min(len(old_line), len(new_line)) while start < limit and old_line[start] == new_line[start]: start += 1 end = -1 limit -= start while -end <= limit and old_line[end] == new_line[end]: end -= 1 end += 1 if start or end: def do(l, tag): last = end + len(l) return b''.join( [l[:start], b'<', tag, b'>', l[start:last], b'', l[last:]]) old_line = do(old_line, b'del') new_line = do(new_line, b'ins') return old_line, new_line def render_diff(a, b, n=3): """Parse the diff an return data for the template.""" actions = [] chunks = [] for group in SequenceMatcher(None, a, b).get_grouped_opcodes(n): old_line, old_end, new_line, new_end = group[0][1], group[-1][2], group[0][3], group[-1][4] lines = [] def add_line(old_lineno, new_lineno, action, line): actions.append(action) lines.append({ 'old_lineno': old_lineno, 'new_lineno': new_lineno, 'action': action, 'line': line, 'no_newline': not line.endswith(b'\n') }) chunks.append(lines) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for c, line in enumerate(a[i1:i2]): add_line(i1+c, j1+c, 'unmod', e(line)) elif tag == 'insert': for c, line in enumerate(b[j1:j2]): add_line(None, j1+c, 'add', e(line)) elif tag == 'delete': for c, line in enumerate(a[i1:i2]): add_line(i1+c, None, 'del', e(line)) elif tag == 'replace': # TODO: not sure if this is the best way to deal with replace # blocks, but it's consistent with the previous version. for c, line in enumerate(a[i1:i2-1]): add_line(i1+c, None, 'del', e(line)) old_line, new_line = highlight_line(e(a[i2-1]), e(b[j1])) add_line(i2-1, None, 'del', old_line) add_line(None, j1, 'add', new_line) for c, line in enumerate(b[j1+1:j2]): add_line(None, j1+c+1, 'add', e(line)) else: raise AssertionError('unknown tag %s' % tag) return actions.count('add'), actions.count('del'), chunks klaus-1.2.1/klaus/highlighting.py000066400000000000000000000106601312721275200167750ustar00rootroot00000000000000from six.moves import filter from pygments import highlight from pygments.lexers import get_lexer_by_name, get_lexer_for_filename, \ guess_lexer, ClassNotFound, TextLexer from pygments.formatters import HtmlFormatter from klaus import markup CTAGS_SUPPORTED_LANGUAGES = ( "Asm Awk Basic C C# C++ Cobol DosBatch Eiffel Erlang Fortran HTML Java " "JavaScript Lisp Lua Make Makefile MatLab OCaml PHP Pascal Perl Python " "REXX Ruby SML SQL Scheme Sh Tcl Tex VHDL Verilog Vim" # Not supported by Pygments: Asp Ant BETA Flex SLang Vera YACC ).split() PYGMENTS_CTAGS_LANGUAGE_MAP = dict((get_lexer_by_name(l).name, l) for l in CTAGS_SUPPORTED_LANGUAGES) class KlausDefaultFormatter(HtmlFormatter): def __init__(self, language, ctags, **kwargs): HtmlFormatter.__init__(self, linenos='table', lineanchors='L', linespans='L', anchorlinenos=True, **kwargs) self.language = language if ctags: # Use Pygments' ctags system but provide our own CTags instance self.tagsfile = True # some trueish object self._ctags = ctags def _format_lines(self, tokensource): for tag, line in HtmlFormatter._format_lines(self, tokensource): if tag == 1: # sourcecode line line = '%s' % line yield tag, line def _lookup_ctag(self, token): matches = list(self._get_all_ctags_matches(token)) best_matches = list(self.get_best_ctags_matches(matches)) if not best_matches: return None, None else: return (best_matches[0]['file'].decode("utf-8"), best_matches[0]['lineNumber']) def _get_all_ctags_matches(self, token): FIELDS = ('file', 'lineNumber', 'kind', b'language') from ctags import TagEntry entry = TagEntry() # target "buffer" for ctags if self._ctags.find(entry, token.encode("utf-8"), 0): yield dict((k, entry[k]) for k in FIELDS) while self._ctags.findNext(entry): yield dict((k, entry[k]) for k in FIELDS) def get_best_ctags_matches(self, matches): if self.language is None: return matches else: return filter(lambda match: match[b'language'] == self.language.encode("utf-8"), matches) class KlausPythonFormatter(KlausDefaultFormatter): def get_best_ctags_matches(self, matches): # The first ctags match may be an import, which ctags sees as a # definition of the tag -- even though it might very well have found # the "real" definition of the tag. Import matches aren't very helpful: # In the best case, we are brought to the line where the tag is imported # in the same file. But it may also bring us to some completely unrelated # import of the tag in some other file. We change the tag lookup mechanics # so that non-import matches are always preferred over import matches. return filter( lambda match: match['kind'] != b'i', super(KlausPythonFormatter, self).get_best_ctags_matches(matches) ) def highlight_or_render(code, filename, render_markup=True, ctags=None, ctags_baseurl=None): """Render code using Pygments, markup (markdown, rst, ...) using the corresponding renderer, if available. :param code: the program code to highlight, str :param filename: name of the source file the code is taken from, str :param render_markup: whether to render markup if possible, bool :param ctags: tagsfile obj used for source code hyperlinks, ``ctags.CTags`` :param ctags_baseurl: base url used for source code hyperlinks, str """ if render_markup and markup.can_render(filename): return markup.render(filename, code) try: lexer = get_lexer_for_filename(filename, code) except ClassNotFound: try: lexer = guess_lexer(code) except ClassNotFound: lexer = TextLexer() formatter_cls = { 'Python': KlausPythonFormatter, }.get(lexer.name, KlausDefaultFormatter) if ctags: ctags_urlscheme = ctags_baseurl + "%(path)s%(fname)s%(fext)s" else: ctags_urlscheme = None formatter = formatter_cls( language=PYGMENTS_CTAGS_LANGUAGE_MAP.get(lexer.name), ctags=ctags, tagurlformat=ctags_urlscheme, ) return highlight(code, lexer, formatter) klaus-1.2.1/klaus/markup.py000066400000000000000000000025331312721275200156270ustar00rootroot00000000000000import os LANGUAGES = [] def get_renderer(filename): _, ext = os.path.splitext(filename) for extensions, renderer in LANGUAGES: if ext in extensions: return renderer def can_render(filename): return get_renderer(filename) is not None def render(filename, content=None): if content is None: content = open(filename).read() return get_renderer(filename)(content) def _load_markdown(): try: import markdown except ImportError: return def render_markdown(content): return markdown.markdown(content, extensions=['toc', 'extra']) LANGUAGES.append((['.md', '.mkdn', '.mdwn', '.markdown'], render_markdown)) def _load_restructured_text(): try: from docutils.core import publish_parts from docutils.writers.html4css1 import Writer except ImportError: return def render_rest(content): # start by h2 and ignore invalid directives and so on # (most likely from Sphinx) settings = {'initial_header_level': 2, 'report_level': '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-1.2.1/klaus/repo.py000066400000000000000000000231531312721275200152760ustar00rootroot00000000000000import os import io import stat import subprocess from dulwich.objects import S_ISGITLINK from dulwich.object_store import tree_lookup_path from dulwich.objects import Blob from dulwich.errors import NotTreeError import dulwich, dulwich.patch from klaus.utils import force_unicode, parent_directory, encode_for_git, decode_from_git from klaus.diff import render_diff class FancyRepo(dulwich.repo.Repo): """A wrapper around Dulwich's Repo that adds some helper methods.""" # TODO: factor out stuff into dulwich @property def name(self): """Get repository name from path. 1. /x/y.git -> /x/y and /x/y/.git/ -> /x/y// 2. /x/y/ -> /x/y 3. /x/y -> y """ 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) for ref in refs: # Find the latest ref that has a commit_time; tags do not # have a commit time if hasattr(ref, "commit_time"): return ref.commit_time return None @property def cloneurl(self): """Retrieve the gitweb notion of the public clone URL of this repo.""" f = self.get_named_file('cloneurl') if f is not None: return f.read() c = self.get_config() try: return force_unicode(c.get(b'gitweb', b'url')) except KeyError: return None def get_description(self): """Like Dulwich's `get_description`, but returns None if the file contains Git's default text "Unnamed repository[...]". """ description = super(FancyRepo, self).get_description() if description: description = force_unicode(description) if not description.startswith("Unnamed repository;"): return force_unicode(description) def get_commit(self, rev): """Get commit object identified by `rev` (SHA or branch or tag name).""" for prefix in ['refs/heads/', 'refs/tags/', '']: key = prefix + rev try: obj = self[encode_for_git(key)] if isinstance(obj, dulwich.objects.Tag): obj = self[obj.object[1]] return obj except KeyError: pass raise KeyError(rev) def get_default_branch(self): """Tries to guess the default repo branch name.""" for candidate in ['master', 'trunk', 'default', 'gh-pages']: try: self.get_commit(candidate) return candidate except KeyError: pass try: return self.get_branch_names()[0] except IndexError: return None def get_ref_names_ordered_by_last_commit(self, prefix, exclude=None): """Return a list of ref names that begin with `prefix`, ordered by the time they have been committed to last. """ def get_commit_time(refname): obj = self[refs[refname]] if isinstance(obj, dulwich.objects.Tag): return obj.tag_time return obj.commit_time refs = self.refs.as_dict(encode_for_git(prefix)) if exclude: refs.pop(prefix + exclude, None) sorted_names = sorted(refs.keys(), key=get_commit_time, reverse=True) return [decode_from_git(ref) for ref in sorted_names] def get_branch_names(self, exclude=None): """Return a list of branch names of this repo, ordered by the time they have been committed to last. """ return self.get_ref_names_ordered_by_last_commit('refs/heads', exclude) def get_tag_names(self): """Return a list of tag names of this repo, ordered by creation time.""" return self.get_ref_names_ordered_by_last_commit('refs/tags') def get_tag_and_branch_shas(self): """Return a list of SHAs of all tags and branches.""" tag_shas = self.refs.as_dict(b'refs/tags/').values() branch_shas = self.refs.as_dict(b'refs/heads/').values() return set(tag_shas) | set(branch_shas) def history(self, commit, path=None, max_commits=None, skip=0): """Return a list of all commits that affected `path`, starting at branch or commit `commit`. `skip` can be used for pagination, `max_commits` to limit the number of commits returned. Similar to `git log [branch/commit] [--skip skip] [-n max_commits]`. """ # XXX The pure-Python/dulwich code is very slow compared to `git log` # at the time of this writing (mid-2012). # For instance, `git log .tx` in the Django root directory takes # about 0.15s on my machine whereas the history() method needs 5s. # Therefore we use `git log` here until dulwich gets faster. # For the pure-Python implementation, see the 'purepy-hist' branch. cmd = ['git', 'log', '--format=%H'] if skip: cmd.append('--skip=%d' % skip) if max_commits: cmd.append('--max-count=%d' % max_commits) cmd.append(commit.id) if path: cmd.extend(['--', path]) output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path)) sha1_sums = output.strip().split(b'\n') return [self[sha1] for sha1 in sha1_sums] def blame(self, commit, path): """Return a 'git blame' list for the file at `path`: For each line in the file, the list contains the commit that last changed that line. """ # XXX see comment in `.history()` cmd = ['git', 'blame', '-ls', '--root', commit.id, '--', path] output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path)) sha1_sums = [line[:40] for line in output.strip().split(b'\n')] return [None if self[sha1] is None else decode_from_git(self[sha1].id) for sha1 in sha1_sums] def get_blob_or_tree(self, commit, path): """Return the Git tree or blob object for `path` at `commit`.""" try: (mode, oid) = tree_lookup_path(self.__getitem__, commit.tree, encode_for_git(path)) except NotTreeError: # Some part of the path was a file where a folder was expected. # Example: path="/path/to/foo.txt" but "to" is a file in "/path". raise KeyError return self[oid] def listdir(self, commit, path): """Return a list of directories and files in given directory.""" submodules, 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 S_ISGITLINK(entry.mode): submodules.append( (name.lower(), name, entry.path, entry.sha)) elif stat.S_ISDIR(entry.mode): 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 {'submodules': submodules, 'dirs' : dirs, 'files' : files} def commit_diff(self, commit): """Return the list of changes introduced by `commit`.""" from klaus.utils import guess_is_binary if commit.parents: parent_tree = self[commit.parents[0]].tree else: parent_tree = None summary = {'nfiles': 0, 'nadditions': 0, 'ndeletions': 0} file_changes = [] # the changes in detail dulwich_changes = self.object_store.tree_changes(parent_tree, commit.tree) for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in dulwich_changes: summary['nfiles'] += 1 try: oldblob = self.object_store[oldsha] if oldsha else Blob.from_string(b'') newblob = self.object_store[newsha] if newsha else Blob.from_string(b'') except KeyError: # newsha/oldsha are probably related to submodules. # Dulwich will handle that. pass # Check for binary files -- can't show diffs for these if guess_is_binary(newblob) or \ guess_is_binary(oldblob): file_changes.append({ 'is_binary': True, 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': None }) continue additions, deletions, chunks = render_diff( oldblob.splitlines(), newblob.splitlines()) change = { 'is_binary': False, 'old_filename': oldpath or '/dev/null', 'new_filename': newpath or '/dev/null', 'chunks': chunks, 'additions': additions, 'deletions': deletions, } summary['nadditions'] += additions summary['ndeletions'] += deletions file_changes.append(change) return summary, file_changes def raw_commit_diff(self, commit): if commit.parents: parent_tree = self[commit.parents[0]].tree else: parent_tree = None bytesio = io.BytesIO() dulwich.patch.write_tree_diff(bytesio, self.object_store, parent_tree, commit.tree) return bytesio.getvalue() klaus-1.2.1/klaus/static/000077500000000000000000000000001312721275200152425ustar00rootroot00000000000000klaus-1.2.1/klaus/static/favicon.png000066400000000000000000000054741312721275200174070ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYsmmsH(tEXtSoftwarewww.inkscape.org< IDATxM$eO =vO/d &dj(YN9q(eA  WM%MWAvxzvSꞪ}KT̈́1fOMt"@/=1y%Yt:bADn3 n,{˓M!V98|,¡zq}Y8/yGx>%8ӥ |߿V\xԭ7V `5|sq^'}WqZX WqV}GM'%pFc̅50‡R(`nn񢫀yB%(ss/W$@%ARJ/T$5θJ(Ƙ jǚM8+ Fwvo4WI0`<߬$OvoEԩ$Gs{WJ{FQ/K`S`8`;$HNc.Z-sAg%y2*gGP<#Q/*"AxO2mc>y+l\%EbIhƷ;,t$;@jjѥ`wm ^2+r_&/#2ӿVj ]!+AfdVK(^QgD2 5j>Ziqq1UkDR @)[TuI[F$U P"aV]pGjX(rZ٨T$&@_ἙF6 FBb]+mR<`N,\$ҞWazpswh+۟CU# he|![*{/$K+#6$ׁZ\j7xƘ=+SKͅ6Zp]DSBJF0|Z10RFo-jB++{[$KDwy'3;xbbֆȔʱN&/ H[!WTbmiEv۪A䪴Due<<@=: m۶-@]\bQ}, ã@"n2;Xd*8P޸x>;Ƙ=ob727k׮!XS}Tˤ'd:o[ |7NG͉+\I+3-q&ɖi܃z4[/|X@%AbA4~c#"xNSo6{#"i6t&;;ξɅgi60WzhƩVӼ z ѳ@văh>A$P+;faAy>ĘPIP>1'TQ‡fUQÇUq‡1VqÇ1'X.>D'n,$|H0P`woŋ.Gy5%i`aNS/"E/bcM$,2<8΁gOM4Y{hY)K B qoq>qnPRlWQEoK41z? ?򒰂I[R(5z>&@vĉ;:VҢ"H j-I3|HqUpl p>|H;|HyYxX_o(H/v"|`_$P+l-ׂ‡6I8S'zY īuZVSlDC[d-mIV,< tt4499 5=("c1a>9T7MgG9l~FESm|ქSķn m?z)rrxJ-vv=&vF& mZ>@u> ?E  ChhC%(bP0M >PpK"ܐCrKP@9%(KP\)|(P >H(e J&S%%AÇ ŐC|%p!|(JR Jb v%p9|pT#@2 6Cp>l`Y CɍP}>ոt3Zv}{Td7@@^@0 cGFIENDB`klaus-1.2.1/klaus/static/klaus.css000066400000000000000000000222651312721275200171020ustar00rootroot00000000000000@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-1.2.1/klaus/static/klaus.js000066400000000000000000000035701312721275200167240ustar00rootroot00000000000000var forEach = function(collection, func) { for(var i = 0; i < collection.length; ++i) { func(collection[i]); } } /* General collapse/expand/toggle framework. Used for hiding diffs in commits */ var toggler = { expand: function(elem) { elem.className = elem.className.replace("collapsed", ""); }, collapse: function(elem) { if (!/collapsed/.test(elem.className)) { elem.className += " collapsed"; } }, collapseAll: function(selector) { forEach(document.querySelectorAll(selector), toggler.collapse); }, expandAll: function(selector) { forEach(document.querySelectorAll(selector), toggler.expand); } }; /* Line highlighting logic for diffs */ var highlight_linenos = function(opts) { var links = document.querySelectorAll(opts.linksSelector), currentHash = location.hash; forEach(links, function(a) { var lineno = a.getAttribute('href').substr(1), associatedLine = document.getElementById(lineno); var highlight = function() { a.className = 'highlight-line'; associatedLine.className = 'highlight-line'; currentHighlight = a; } var unhighlight = function() { if (a.getAttribute('href') != location.hash) { a.className = ''; associatedLine.className = ''; } } a.onmouseover = associatedLine.onmouseover = highlight; a.onmouseout = associatedLine.onmouseout = unhighlight; // Initial highlight if (a.getAttribute('href') == location.hash) { highlight(); } }); window.onpopstate = function() { if (currentHash) { forEach(document.querySelectorAll('a[href="' + currentHash + '"]'), function(e) { e.onmouseout() }) } if (location.hash) { forEach(document.querySelectorAll('a[href="' + location.hash + '"]'), function(e) { e.onmouseover() }); currentHash = location.hash; } }; } klaus-1.2.1/klaus/static/pygments.css000066400000000000000000000065361312721275200176340ustar00rootroot00000000000000/* This is the Pygments Trac theme */ .code .hll { background-color: #ffffcc } .code { background: #ffffff; } .code .c { color: #999988; font-style: italic } /* Comment */ .code .err { color: #a61717; background-color: #e3d2d2 } /* Error */ .code .k { font-weight: bold } /* Keyword */ .code .o { font-weight: bold } /* Operator */ .code .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .code .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ .code .c1 { color: #999988; font-style: italic } /* Comment.Single */ .code .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ .code .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .code .ge { font-style: italic } /* Generic.Emph */ .code .gr { color: #aa0000 } /* Generic.Error */ .code .gh { color: #999999 } /* Generic.Heading */ .code .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .code .go { color: #888888 } /* Generic.Output */ .code .gp { color: #555555 } /* Generic.Prompt */ .code .gs { font-weight: bold } /* Generic.Strong */ .code .gu { color: #aaaaaa } /* Generic.Subheading */ .code .gt { color: #aa0000 } /* Generic.Traceback */ .code .kc { font-weight: bold } /* Keyword.Constant */ .code .kd { font-weight: bold } /* Keyword.Declaration */ .code .kn { font-weight: bold } /* Keyword.Namespace */ .code .kp { font-weight: bold } /* Keyword.Pseudo */ .code .kr { font-weight: bold } /* Keyword.Reserved */ .code .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .code .m { color: #009999 } /* Literal.Number */ .code .s { color: #bb8844 } /* Literal.String */ .code .na { color: #008080 } /* Name.Attribute */ .code .nb { color: #999999 } /* Name.Builtin */ .code .nc { color: #445588; font-weight: bold } /* Name.Class */ .code .no { color: #008080 } /* Name.Constant */ .code .ni { color: #800080 } /* Name.Entity */ .code .ne { color: #990000; font-weight: bold } /* Name.Exception */ .code .nf { color: #990000; font-weight: bold } /* Name.Function */ .code .nn { color: #555555 } /* Name.Namespace */ .code .nt { color: #000080 } /* Name.Tag */ .code .nv { color: #008080 } /* Name.Variable */ .code .ow { font-weight: bold } /* Operator.Word */ .code .w { color: #bbbbbb } /* Text.Whitespace */ .code .mf { color: #009999 } /* Literal.Number.Float */ .code .mh { color: #009999 } /* Literal.Number.Hex */ .code .mi { color: #009999 } /* Literal.Number.Integer */ .code .mo { color: #009999 } /* Literal.Number.Oct */ .code .sb { color: #bb8844 } /* Literal.String.Backtick */ .code .sc { color: #bb8844 } /* Literal.String.Char */ .code .sd { color: #bb8844 } /* Literal.String.Doc */ .code .s2 { color: #bb8844 } /* Literal.String.Double */ .code .se { color: #bb8844 } /* Literal.String.Escape */ .code .sh { color: #bb8844 } /* Literal.String.Heredoc */ .code .si { color: #bb8844 } /* Literal.String.Interpol */ .code .sx { color: #bb8844 } /* Literal.String.Other */ .code .sr { color: #808000 } /* Literal.String.Regex */ .code .s1 { color: #bb8844 } /* Literal.String.Single */ .code .ss { color: #bb8844 } /* Literal.String.Symbol */ .code .bp { color: #999999 } /* Name.Builtin.Pseudo */ .code .vc { color: #008080 } /* Name.Variable.Class */ .code .vg { color: #008080 } /* Name.Variable.Global */ .code .vi { color: #008080 } /* Name.Variable.Instance */ .code .il { color: #009999 } /* Literal.Number.Integer.Long */ klaus-1.2.1/klaus/static/robots.txt000066400000000000000000000003111312721275200173060ustar00rootroot00000000000000User-agent: * Allow: /*/blob/master/ Allow: /*/tree/master/ Allow: /*/raw/master/ Disallow: /*/tree/ Disallow: /*/blob/ Disallow: /*/raw/ Disallow: /*/blame/ Disallow: /*/commit/ Disallow: /*/tarball/ klaus-1.2.1/klaus/templates/000077500000000000000000000000001312721275200157515ustar00rootroot00000000000000klaus-1.2.1/klaus/templates/base.html000066400000000000000000000023361312721275200175550ustar00rootroot00000000000000{% extends 'skeleton.html' %} {% block title %} {{ repo.name }} ({{ rev|shorten_sha1 }}) {% endblock %} {% block breadcrumbs %} {{ repo.name }} / {{ rev|shorten_sha1 }} {% if subpaths %} {% for name, subpath in subpaths %} {% if loop.last %} {{ name|force_unicode }} {% else %} {{ name|force_unicode }} / {% endif %} {% endfor %} {% endif %} {% endblock %} {% block extra_header %}
{{ rev|shorten_sha1 }}
{% if tags %}
    {% for tag in tags %}
  • {{ tag }}
  • {% endfor %}
{% endif %}
{% endblock %} klaus-1.2.1/klaus/templates/blame_blob.html000066400000000000000000000022741312721275200207220ustar00rootroot00000000000000{% extends 'base.html' %} {% block title %} {{ path }} - {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %}

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

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

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

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

Repositories (order by last update)

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

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

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

{% endblock %} klaus-1.2.1/klaus/templates/tree.inc.html000066400000000000000000000015141312721275200203470ustar00rootroot00000000000000

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

klaus-1.2.1/klaus/templates/view_blob.html000066400000000000000000000031461312721275200206130ustar00rootroot00000000000000{% extends 'base.html' %} {% block title %} {{ path }} - {{ super() }} {% endblock %} {% block content %} {% include 'tree.inc.html' %} {% set raw_url = url_for('raw', repo=repo.name, rev=rev, path=path) %} {% macro not_shown(reason) %}
({{ reason }} not shown — Download file)
{% endmacro %}

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

{% if is_binary %} {% if is_image %} {% else %} {{ not_shown("Binary data") }} {% endif %} {% elif too_large %} {{ not_shown("Large file") }} {% else %} {% autoescape false %} {% if is_markup and render_markup %}
{{ rendered_code }}
{% else %} {{ rendered_code }} {% endif %} {% endautoescape %} {% endif %}
{% endblock %} klaus-1.2.1/klaus/templates/view_commit.html000066400000000000000000000137351312721275200211720ustar00rootroot00000000000000{% extends 'base.html' %} {% block extra_header %}{% endblock %} {# hide branch selector #} {% block title %} Commit {{ rev }} - {{ repo.name }} {% endblock %} {% block content %} {% set summary, file_changes = repo.commit_diff(commit) %}
{{ commit.message|force_unicode }} {% if commit.author != commit.committer %} {{ commit.author|force_unicode|extract_author_name }} authored {{ commit.author_time|timesince }} {{ commit.committer|force_unicode|extract_author_name }} committed {{ commit.commit_time|timesince }} {% else %} {{ commit.committer|force_unicode|extract_author_name }} {{ commit.commit_time|timesince }} {% endif %}
{{ summary.nfiles }} changed file(s) with {{ summary.nadditions }} addition(s) and {{ summary.ndeletions }} deletion(s). Raw diff Collapse all Expand all
{% for file in file_changes %}
{% set fileno = loop.index0 %}
{% if not file.get('is_binary') %}
+{{ file.additions }}
-{{ file.deletions }}
{% endif %} {# TODO dulwich doesn't do rename recognition {% if file.old_filename != file.new_filename %} {{ file.old_filename }} → {% endif %}#} {% if file.new_filename == '/dev/null' %} {{ file.old_filename|force_unicode }} {% else %} {{ file.new_filename|force_unicode }} {% endif %} less more
{% if file.get('is_binary') %}
Binary diff not shown
{% else %} {% for chunk in file.chunks %} {%- for line in chunk -%} {#- left column: linenos -#} {%- if line.old_lineno -%} {%- 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-1.2.1/klaus/utils.py000066400000000000000000000162341312721275200154730ustar00rootroot00000000000000# encoding: utf-8 import os import re import time import datetime import mimetypes import locale import warnings import subprocess import six try: import chardet except ImportError: chardet = None from werkzeug.contrib.fixers import ProxyFix as WerkzeugProxyFix from humanize import naturaltime class ProxyFix(WerkzeugProxyFix): """This middleware can be applied to add HTTP (reverse) proxy support to a WSGI application (klaus), making it possible to: * Mount it under a sub-URL (http://example.com/git/...) * Use a different HTTP scheme (HTTP vs. HTTPS) * Make it appear under a different domain altogether It sets `REMOTE_ADDR`, `HTTP_HOST` and `wsgi.url_scheme` from `X-Forwarded-*` headers. It also sets `SCRIPT_NAME` from the `X-Script-Name` header. For instance if you have klaus mounted under /git/ and your site uses SSL (but your proxy doesn't), make the proxy pass :: X-Script-Name = '/git' X-Forwarded-Proto = 'https' ... If you have more than one proxy server in front of your app, set `num_proxies` accordingly. Do not use this middleware in non-proxy setups for security reasons. The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and `werkzeug.proxy_fix.orig_http_host`. :param app: the WSGI application :param num_proxies: the number of proxy servers in front of the app. """ def __call__(self, environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME') if script_name is not None: if script_name.endswith('/'): warnings.warn( "'X-Script-Name' header should not end in '/' (found: %r). " "Please fix your proxy's configuration." % script_name) script_name = script_name.rstrip('/') environ['SCRIPT_NAME'] = script_name return super(ProxyFix, self).__call__(environ, start_response) class SubUri(object): """WSGI middleware to tweak the WSGI environ so that it's possible to serve the wrapped app (klaus) under a sub-URL and/or to use a different HTTP scheme (http:// vs. https://) for proxy communication. This is done by making your proxy pass appropriate HTTP_X_SCRIPT_NAME and HTTP_X_SCHEME headers. For instance if you have klaus mounted under /git/ and your site uses SSL (but your proxy doesn't), make it pass :: X-Script-Name = '/git' X-Scheme = 'https' Snippet stolen from http://flask.pocoo.org/snippets/35/ """ def __init__(self, app): warnings.warn( "'klaus.utils.SubUri' is deprecated and will be removed. " "Please upgrade your code to use 'klaus.utils.ProxyFix' instead.", DeprecationWarning ) self.app = app def __call__(self, environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: environ['SCRIPT_NAME'] = script_name.rstrip('/') if script_name and environ['PATH_INFO'].startswith(script_name): # strip `script_name` from PATH_INFO environ['PATH_INFO'] = environ['PATH_INFO'][len(script_name):] if 'HTTP_X_SCHEME' in environ: environ['wsgi.url_scheme'] = environ['HTTP_X_SCHEME'] return self.app(environ, start_response) def timesince(when, now=time.time): """Return the difference between `when` and `now` in human readable form.""" return naturaltime(now() - when) def formattimestamp(timestamp): return datetime.datetime.fromtimestamp(timestamp).strftime('%b %d, %Y %H:%M:%S') def guess_is_binary(dulwich_blob): return any(b'\0' in chunk for chunk in dulwich_blob.chunked) def guess_is_image(filename): mime, _ = mimetypes.guess_type(filename) if mime is None: return False return mime.startswith('image/') def encode_for_git(s): # XXX This assumes everything to be UTF-8 encoded return s.encode('utf8') def decode_from_git(b): # XXX This assumes everything to be UTF-8 encoded return b.decode('utf8') def force_unicode(s): """Do all kinds of magic to turn `s` into unicode""" # It's already unicode, don't do anything: if isinstance(s, six.text_type): return s # 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 def guess_git_revision(): """Try to guess whether this instance of klaus is run directly from a klaus git checkout. If it is, guess and return the currently checked-out commit SHA. If it's not (installed using pip, setup.py or the like), return None. This is used to display the "powered by klaus $VERSION" footer on each page, $VERSION being either the SHA guessed by this function or the latest release number. """ git_dir = os.path.join(os.path.dirname(__file__), '..', '.git') try: return force_unicode(subprocess.check_output( ['git', 'log', '--format=%h', '-n', '1'], cwd=git_dir ).strip()) except OSError: # Either the git executable couldn't be found in the OS's PATH # or no ".git" directory exists, i.e. this is no "bleeding-edge" installation. return None def sanitize_branch_name(name, chars='./', repl='-'): for char in chars: name = name.replace(char, repl) return name def escape_html(s): return s.replace(b'&', b'&').replace(b'<', b'<') \ .replace(b'>', b'>').replace(b'"', b'"') klaus-1.2.1/klaus/views.py000066400000000000000000000365631312721275200154770ustar00rootroot00000000000000from io import BytesIO import os import sys from flask import request, render_template, current_app, url_for from flask.views import View from werkzeug.wrappers import Response from werkzeug.exceptions import NotFound import dulwich.objects import dulwich.archive import dulwich.config from dulwich.object_store import tree_lookup_path try: import ctags except ImportError: ctags = None else: from klaus import ctagscache CTAGS_CACHE = ctagscache.CTagsCache() from klaus import markup from klaus.highlighting import highlight_or_render from klaus.utils import parent_directory, subpaths, force_unicode, guess_is_binary, \ guess_is_image, replace_dupes, sanitize_branch_name, encode_for_git README_FILENAMES = [b'README', b'README.md', b'README.rst'] 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, base_href=None) def robots_txt(): """Serve the robots.txt file to manage the indexing of the site by search engines.""" return current_app.send_static_file('robots.txt') def _get_repo_and_rev(repo, rev=None, path=None): if path and rev: rev += "/" + path.rstrip("/") try: repo = current_app.repos[repo] except KeyError: raise NotFound("No such repository %r" % repo) if rev is None: rev = repo.get_default_branch() if rev is None: raise NotFound("Empty repository") i = len(rev) while i > 0: try: commit = repo.get_commit(rev[:i]) path = rev[i:].strip("/") rev = rev[:i] except (KeyError, IOError): i = rev.rfind("/", 0, i) else: break else: raise NotFound("No such commit %r" % rev) return repo, rev, path, commit def _get_submodule(repo, commit, path): """Retrieve submodule URL and path.""" submodule_blob = repo.get_blob_or_tree(commit, '.gitmodules') config = dulwich.config.ConfigFile.from_file( BytesIO(submodule_blob.as_raw_string())) key = (b'submodule', path) submodule_url = config.get(key, b'url') submodule_path = config.get(key, b'path') return (submodule_url, submodule_path) class BaseRepoView(View): """Base for all views with a repo context. The arguments `repo`, `rev`, `path` (see `dispatch_request`) define the repository, branch/commit and directory/file context, respectively -- that is, they specify what (and in what state) is being displayed in all the derived views. For example: The 'history' view is the `git log` equivalent, i.e. if `path` is "/foo/bar", only commits related to "/foo/bar" are displayed, and if `rev` is "master", the history of the "master" branch is displayed. """ def __init__(self, view_name): self.view_name = view_name self.context = {} def dispatch_request(self, repo, rev=None, path=''): """Dispatch repository, revision (if any) and path (if any). To retain compatibility with :func:`url_for`, view routing uses two arguments: rev and path, although a single path is sufficient (from Git's point of view, '/foo/bar/baz' may be a branch '/foo/bar' containing baz, or a branch '/foo' containing 'bar/baz', but never both [1]. Hence, rebuild rev and path to a single path argument, which is then later split into rev and path again, but revision now may contain slashes. [1] https://github.com/jonashaag/klaus/issues/36#issuecomment-23990266 """ self.make_template_context(repo, rev, path.strip('/')) return self.get_response() def get_response(self): return render_template(self.template_name, **self.context) def make_template_context(self, repo, rev, path): repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) try: blob_or_tree = repo.get_blob_or_tree(commit, path) except KeyError: raise NotFound("File not found") self.context = { 'view': self.view_name, 'repo': repo, 'rev': rev, 'commit': commit, 'branches': repo.get_branch_names(exclude=rev), 'tags': repo.get_tag_names(), 'path': path, 'blob_or_tree': blob_or_tree, 'subpaths': list(subpaths(path)) if path else None, 'base_href': None, } class CommitView(BaseRepoView): template_name = 'view_commit.html' class PatchView(BaseRepoView): def get_response(self): return Response( self.context['repo'].raw_commit_diff(self.context['commit']), mimetype='text/plain', ) class TreeViewMixin(object): """The logic required for displaying the current directory in the sidebar.""" def make_template_context(self, *args): super(TreeViewMixin, self).make_template_context(*args) self.context['root_tree'] = self.listdir() def listdir(self): """Return a list of directories and files in the current path of the selected commit.""" root_directory = self.get_root_directory() return self.context['repo'].listdir( self.context['commit'], root_directory ) def get_root_directory(self): root_directory = self.context['path'] if isinstance(self.context['blob_or_tree'], dulwich.objects.Blob): # 'path' is a file (not folder) name root_directory = parent_directory(root_directory) return root_directory class HistoryView(TreeViewMixin, BaseRepoView): """Show commits of a branch + path, just like `git log`. With pagination.""" template_name = 'history.html' def make_template_context(self, *args): super(HistoryView, self).make_template_context(*args) try: page = int(request.args.get('page')) except (TypeError, ValueError): page = 0 self.context['page'] = page history_length = 30 if page: skip = (self.context['page']-1) * 30 + 10 if page > 7: self.context['previous_pages'] = [0, 1, 2, None] + list(range(page))[-3:] else: self.context['previous_pages'] = range(page) else: skip = 0 history = self.context['repo'].history( self.context['commit'], self.context['path'], history_length + 1, skip ) if len(history) == history_length + 1: # At least one more commit for next page left more_commits = True # We don't want show the additional commit on this page history.pop() else: more_commits = False self.context.update({ 'history': history, 'more_commits': more_commits, }) class IndexView(TreeViewMixin, BaseRepoView): """Show commits of a branch, just like `git log`. Also, README, if available.""" template_name = 'index.html' def _get_readme(self): tree = self.context['repo'][self.context['commit'].tree] for name in README_FILENAMES: if name in tree: readme_data = self.context['repo'][tree[name][1]].data readme_filename = name return (readme_filename, readme_data) else: raise KeyError def make_template_context(self, *args): super(IndexView, self).make_template_context(*args) self.context['base_href'] = url_for( 'blob', repo=self.context['repo'].name, rev=self.context['rev'], path='' ) self.context['page'] = 0 history_length = 10 history = self.context['repo'].history( self.context['commit'], self.context['path'], history_length + 1, skip=0, ) if len(history) == history_length + 1: # At least one more commit for next page left more_commits = True # We don't want show the additional commit on this page history.pop() else: more_commits = False self.context.update({ 'history': history, 'more_commits': more_commits, }) try: (readme_filename, readme_data) = self._get_readme() except KeyError: self.context.update({ 'is_markup': None, 'rendered_code': None, }) else: self.context.update({ 'is_markup': markup.can_render(readme_filename), 'rendered_code': highlight_or_render( force_unicode(readme_data), force_unicode(readme_filename), ), }) class BaseBlobView(BaseRepoView): def make_template_context(self, *args): super(BaseBlobView, self).make_template_context(*args) if not isinstance(self.context['blob_or_tree'], dulwich.objects.Blob): raise NotFound("Not a blob") self.context['filename'] = os.path.basename(self.context['path']) class SubmoduleView(BaseRepoView): """Show an information page about a submodule.""" template_name = 'submodule.html' def make_template_context(self, repo, rev, path): repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) try: submodule_rev = tree_lookup_path( repo.__getitem__, commit.tree, encode_for_git(path))[1] except KeyError: raise NotFound("Parent path for submodule missing") try: (submodule_url, submodule_path) = _get_submodule( repo, commit, encode_for_git(path)) except KeyError: submodule_url = None submodule_path = None # TODO(jelmer): Rather than printing an information page, # redirect to the page in klaus for the repository at # submodule_path, revision submodule_rev. self.context = { 'view': self.view_name, 'repo': repo, 'rev': rev, 'commit': commit, 'branches': repo.get_branch_names(exclude=rev), 'tags': repo.get_tag_names(), 'path': path, 'subpaths': list(subpaths(path)) if path else None, 'submodule_url': force_unicode(submodule_url), 'submodule_path': force_unicode(submodule_path), 'submodule_rev': force_unicode(submodule_rev), 'base_href': None, } class BaseFileView(TreeViewMixin, BaseBlobView): """Base for FileView and BlameView.""" def render_code(self, render_markup): should_use_ctags = current_app.should_use_ctags(self.context['repo'], self.context['commit']) if should_use_ctags: if ctags is None: raise ImportError("Ctags enabled but python-ctags not installed") ctags_base_url = url_for( self.view_name, repo=self.context['repo'].name, rev=self.context['rev'], path='' ) ctags_tagsfile = CTAGS_CACHE.get_tagsfile( self.context['repo'].path, self.context['commit'].id ) ctags_args = { 'ctags': ctags.CTags(ctags_tagsfile.encode(sys.getfilesystemencoding())), 'ctags_baseurl': ctags_base_url, } else: ctags_args = {} return highlight_or_render( force_unicode(self.context['blob_or_tree'].data), self.context['filename'], render_markup, **ctags_args ) def make_template_context(self, *args): super(BaseFileView, self).make_template_context(*args) self.context.update({ 'can_render': True, 'is_binary': False, 'too_large': False, 'is_markup': False, }) binary = guess_is_binary(self.context['blob_or_tree']) too_large = sum(map(len, self.context['blob_or_tree'].chunked)) > 100*1024 if binary: self.context.update({ 'can_render': False, 'is_binary': True, 'is_image': guess_is_image(self.context['filename']), }) elif too_large: self.context.update({ 'can_render': False, 'too_large': True, }) class FileView(BaseFileView): """Shows a file rendered using ``pygmentize``.""" template_name = 'view_blob.html' def make_template_context(self, *args): super(FileView, self).make_template_context(*args) if self.context['can_render']: render_markup = 'markup' not in request.args self.context.update({ 'is_markup': markup.can_render(self.context['filename']), 'render_markup': render_markup, 'rendered_code': self.render_code(render_markup), }) class BlameView(BaseFileView): template_name = 'blame_blob.html' def make_template_context(self, *args): super(BlameView, self).make_template_context(*args) if self.context['can_render']: line_commits = self.context['repo'].blame(self.context['commit'], self.context['path']) replace_dupes(line_commits, None) self.context.update({ 'rendered_code': self.render_code(render_markup=False), 'line_commits': line_commits, }) class RawView(BaseBlobView): """Show a single file in raw for (as if it were a normal filesystem file served through a static file server). """ def get_response(self): # Explicitly set an empty mimetype. This should work well for most # browsers as they do file type recognition anyway. # The correct way would be to implement proper file type recognition here. return Response(self.context['blob_or_tree'].chunked, mimetype='') class DownloadView(BaseRepoView): """Download a repo as a tar.gz file.""" def get_response(self): tarname = "%s@%s.tar.gz" % (self.context['repo'].name, sanitize_branch_name(self.context['rev'])) headers = { 'Content-Disposition': "attachment; filename=%s" % tarname, 'Cache-Control': "no-store", # Disables browser caching } tar_stream = dulwich.archive.tar_stream( self.context['repo'], self.context['blob_or_tree'], self.context['commit'].commit_time, format="gz" ) return Response( tar_stream, mimetype="application/x-tgz", headers=headers ) history = HistoryView.as_view('history', 'history') index = IndexView.as_view('index', 'index') commit = CommitView.as_view('commit', 'commit') patch = PatchView.as_view('patch', 'patch') blame = BlameView.as_view('blame', 'blame') blob = FileView.as_view('blob', 'blob') raw = RawView.as_view('raw', 'raw') download = DownloadView.as_view('download', 'download') submodule = SubmoduleView.as_view('submodule', 'submodule') klaus-1.2.1/runtests.sh000077500000000000000000000004221312721275200150600ustar00rootroot00000000000000#!/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 PYTHONPATH=tests py.test $tests -v klaus-1.2.1/setup.py000066400000000000000000000026401312721275200143500ustar00rootroot00000000000000# encoding: utf-8 from setuptools import setup def install_data_files_hack(): # This is a clever hack to circumvent distutil's data_files # policy "install once, find never". Definitely a TODO! # -- https://groups.google.com/group/comp.lang.python/msg/2105ee4d9e8042cb from distutils.command.install import INSTALL_SCHEMES for scheme in INSTALL_SCHEMES.values(): scheme['data'] = scheme['purelib'] install_data_files_hack() requires = ['six', 'flask', 'pygments', 'dulwich>=0.13.0', 'httpauth', 'humanize'] setup( name='klaus', version='1.2.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.7", ], install_requires=requires, ) klaus-1.2.1/test_requirements.txt000066400000000000000000000001001312721275200171460ustar00rootroot00000000000000pytest requests python-ctags3>=1.2.1 mock; python_version < '3' klaus-1.2.1/tests/000077500000000000000000000000001312721275200137765ustar00rootroot00000000000000klaus-1.2.1/tests/__init__.py000066400000000000000000000000001312721275200160750ustar00rootroot00000000000000klaus-1.2.1/tests/credentials.htdigest000066400000000000000000000000571312721275200200320ustar00rootroot00000000000000testuser:test:41e45fc24eaddd7545c4819e49d1198b klaus-1.2.1/tests/klaus_cli.py000077700000000000000000000000001312721275200203362../bin/klausustar00rootroot00000000000000klaus-1.2.1/tests/repos/000077500000000000000000000000001312721275200151265ustar00rootroot00000000000000klaus-1.2.1/tests/repos/scripts/000077500000000000000000000000001312721275200166155ustar00rootroot00000000000000klaus-1.2.1/tests/repos/scripts/dont-render000077500000000000000000000004111312721275200207600ustar00rootroot00000000000000#!/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 --no-gpg-sign -am first klaus-1.2.1/tests/repos/scripts/no-newline-at-end-of-file000077500000000000000000000002611312721275200233020ustar00rootroot00000000000000#!/bin/bash -e git init echo 1 > test echo -n 2 >> test git add test git commit --no-gpg-sign -m old echo 1 > test echo 2 >> test git add test git commit --no-gpg-sign -m new klaus-1.2.1/tests/repos/scripts/test_repo000077500000000000000000000004441312721275200205510ustar00rootroot00000000000000#!/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 --no-gpg-sign -m "Add some code" git commit --no-gpg-sign --allow-empty -m "Empty commit 1" git tag tag1 git commit --no-gpg-sign --allow-empty -m "Empty commit 2" klaus-1.2.1/tests/test_blame.py000066400000000000000000000014021312721275200164640ustar00rootroot00000000000000import requests from .utils import * def test_blame(): with serve(): response = requests.get(UNAUTH_TEST_REPO_URL + "blob/HEAD/test.c") assert response.status_code == 200 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).text 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).text assert "Can't show blame" in response klaus-1.2.1/tests/test_contrib.py000066400000000000000000000070451312721275200170550ustar00rootroot00000000000000import os try: from importlib import reload # Python 3.4+ except ImportError: pass import pytest from klaus.contrib import app_args from .utils import * from .test_make_app import can_reach_unauth, can_push_auth def check_env(env, expected_args, expected_kwargs): os.environ.update(env) args, kwargs = app_args.get_args_from_env() assert args == expected_args assert kwargs == expected_kwargs def test_missing_in_env(monkeypatch): """Test that KeyError is raised when required env var is missing""" monkeypatch.setattr(os, 'environ', os.environ.copy()) with pytest.raises(KeyError): args, kwargs = app_args.get_args_from_env() def test_minimum_env(monkeypatch): """Test to provide only required env var""" monkeypatch.setattr(os, 'environ', os.environ.copy()) check_env( {'KLAUS_SITE_NAME': TEST_SITE_NAME}, ([], TEST_SITE_NAME), dict( htdigest_file=None, use_smarthttp=False, require_browser_auth=False, disable_push=False, unauthenticated_push=False, ctags_policy='none') ) def test_complete_env(monkeypatch): """Test to provide all supported env var""" monkeypatch.setattr(os, 'environ', os.environ.copy()) check_env( { 'KLAUS_REPOS': TEST_REPO, 'KLAUS_SITE_NAME': TEST_SITE_NAME, 'KLAUS_HTDIGEST_FILE': HTDIGEST_FILE, 'KLAUS_USE_SMARTHTTP': 'yes', 'KLAUS_REQUIRE_BROWSER_AUTH': '1', 'KLAUS_DISABLE_PUSH': 'false', 'KLAUS_UNAUTHENTICATED_PUSH': '0', 'KLAUS_CTAGS_POLICY': 'ALL' }, ([TEST_REPO], TEST_SITE_NAME), dict( htdigest_file=HTDIGEST_FILE, use_smarthttp=True, require_browser_auth=True, disable_push=False, unauthenticated_push=False, ctags_policy='ALL') ) def test_unsupported_boolean_env(monkeypatch): """Test that unsupported boolean env var raises ValueError""" monkeypatch.setattr(os, 'environ', os.environ.copy()) with pytest.raises(ValueError): check_env( { 'KLAUS_REPOS': TEST_REPO, 'KLAUS_SITE_NAME': TEST_SITE_NAME, 'KLAUS_HTDIGEST_FILE': HTDIGEST_FILE, 'KLAUS_USE_SMARTHTTP': 'unsupported', }, (), {} ) def test_wsgi(monkeypatch): """Test start of wsgi app""" monkeypatch.setattr(os, 'environ', os.environ.copy()) os.environ['KLAUS_REPOS'] = TEST_REPO os.environ['KLAUS_SITE_NAME'] = TEST_SITE_NAME from klaus.contrib import wsgi with serve_app(wsgi.application): assert can_reach_unauth() assert not can_push_auth() os.environ['KLAUS_HTDIGEST_FILE'] = HTDIGEST_FILE os.environ['KLAUS_USE_SMARTHTTP'] = 'yes' reload(wsgi) with serve_app(wsgi.application): assert can_reach_unauth() assert can_push_auth() def test_wsgi_autoreload(monkeypatch): """Test start of wsgi autoreload app""" monkeypatch.setattr(os, 'environ', os.environ.copy()) os.environ['KLAUS_REPOS_ROOT'] = TEST_REPO_ROOT os.environ['KLAUS_SITE_NAME'] = TEST_SITE_NAME from klaus.contrib import wsgi_autoreload with serve_app(wsgi_autoreload.application): assert can_reach_unauth() assert not can_push_auth() os.environ['KLAUS_HTDIGEST_FILE'] = HTDIGEST_FILE os.environ['KLAUS_USE_SMARTHTTP'] = 'yes' reload(wsgi_autoreload) with serve_app(wsgi_autoreload.application): assert can_push_auth() klaus-1.2.1/tests/test_make_app.py000066400000000000000000000123151312721275200171660ustar00rootroot00000000000000import os import re import sys 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([ "git clone" in http_get(TEST_REPO_URL).text, _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).text) 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)) assert response.status_code == 200, response.text href = '' % (TEST_REPO_URL, ref, filename) return href in response.text 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-1.2.1/tests/test_manpage.py000066400000000000000000000017411312721275200170220ustar00rootroot00000000000000import sys import re import subprocess import klaus_cli try: from unittest import mock except ImportError: import mock from klaus.utils import force_unicode def test_covers_all_cli_options(): manpage = force_unicode(subprocess.check_output(["man", "./klaus.1"])) def assert_in_manpage(s): clean = lambda x: re.sub('(.\\x08)|\s', '', x) assert clean(s) in clean(manpage), "%r not found in manpage" % s mock_parser = mock.Mock() with mock.patch('argparse.ArgumentParser') as mock_cls: mock_cls.return_value = mock_parser klaus_cli.make_parser() for args, kwargs in mock_parser.add_argument.call_args_list: if kwargs.get('metavar') == 'DIR': continue for string in args: assert_in_manpage(string) if 'help' in kwargs: assert_in_manpage(kwargs['help']) if 'choices' in kwargs: for choice in kwargs['choices']: assert_in_manpage(choice) klaus-1.2.1/tests/test_views.py000066400000000000000000000024561312721275200165530ustar00rootroot00000000000000from 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/").text 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").text 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").text 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()