summaryrefslogtreecommitdiff
path: root/sandbox/axk/viewcvs/viewcvs.py
diff options
context:
space:
mode:
authoraxk <axk@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2005-04-22 11:05:28 +0000
committeraxk <axk@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2005-04-22 11:05:28 +0000
commit76ad37b421e925fe5320f23ab130ef9fed55bb7b (patch)
treec5ec322a8f70bd09cef47d4e1947981ef84936a7 /sandbox/axk/viewcvs/viewcvs.py
parent852ee1fa8b3e90d26f136064b264dc732f224308 (diff)
downloaddocutils-76ad37b421e925fe5320f23ab130ef9fed55bb7b.tar.gz
renaming my sandbox to my changed berlios username
git-svn-id: http://svn.code.sf.net/p/docutils/code/trunk@3242 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
Diffstat (limited to 'sandbox/axk/viewcvs/viewcvs.py')
-rw-r--r--sandbox/axk/viewcvs/viewcvs.py2833
1 files changed, 2833 insertions, 0 deletions
diff --git a/sandbox/axk/viewcvs/viewcvs.py b/sandbox/axk/viewcvs/viewcvs.py
new file mode 100644
index 000000000..23e1cbd84
--- /dev/null
+++ b/sandbox/axk/viewcvs/viewcvs.py
@@ -0,0 +1,2833 @@
+# -*-python-*-
+#
+# Copyright (C) 1999-2002 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewCVS
+# distribution or at http://viewcvs.sourceforge.net/license-1.html.
+#
+# Contact information:
+# Greg Stein, PO Box 760, Palo Alto, CA, 94302
+# gstein@lyra.org, http://viewcvs.sourceforge.net/
+#
+# -----------------------------------------------------------------------
+#
+# viewcvs: View CVS repositories via a web browser
+#
+# -----------------------------------------------------------------------
+#
+# This software is based on "cvsweb" by Henner Zeller (which is, in turn,
+# derived from software by Bill Fenner, with additional modifications by
+# Henrik Nordstrom and Ken Coar). The cvsweb distribution can be found
+# on Zeller's site:
+# http://stud.fh-heilbronn.de/~zeller/cgi/cvsweb.cgi/
+#
+# -----------------------------------------------------------------------
+#
+
+__version__ = '1.0-dev'
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+CONF_PATHNAME = None
+
+#########################################################################
+
+# this comes from our library; measure the startup time
+import debug
+debug.t_start('startup')
+debug.t_start('imports')
+
+# standard modules that we know are in the path or builtin
+import sys
+import os
+import sapi
+import cgi
+import string
+import urllib
+import mimetypes
+import time
+import re
+import stat
+import struct
+import types
+
+# these modules come from our library (the stub has set up the path)
+import compat
+import config
+import popen
+import ezt
+import accept
+import vclib
+from vclib import bincvs
+
+debug.t_end('imports')
+
+#########################################################################
+
+checkout_magic_path = '*checkout*'
+# According to RFC 1738 the '~' character is unsafe in URLs.
+# But for compatibility with URLs bookmarked with older releases of ViewCVS:
+oldstyle_checkout_magic_path = '~checkout~'
+docroot_magic_path = '*docroot*'
+viewcvs_mime_type = 'text/vnd.viewcvs-markup'
+alt_mime_type = 'text/x-cvsweb-markup'
+
+# put here the variables we need in order to hold our state - they will be
+# added (with their current value) to any link/query string you construct
+_sticky_vars = (
+ 'root',
+ 'hideattic',
+ 'sortby',
+ 'sortdir',
+ 'logsort',
+ 'diff_format',
+ 'only_with_tag',
+ 'search',
+ 'dir_pagestart',
+ 'log_pagestart',
+ )
+
+# for reading/writing between a couple descriptors
+CHUNK_SIZE = 8192
+
+# for rcsdiff processing of header
+_RCSDIFF_IS_BINARY = 'binary'
+_RCSDIFF_ERROR = 'error'
+
+# global configuration:
+cfg = None # see below
+
+# special characters that don't need to be URL encoded
+_URL_SAFE_CHARS = "/*~"
+
+if CONF_PATHNAME:
+ # installed
+ g_install_dir = os.path.dirname(CONF_PATHNAME)
+else:
+ # development directories
+ g_install_dir = os.path.join(os.pardir, os.pardir) # typically, "../.."
+
+
+class Request:
+ def __init__(self, server):
+ self.server = server
+ self.script_name = server.getenv('SCRIPT_NAME', '')
+ self.browser = server.getenv('HTTP_USER_AGENT', 'unknown')
+
+ # in lynx, it it very annoying to have two links per file, so
+ # disable the link at the icon in this case:
+ self.no_file_links = string.find(self.browser, 'Lynx') != -1
+
+ # newer browsers accept gzip content encoding and state this in a
+ # header (netscape did always but didn't state it) It has been
+ # reported that these braindamaged MS-Internet Explorers claim
+ # that they accept gzip .. but don't in fact and display garbage
+ # then :-/
+ self.may_compress = (
+ ( string.find(server.getenv('HTTP_ACCEPT_ENCODING', ''), 'gzip') != -1
+ or string.find(self.browser, 'Mozilla/3') != -1)
+ and string.find(self.browser, 'MSIE') == -1
+ )
+
+ # process the Accept-Language: header
+ hal = server.getenv('HTTP_ACCEPT_LANGUAGE','')
+ self.lang_selector = accept.language(hal)
+ self.language = self.lang_selector.select_from(cfg.general.languages)
+
+ # load the key/value files, given the selected language
+ self.kv = cfg.load_kv_files(self.language)
+
+ def run_viewcvs(self):
+
+ # global needed because "import vclib.svn" causes the
+ # interpreter to make vclib a local variable
+ global vclib
+
+ # This function first parses the query string and sets the following
+ # variables. Then it executes the request.
+ self.view_func = None # function to call to process the request
+ self.repos = None # object representing current repository
+ self.rootname = None # name of current root (as used in viewcvs.conf)
+ self.roottype = None # current root type ('svn' or 'cvs')
+ self.rootpath = None # physical path to current root
+ self.pathtype = None # type of path, either vclib.FILE or vclib.DIR
+ self.where = None # path to file or directory in current root
+ self.query_dict = None # validated and cleaned up query options
+ self.path_parts = None # for convenience, equals where.split('/')
+
+ # Process PATH_INFO component of query string
+ path_info = self.server.getenv('PATH_INFO', '')
+
+ # clean it up. this removes duplicate '/' characters and any that may
+ # exist at the front or end of the path.
+ path_parts = filter(None, string.split(path_info, '/'))
+
+ if path_parts:
+ # handle magic path prefixes
+ if path_parts[0] == docroot_magic_path:
+ # if this is just a simple hunk of doc, then serve it up
+ self.where = string.join(path_parts[1:], '/')
+ return view_doc(self)
+ elif path_parts[0] in (checkout_magic_path, oldstyle_checkout_magic_path):
+ path_parts.pop(0)
+ self.view_func = view_checkout
+
+ # see if we are treating the first path component (after any
+ # magic) as the repository root. if there are parts, and the
+ # first component is a named root, use it as such. else, we'll be
+ # falling back to the default root a little later.
+ if cfg.options.root_as_url_component and path_parts \
+ and list_roots(cfg).has_key(path_parts[0]):
+ self.rootname = path_parts.pop(0)
+
+ # if this is a forbidden path, stop now
+ if path_parts and cfg.is_forbidden(path_parts[0]):
+ raise debug.ViewCVSException('Access to "%s" is forbidden.'
+ % path_parts[0], '403 Forbidden')
+
+ self.where = string.join(path_parts, '/')
+ self.path_parts = path_parts
+
+ # Done with PATH_INFO, now parse the query params
+ self.query_dict = {}
+
+ for name, values in self.server.params().items():
+ # patch up old queries that use 'cvsroot' to look like they used 'root'
+ if name == 'cvsroot':
+ name = 'root'
+
+ # validate the parameter
+ _validate_param(name, values[0])
+
+ # if we're here, then the parameter is okay
+ self.query_dict[name] = values[0]
+
+ # Special handling for root parameter
+ root_param = self.query_dict.get('root', None)
+ if root_param:
+ self.rootname = root_param
+
+ # in root_as_url_component mode, if we see a root in the query
+ # data, we'll redirect to the new url schema. it may fail, but
+ # at least we tried.
+ if cfg.options.root_as_url_component:
+ del self.query_dict['root']
+ self.server.redirect(self.get_url())
+
+ elif self.rootname is None:
+ self.rootname = cfg.general.default_root
+
+ # Create the repository object
+ if cfg.general.cvs_roots.has_key(self.rootname):
+ self.rootpath = cfg.general.cvs_roots[self.rootname]
+ try:
+ self.repos = bincvs.BinCVSRepository(self.rootname, self.rootpath,
+ cfg.general)
+ self.roottype = 'cvs'
+ except vclib.ReposNotFound:
+ raise debug.ViewCVSException(
+ '%s not found!\nThe wrong path for this repository was '
+ 'configured, or the server on which the CVS tree lives may be '
+ 'down. Please try again in a few minutes.'
+ % self.server.escape(self.rootname))
+ # required so that spawned rcs programs correctly expand $CVSHeader$
+ os.environ['CVSROOT'] = self.rootpath
+ elif cfg.general.svn_roots.has_key(self.rootname):
+ self.rootpath = cfg.general.svn_roots[self.rootname]
+ try:
+ if re.match(_re_rewrite_url, self.rootpath):
+ # If the rootpath is a URL, we'll use the svn_ra module, but
+ # lie about its name.
+ import vclib.svn_ra
+ vclib.svn = vclib.svn_ra
+ else:
+ import vclib.svn
+ rev = None
+ if self.query_dict.has_key('rev') \
+ and self.query_dict['rev'] != 'HEAD':
+ rev = int(self.query_dict['rev'])
+ self.repos = vclib.svn.SubversionRepository(self.rootname,
+ self.rootpath, rev)
+ self.repos.cross_copies = cfg.options.cross_copies
+ self.roottype = 'svn'
+ except vclib.ReposNotFound:
+ raise debug.ViewCVSException(
+ '%s not found!\nThe wrong path for this repository was '
+ 'configured, or the server on which the Subversion tree lives may'
+ 'be down. Please try again in a few minutes.'
+ % self.server.escape(self.rootname))
+ except vclib.InvalidRevision, ex:
+ raise debug.ViewCVSException(str(ex))
+ else:
+ raise debug.ViewCVSException(
+ 'The root "%s" is unknown. If you believe the value is '
+ 'correct, then please double-check your configuration.'
+ % self.server.escape(self.rootname), "404 Repository not found")
+
+ # Make sure path exists
+ self.pathtype = _repos_pathtype(self.repos, self.path_parts)
+
+ if self.pathtype is None:
+ # path doesn't exist, try stripping known fake suffixes
+ result = _strip_suffix('.diff', self.where, self.path_parts, \
+ vclib.FILE, self.repos, view_diff) or \
+ _strip_suffix('.tar.gz', self.where, self.path_parts, \
+ vclib.DIR, self.repos, download_tarball) or \
+ _strip_suffix('root.tar.gz', self.where, self.path_parts, \
+ vclib.DIR, self.repos, download_tarball)
+ if result:
+ self.where, self.path_parts, self.pathtype, self.view_func = result
+ else:
+ raise debug.ViewcvsException('%s: unknown location'
+ % self.where, '404 Not Found')
+
+ # Try to figure out what to do based on view parameter
+ self.view_func = _views.get(self.query_dict.get('view', None),
+ self.view_func)
+
+ if self.view_func is None:
+ # view parameter is not set, try looking at pathtype and the
+ # other parameters
+ if self.pathtype == vclib.DIR:
+ self.view_func = view_directory
+ elif self.pathtype == vclib.FILE:
+ if self.query_dict.has_key('rev'):
+ if self.query_dict.get('content-type', None) in (viewcvs_mime_type,
+ alt_mime_type):
+ self.view_func = view_markup
+ else:
+ self.view_func = view_checkout
+ elif self.query_dict.has_key('annotate'):
+ self.view_func = view_annotate
+ elif self.query_dict.has_key('r1') and self.query_dict.has_key('r2'):
+ self.view_func = view_diff
+ elif self.query_dict.has_key('tarball'):
+ self.view_func = download_tarball
+ elif self.query_dict.has_key('graph'):
+ if not self.query_dict.has_key('makeimage'):
+ self.view_func = view_cvsgraph
+ else:
+ self.view_func = view_cvsgraph_image
+ else:
+ self.view_func = view_log
+
+ # Finally done parsing query string, set some extra variables
+ # and call view_func
+ self.full_name = self.rootpath + (self.where and '/' + self.where)
+ if self.pathtype == vclib.FILE:
+ self.setup_mime_type_info()
+
+ # startup is done now.
+ debug.t_end('startup')
+
+ self.view_func(self)
+
+ def get_url(self, **args):
+ """Constructs a link to another ViewCVS page just like the get_link
+ function except that it returns a single URL instead of a URL
+ split into components"""
+
+ url, params = apply(self.get_link, (), args)
+ qs = compat.urlencode(params)
+ if qs:
+ return urllib.quote(url, _URL_SAFE_CHARS) + '?' + qs
+ else:
+ return urllib.quote(url, _URL_SAFE_CHARS)
+
+ def get_link(self, view_func = None, rootname = None, where = None,
+ params = None, pathtype = None):
+ """Constructs a link pointing to another ViewCVS page. All arguments
+ correspond to members of the Request object. If they are set to
+ None they take values from the current page. Return value is a base
+ URL and a dictionary of parameters"""
+
+ if view_func is None:
+ view_func = self.view_func
+
+ if rootname is None:
+ rootname = self.rootname
+
+ if params is None:
+ params = self.query_dict.copy()
+
+ # must specify both where and pathtype or neither
+ assert (where is None) == (pathtype is None)
+
+ if where is None:
+ where = self.where
+ pathtype = self.pathtype
+
+ last_link = view_func is view_checkout or view_func is download_tarball
+
+ # The logic used to construct the URL is an inverse of the
+ # logic used to interpret URLs in Request.run_viewcvs
+
+ url = self.script_name
+
+ # add checkout magic if possible
+ if view_func is view_checkout and cfg.options.checkout_magic:
+ url = url + '/' + checkout_magic_path
+ view_func = None
+
+ # add root name
+ if cfg.options.root_as_url_component:
+ url = url + '/' + rootname
+ elif not (params.has_key('root') and params['root'] is None):
+ if rootname != cfg.general.default_root:
+ params['root'] = rootname
+ else:
+ params['root'] = None
+
+ # if we are asking for the revision info view, we don't need any
+ # path information
+ if view_func == view_revision:
+ where = None
+ pathtype = vclib.DIR
+
+ # add path
+ if where:
+ url = url + '/' + where
+
+ # add suffix for tarball
+ if view_func is download_tarball:
+ if not where: url = url + '/root'
+ url = url + '.tar.gz'
+
+ # add trailing slash for a directory
+ elif pathtype == vclib.DIR:
+ url = url + '/'
+
+ # no need to explicitly specify log view for a file
+ if view_func is view_log and pathtype == vclib.FILE:
+ view_func = None
+
+ # no need to explicitly specify directory view for a directory
+ if view_func is view_directory and pathtype == vclib.DIR:
+ view_func = None
+
+ # no need to explicitly specify annotate view when
+ # there's an annotate parameter
+ if view_func is view_annotate and params.has_key('annotate'):
+ view_func = None
+
+ # no need to explicitly specify diff view when
+ # there's r1 and r2 parameters
+ if view_func is view_diff and params.has_key('r1') \
+ and params.has_key('r2'):
+ view_func = None
+
+ # no need to explicitly specify checkout view when
+ # there's a rev parameter
+ if view_func is view_checkout and params.has_key('rev'):
+ view_func = None
+
+ view_code = _view_codes.get(view_func)
+ if view_code and not (params.has_key('view') and params['view'] is None):
+ params['view'] = view_code
+
+ return url, self.get_options(params, not last_link)
+
+ def get_options(self, params = {}, sticky_vars=1):
+ """Combine params with current sticky values"""
+ ret = { }
+ if sticky_vars:
+ for name in _sticky_vars:
+ value = self.query_dict.get(name)
+ if value is not None and not params.has_key(name):
+ ret[name] = self.query_dict[name]
+ for name, val in params.items():
+ if val is not None:
+ ret[name] = val
+ return ret
+
+ def setup_mime_type_info(self):
+ if cfg.general.mime_types_file:
+ mimetypes.init([cfg.general.mime_types_file])
+ self.mime_type, self.encoding = mimetypes.guess_type(self.where)
+ if not self.mime_type:
+ self.mime_type = 'text/plain'
+ self.default_viewable = cfg.options.allow_markup and \
+ (is_viewable_image(self.mime_type)
+ or is_text(self.mime_type))
+
+def _validate_param(name, value):
+ """Validate whether the given value is acceptable for the param name.
+
+ If the value is not allowed, then an error response is generated, and
+ this function throws an exception. Otherwise, it simply returns None.
+ """
+
+ try:
+ validator = _legal_params[name]
+ except KeyError:
+ raise debug.ViewcvsException(
+ 'An illegal parameter name ("%s") was passed.' % cgi.escape(name),
+ '400 Bad Request')
+
+ if validator is None:
+ return
+
+ # is the validator a regex?
+ if hasattr(validator, 'match'):
+ if not validator.match(value):
+ raise debug.ViewcvsException(
+ 'An illegal value ("%s") was passed as a parameter.' %
+ cgi.escape(value), '400 Bad Request')
+ return
+
+ # the validator must be a function
+ validator(value)
+
+def _validate_regex(value):
+ # hmm. there isn't anything that we can do here.
+
+ ### we need to watch the flow of these parameters through the system
+ ### to ensure they don't hit the page unescaped. otherwise, these
+ ### parameters could constitute a CSS attack.
+ pass
+
+# obvious things here. note that we don't need uppercase for alpha.
+_re_validate_alpha = re.compile('^[a-z]+$')
+_re_validate_number = re.compile('^[0-9]+$')
+
+# when comparing two revs, we sometimes construct REV:SYMBOL, so ':' is needed
+_re_validate_revnum = re.compile('^[-_.a-zA-Z0-9:]*$')
+
+# it appears that RFC 2045 also says these chars are legal: !#$%&'*+^{|}~`
+# but woah... I'll just leave them out for now
+_re_validate_mimetype = re.compile('^[-_.a-zA-Z0-9/]+$')
+
+# the legal query parameters and their validation functions
+_legal_params = {
+ 'root' : None,
+ 'view' : None,
+ 'search' : _validate_regex,
+ 'p1' : None,
+ 'p2' : None,
+
+ 'hideattic' : _re_validate_number,
+ 'sortby' : _re_validate_alpha,
+ 'sortdir' : _re_validate_alpha,
+ 'logsort' : _re_validate_alpha,
+ 'diff_format' : _re_validate_alpha,
+ 'only_with_tag' : _re_validate_revnum,
+ 'dir_pagestart' : _re_validate_number,
+ 'log_pagestart' : _re_validate_number,
+ 'hidecvsroot' : _re_validate_number,
+ 'annotate' : _re_validate_revnum,
+ 'graph' : _re_validate_revnum,
+ 'makeimage' : _re_validate_number,
+ 'tarball' : _re_validate_number,
+ 'r1' : _re_validate_revnum,
+ 'tr1' : _re_validate_revnum,
+ 'r2' : _re_validate_revnum,
+ 'tr2' : _re_validate_revnum,
+ 'rev' : _re_validate_revnum,
+ 'content-type' : _re_validate_mimetype,
+ }
+
+# regex used to move from a file to a directory
+_re_up_path = re.compile('(^|/)[^/]+$')
+_re_up_attic_path = re.compile('(^|/)(Attic/)?[^/]+$')
+def get_up_path(request, path, hideattic=0):
+ if request.roottype == 'svn' or hideattic:
+ return re.sub(_re_up_path, '', path)
+ else:
+ return re.sub(_re_up_attic_path, '', path)
+
+def _strip_suffix(suffix, where, path_parts, pathtype, repos, view_func):
+ """strip the suffix from a repository path if the resulting path
+ is of the specified type, otherwise return None"""
+ l = len(suffix)
+ if where[-l:] == suffix:
+ path_parts = path_parts[:]
+ path_parts[-1] = path_parts[-1][:-l]
+ t = _repos_pathtype(repos, path_parts)
+ if pathtype == t:
+ return where[:-l], path_parts, t, view_func
+ return None
+
+def _repos_pathtype(repos, path_parts):
+ """return the type of a repository path, or None if the path
+ does not exist"""
+ type = None
+ try:
+ type = repos.itemtype(path_parts)
+ except vclib.ItemNotFound:
+ pass
+ return type
+
+def generate_page(request, tname, data):
+ # allow per-language template selection
+ if request:
+ tname = string.replace(tname, '%lang%', request.language)
+ else:
+ tname = string.replace(tname, '%lang%', 'en')
+
+ debug.t_start('ezt-parse')
+ template = ezt.Template(os.path.join(g_install_dir, tname))
+ debug.t_end('ezt-parse')
+
+ template.generate(sys.stdout, data)
+
+def html_footer(request):
+ data = common_template_data(request)
+
+ # generate the footer
+ generate_page(request, cfg.templates.footer, data)
+
+def clickable_path(request, leaf_is_link, drop_leaf):
+ where = ''
+ s = '<a href="%s#dirlist">[%s]</a>' % (_dir_url(request, where),
+ request.repos.name)
+
+ for part in request.path_parts[:-1]:
+ if where: where = where + '/'
+ where = where + part
+ s = s + ' / <a href="%s#dirlist">%s</a>' % (_dir_url(request, where), part)
+
+ if not drop_leaf and request.path_parts:
+ if leaf_is_link:
+ s = s + ' / %s' % (request.path_parts[-1])
+ else:
+ if request.pathtype == vclib.DIR:
+ url = request.get_url(view_func=view_directory, params={}) + '#dirlist'
+ else:
+ url = request.get_url(view_func=view_log, params={})
+ s = s + ' / <a href="%s">%s</a>' % (url, request.path_parts[-1])
+
+ return s
+
+def _dir_url(request, where):
+ """convenient wrapper for get_url used by clickable_path()"""
+ return request.get_url(view_func=view_directory, where=where,
+ pathtype=vclib.DIR, params={})
+
+
+def prep_tags(request, tags):
+ url, params = request.get_link(params={'only_with_tag': None})
+ params = compat.urlencode(params)
+ if params:
+ url = urllib.quote(url, _URL_SAFE_CHARS) + '?' + params + '&only_with_tag='
+ else:
+ url = urllib.quote(url, _URL_SAFE_CHARS) + '?only_with_tag='
+
+ links = [ ]
+ for tag in tags:
+ links.append(_item(name=tag, href=url+tag))
+ return links
+
+def is_viewable_image(mime_type):
+ return mime_type in ('image/gif', 'image/jpeg', 'image/png')
+
+def is_text(mime_type):
+ return mime_type[:5] == 'text/'
+
+_re_rewrite_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)(://[-a-zA-Z0-9%.~:_/]+)([?&]([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*(#([-a-zA-Z0-9%.~:_]+)?)?)')
+_re_rewrite_email = re.compile('([-a-zA-Z0-9_.]+@([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})')
+def htmlify(html):
+ html = cgi.escape(html)
+ html = re.sub(_re_rewrite_url, r'<a href="\1">\1</a>', html)
+ html = re.sub(_re_rewrite_email, r'<a href="mailto:\1">\1</a>', html)
+ return html
+
+def format_log(log):
+ s = htmlify(log[:cfg.options.short_log_len])
+ if len(log) > cfg.options.short_log_len:
+ s = s + '...'
+ return s
+
+_time_desc = {
+ 1 : 'second',
+ 60 : 'minute',
+ 3600 : 'hour',
+ 86400 : 'day',
+ 604800 : 'week',
+ 2628000 : 'month',
+ 31536000 : 'year',
+ }
+
+def get_time_text(request, interval, num):
+ "Get some time text, possibly internationalized."
+ ### some languages have even harder pluralization rules. we'll have to
+ ### deal with those on demand
+ if num == 0:
+ return ''
+ text = _time_desc[interval]
+ if num == 1:
+ attr = text + '_singular'
+ fmt = '%d ' + text
+ else:
+ attr = text + '_plural'
+ fmt = '%d ' + text + 's'
+ try:
+ fmt = getattr(request.kv.i18n.time, attr)
+ except AttributeError:
+ pass
+ return fmt % num
+
+def little_time(request):
+ try:
+ return request.kv.i18n.time.little_time
+ except AttributeError:
+ return 'very little time'
+
+def html_time(request, secs, extended=0):
+ secs = long(time.time()) - secs
+ if secs < 2:
+ return little_time(request)
+ breaks = _time_desc.keys()
+ breaks.sort()
+ i = 0
+ while i < len(breaks):
+ if secs < 2 * breaks[i]:
+ break
+ i = i + 1
+ value = breaks[i - 1]
+ s = get_time_text(request, value, secs / value)
+
+ if extended and i > 1:
+ secs = secs % value
+ value = breaks[i - 2]
+ ext = get_time_text(request, value, secs / value)
+ if ext:
+ ### this is not i18n compatible. pass on it for now
+ s = s + ', ' + ext
+ return s
+
+def common_template_data(request):
+ data = {
+ 'cfg' : cfg,
+ 'vsn' : __version__,
+ 'kv' : request.kv,
+ 'icons' : cfg.options.icons,
+ 'docroot' : cfg.options.docroot is None \
+ and request.script_name + '/' + docroot_magic_path \
+ or cfg.options.docroot,
+ }
+ return data
+
+def nav_header_data(request, rev):
+ path, filename = os.path.split(request.where)
+ if request.roottype == 'cvs' and path[-6:] == '/Attic':
+ path = path[:-6]
+
+ data = common_template_data(request)
+ data.update({
+ 'nav_path' : clickable_path(request, 1, 0),
+ 'path' : path,
+ 'filename' : filename,
+ 'file_url' : request.get_url(view_func=view_log, params={}),
+ 'rev' : rev
+ })
+ return data
+
+def copy_stream(fp):
+ while 1:
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ break
+ sys.stdout.write(chunk)
+
+def read_stream(fp):
+ string = ''
+ while 1:
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ break
+ else:
+ string = string + chunk
+ return string
+
+def markup_stream_default(fp):
+ print '<pre>'
+ while 1:
+ ### technically, the htmlify() could fail if something falls across
+ ### the chunk boundary. TFB.
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ break
+ sys.stdout.write(htmlify(chunk))
+ print '</pre>'
+
+def markup_stream_python(fp):
+ ### convert this code to use the recipe at:
+ ### http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52298
+ ### note that the cookbook states all the code is licensed according to
+ ### the Python license.
+ try:
+ # see if Marc-Andre Lemburg's py2html stuff is around
+ # http://starship.python.net/crew/lemburg/SoftwareDescriptions.html#py2html.py
+ ### maybe restrict the import to *only* this directory?
+ sys.path.insert(0, cfg.options.py2html_path)
+ import py2html
+ import PyFontify
+ except ImportError:
+ # fall back to the default streamer
+ markup_stream_default(fp)
+ else:
+ ### it doesn't escape stuff quite right, nor does it munge URLs and
+ ### mailtos as well as we do.
+ html = cgi.escape(fp.read())
+ pp = py2html.PrettyPrint(PyFontify.fontify, "rawhtml", "color")
+ html = pp.fontify(html)
+ html = re.sub(_re_rewrite_url, r'<a href="\1">\1</a>', html)
+ html = re.sub(_re_rewrite_email, r'<a href="mailto:\1">\1</a>', html)
+ sys.stdout.write(html)
+
+def markup_stream_php(fp):
+ sys.stdout.flush()
+
+ os.putenv("SERVER_SOFTWARE", "")
+ os.putenv("SERVER_NAME", "")
+ os.putenv("GATEWAY_INTERFACE", "")
+ os.putenv("REQUEST_METHOD", "")
+ php = popen.pipe_cmds([["php","-q"]])
+
+ php.write("<?\n$file = '';\n")
+
+ while 1:
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ if fp.eof() is None:
+ time.sleep(1)
+ continue
+ break
+ php.write("$file .= '")
+ php.write(string.replace(string.replace(chunk, "\\", "\\\\"),"'","\\'"))
+ php.write("';\n")
+
+ php.write("\n\nhighlight_string($file);\n?>")
+ php.close()
+
+def markup_stream_enscript(lang, fp):
+ sys.stdout.flush()
+ # I've tried to pass option '-C' to enscript to generate line numbers
+ # Unfortunately this option doesn'nt work with HTML output in enscript
+ # version 1.6.2.
+ enscript = popen.pipe_cmds([(os.path.normpath(os.path.join(cfg.options.enscript_path,'enscript')),
+ '--color', '--language=html',
+ '--pretty-print=' + lang, '-o',
+ '-', '-'),
+ ('sed', '-n', '/^<PRE>$/,/<\\/PRE>$/p')])
+
+ try:
+ while 1:
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ if fp.eof() is None:
+ time.sleep(1)
+ continue
+ break
+ enscript.write(chunk)
+ except IOError:
+ print "<h3>Failure during use of an external program:</h3>"
+ print "The command line was:"
+ print "<pre>"
+ print os.path.normpath(os.path.join(cfg.options.enscript_path,'enscript')
+ ) + " --color --language=html --pretty-print="+lang+" -o - -"
+ print "</pre>"
+ print "Please look at the error log of your webserver for more info."
+ raise
+
+ enscript.close()
+ if sys.platform != "win32":
+ os.wait()
+
+def markup_stream_rst(fp):
+ import locale
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except:
+ pass
+
+ try:
+ import docutils.readers.standalone
+
+ class ViewCVSReader(docutils.readers.standalone.Reader):
+
+ def __init__(self, fp):
+ docutils.readers.Reader.__init__(self, parser=None, parser_name='restructuredtext')
+ self.fp = fp
+
+ def read(self, source, parser, settings):
+ self.source = source
+ if not self.parser:
+ self.parser = parser
+ self.settings = settings
+ #self.input = self.source.read()
+ # read all at once
+ self.input = read_stream(self.fp)
+ self.parse()
+ return self.document
+
+ from docutils.core import publish_file
+ publish_file(reader=ViewCVSReader(fp), writer_name='html')
+ except:
+ raise
+
+markup_streamers = {
+# '.py' : markup_stream_python,
+# '.php' : markup_stream_php,
+# '.inc' : markup_stream_php,
+ '.txt' : markup_stream_rst
+ }
+
+### this sucks... we have to duplicate the extensions defined by enscript
+enscript_extensions = {
+ '.C' : 'cpp',
+ '.EPS' : 'postscript',
+ '.DEF' : 'modula_2', # requires a patch for enscript 1.6.2, see INSTALL
+ '.F' : 'fortran',
+ '.H' : 'cpp',
+ '.MOD' : 'modula_2', # requires a patch for enscript 1.6.2, see INSTALL
+ '.PS' : 'postscript',
+ '.S' : 'asm',
+ '.SH' : 'sh',
+ '.ada' : 'ada',
+ '.adb' : 'ada',
+ '.ads' : 'ada',
+ '.awk' : 'awk',
+ '.c' : 'c',
+ '.c++' : 'cpp',
+ '.cc' : 'cpp',
+ '.cpp' : 'cpp',
+ '.csh' : 'csh',
+ '.cxx' : 'cpp',
+ '.diff' : 'diffu',
+ '.dpr' : 'delphi',
+ '.el' : 'elisp',
+ '.eps' : 'postscript',
+ '.f' : 'fortran',
+ '.for': 'fortran',
+ '.gs' : 'haskell',
+ '.h' : 'c',
+ '.hpp' : 'cpp',
+ '.hs' : 'haskell',
+ '.htm' : 'html',
+ '.html' : 'html',
+ '.idl' : 'idl',
+ '.java' : 'java',
+ '.js' : 'javascript',
+ '.lgs' : 'haskell',
+ '.lhs' : 'haskell',
+ '.m' : 'objc',
+ '.m4' : 'm4',
+ '.man' : 'nroff',
+ '.nr' : 'nroff',
+ '.p' : 'pascal',
+ # classic setting:
+ # '.pas' : 'pascal',
+ # most people using pascal today are using the Delphi system originally
+ # brought to us as Turbo-Pascal during the eighties of the last century:
+ '.pas' : 'delphi',
+ # ---
+ '.patch' : 'diffu',
+ # For Oracle sql packages. The '.pkg' extension might be used for other
+ # file types, adjust here if necessary.
+ '.pkg' : 'sql',
+ '.pl' : 'perl',
+ '.pm' : 'perl',
+ '.pp' : 'pascal',
+ '.ps' : 'postscript',
+ '.s' : 'asm',
+ '.scheme' : 'scheme',
+ '.scm' : 'scheme',
+ '.scr' : 'synopsys',
+ '.sh' : 'sh',
+ '.shtml' : 'html',
+ '.sql' : 'sql',
+ '.st' : 'states',
+ '.syn' : 'synopsys',
+ '.synth' : 'synopsys',
+ '.tcl' : 'tcl',
+ '.tex' : 'tex',
+ '.texi' : 'tex',
+ '.texinfo' : 'tex',
+ '.v' : 'verilog',
+ '.vba' : 'vba',
+ '.vh' : 'verilog',
+ '.vhd' : 'vhdl',
+ '.vhdl' : 'vhdl',
+
+ ### use enscript or py2html?
+ '.py' : 'python',
+ }
+enscript_filenames = {
+ '.emacs' : 'elisp',
+ 'GNUmakefile' : 'makefile',
+ 'Makefile' : 'makefile',
+ 'makefile' : 'makefile',
+ 'ChangeLog' : 'changelog',
+ }
+
+
+def make_time_string(date):
+ """Returns formatted date string in either local time or UTC.
+
+ The passed in 'date' variable is seconds since epoch.
+
+ """
+ if (cfg.options.use_localtime):
+ localtime = time.localtime(date)
+ return time.asctime(localtime) + ' ' + time.tzname[localtime[8]]
+ else:
+ return time.asctime(time.gmtime(date)) + ' UTC'
+
+def view_auto(request):
+ if request.default_viewable:
+ view_markup(request)
+ else:
+ view_checkout(request)
+
+def view_markup(request):
+ full_name = request.full_name
+ where = request.where
+ query_dict = request.query_dict
+ rev = request.query_dict.get('rev')
+
+ fp, revision = request.repos.openfile(request.path_parts, rev)
+
+ data = nav_header_data(request, revision)
+ data.update({
+ 'nav_file' : clickable_path(request, 1, 0),
+ 'href' : request.get_url(view_func=view_checkout, params={}),
+ 'text_href' : request.get_url(view_func=view_checkout,
+ params={'content-type': 'text/plain'}),
+ 'mime_type' : request.mime_type,
+ 'log' : None,
+ })
+
+ if cfg.options.show_log_in_markup:
+ if request.roottype == 'cvs':
+ show_revs, rev_map, rev_order, taginfo, rev2tag, \
+ cur_branch, branch_points, branch_names = read_log(full_name)
+ entry = rev_map[revision]
+
+ idx = string.rfind(revision, '.')
+ branch = revision[:idx]
+
+ entry.date_str = make_time_string(entry.date)
+
+ data.update({
+ 'roottype' : 'cvs',
+ 'date_str' : entry.date_str,
+ 'ago' : html_time(request, entry.date, 1),
+ 'author' : entry.author,
+ 'branches' : None,
+ 'tags' : None,
+ 'branch_points' : None,
+ 'changed' : entry.changed,
+ 'log' : htmlify(entry.log),
+ 'size' : None,
+ 'state' : entry.state,
+ 'vendor_branch' : ezt.boolean(_re_is_vendor_branch.match(revision)),
+ })
+
+ if rev2tag.has_key(branch):
+ data['branches'] = string.join(rev2tag[branch], ', ')
+ if rev2tag.has_key(revision):
+ data['tags'] = string.join(rev2tag[revision], ', ')
+ if branch_points.has_key(revision):
+ data['branch_points'] = string.join(branch_points[revision], ', ')
+
+ prev_rev = string.split(revision, '.')
+ while 1:
+ if prev_rev[-1] == '0': # .0 can be caused by 'commit -r X.Y.Z.0'
+ prev_rev = prev_rev[:-2] # X.Y.Z.0 becomes X.Y.Z
+ else:
+ prev_rev[-1] = str(int(prev_rev[-1]) - 1)
+ prev = string.join(prev_rev, '.')
+ if rev_map.has_key(prev) or prev == '':
+ break
+ data['prev'] = prev
+ elif request.roottype == 'svn':
+ alltags, logs = vclib.svn.fetch_log(request.repos, where)
+ this_rev = int(revision)
+ entry = logs[this_rev]
+
+ data.update({
+ 'roottype' : 'svn',
+ 'date_str' : make_time_string(entry.date),
+ 'ago' : html_time(request, entry.date, 1),
+ 'author' : entry.author,
+ 'branches' : None,
+ 'tags' : None,
+ 'branch_points' : None,
+ 'changed' : entry.changed,
+ 'log' : htmlify(entry.log),
+ 'state' : entry.state,
+ 'size' : entry.size,
+ 'vendor_branch' : ezt.boolean(0),
+ })
+
+ revs = logs.keys()
+ revs.sort()
+ rev_idx = revs.index(this_rev)
+ if rev_idx > 0:
+ data['prev'] = str(revs[rev_idx - 1])
+ else:
+ data['prev'] = None
+
+ data['tag'] = query_dict.get('only_with_tag')
+
+ request.server.header()
+ generate_page(request, cfg.templates.markup, data)
+
+ if is_viewable_image(request.mime_type):
+ url = request.get_url(view_func=view_checkout, params={})
+ print '<img src="%s"><br>' % url
+ while fp.read(8192):
+ pass
+ else:
+ basename, ext = os.path.splitext(data['filename'])
+ streamer = markup_streamers.get(ext)
+ if streamer:
+ streamer(fp)
+ elif not cfg.options.use_enscript:
+ markup_stream_default(fp)
+ else:
+ lang = enscript_extensions.get(ext)
+ if not lang:
+ lang = enscript_filenames.get(basename)
+ if lang and lang not in cfg.options.disable_enscript_lang:
+ markup_stream_enscript(lang, fp)
+ else:
+ markup_stream_default(fp)
+ status = fp.close()
+ if status:
+ raise 'pipe error status: %d' % status
+ html_footer(request)
+
+def revcmp(rev1, rev2):
+ rev1 = map(int, string.split(rev1, '.'))
+ rev2 = map(int, string.split(rev2, '.'))
+ return cmp(rev1, rev2)
+
+def prepare_hidden_values(params):
+ """returns variables from params encoded as a invisible HTML snippet.
+ """
+ hidden_values = []
+ for name, value in params.items():
+ hidden_values.append('<input type="hidden" name="%s" value="%s" />' %
+ (name, value))
+ return string.join(hidden_values, '')
+
+def sort_file_data(file_data, sortdir, sortby):
+ def file_sort_cmp(file1, file2, sortby=sortby):
+ if file1.kind == vclib.DIR: # is_directory
+ if file2.kind == vclib.DIR:
+ # both are directories. sort on name.
+ return cmp(file1.name, file2.name)
+ # file1 is a directory, it sorts first.
+ return -1
+ if file2.kind == vclib.DIR:
+ # file2 is a directory, it sorts first.
+ return 1
+
+ # we should have data on these. if not, then it is because we requested
+ # a specific tag and that tag is not present on the file.
+ info1 = file1.rev or bincvs._FILE_HAD_ERROR
+ info2 = file2.rev or bincvs._FILE_HAD_ERROR
+ if info1 != bincvs._FILE_HAD_ERROR and info2 != bincvs._FILE_HAD_ERROR:
+ # both are files, sort according to sortby
+ if sortby == 'rev':
+ return revcmp(file1.rev, file2.rev)
+ elif sortby == 'date':
+ return cmp(file2.date, file1.date) # latest date is first
+ elif sortby == 'log':
+ return cmp(file1.log, file2.log)
+ elif sortby == 'author':
+ return cmp(file1.author, file2.author)
+ else:
+ # sort by file name
+ return cmp(file1.name, file2.name)
+
+ # at this point only one of file1 or file2 are _FILE_HAD_ERROR.
+ if info1 != bincvs._FILE_HAD_ERROR:
+ return -1
+
+ return 1
+
+ file_data.sort(file_sort_cmp)
+
+ if sortdir == "down":
+ file_data.reverse()
+
+def view_directory(request):
+ # if we have a directory and the request didn't end in "/", then redirect
+ # so that it does.
+ if request.server.getenv('PATH_INFO', '')[-1:] != '/':
+ request.server.redirect(request.get_url())
+
+ sortby = request.query_dict.get('sortby', cfg.options.sort_by) or 'file'
+ sortdir = request.query_dict.get('sortdir', 'up')
+
+ # prepare the data that will be passed to the template
+ data = common_template_data(request)
+ data.update({
+ 'roottype' : request.roottype,
+ 'where' : request.where,
+ 'current_root' : request.repos.name,
+ 'sortby' : sortby,
+ 'sortdir' : sortdir,
+ 'no_match' : None,
+ 'unreadable' : None,
+ 'tarball_href' : None,
+ 'search_re' : None,
+ 'dir_pagestart' : None,
+ 'have_logs' : 'yes',
+ 'sortby_file_href' : request.get_url(params={'sortby': 'file'}),
+ 'sortby_rev_href' : request.get_url(params={'sortby': 'rev'}),
+ 'sortby_date_href' : request.get_url(params={'sortby': 'date'}),
+ 'sortby_author_href' : request.get_url(params={'sortby': 'author'}),
+ 'sortby_log_href' : request.get_url(params={'sortby': 'log'}),
+ 'sortdir_down_href' : request.get_url(params={'sortdir': 'down'}),
+ 'sortdir_up_href' : request.get_url(params={'sortdir': 'up'}),
+
+ ### in the future, it might be nice to break this path up into
+ ### a list of elements, allowing the template to display it in
+ ### a variety of schemes.
+ 'nav_path' : clickable_path(request, 0, 0),
+ })
+
+ if not request.where:
+ url, params = request.get_link(params={'root': None})
+ data['change_root_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['change_root_hidden_values'] = prepare_hidden_values(params)
+
+ # add in the roots for the selection
+ allroots = list_roots(cfg)
+ if len(allroots) < 2:
+ roots = [ ]
+ else:
+ roots = allroots.keys()
+ roots.sort(lambda n1, n2: cmp(string.lower(n1), string.lower(n2)))
+ data['roots'] = roots
+
+ if cfg.options.use_pagesize:
+ url, params = request.get_link(params={'dir_pagestart': None})
+ data['dir_paging_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['dir_paging_hidden_values'] = prepare_hidden_values(params)
+
+ if cfg.options.allow_tar:
+ data['tarball_href'] = request.get_url(view_func=download_tarball,
+ params={})
+
+ if request.roottype == 'svn':
+ view_directory_svn(request, data, sortby, sortdir)
+ else:
+ view_directory_cvs(request, data, sortby, sortdir)
+
+ if cfg.options.use_pagesize:
+ data['dir_pagestart'] = int(query_dict.get('dir_pagestart',0))
+ data['rows'] = paging(data, 'rows', data['dir_pagestart'], 'name')
+
+ request.server.header()
+ generate_page(request, cfg.templates.directory, data)
+
+def view_directory_cvs(request, data, sortby, sortdir):
+ where = request.where
+ query_dict = request.query_dict
+
+ view_tag = query_dict.get('only_with_tag')
+ hideattic = int(query_dict.get('hideattic', cfg.options.hide_attic))
+
+ search_re = query_dict.get('search', '')
+
+ # Search current directory
+ file_data = request.repos.listdir(request.path_parts,
+ not hideattic or view_tag)
+
+ if cfg.options.use_re_search and search_re:
+ file_data = search_files(request.repos, request.path_parts,
+ file_data, search_re)
+
+ get_dirs = cfg.options.show_subdir_lastmod and cfg.options.show_logs
+
+ bincvs.get_logs(request.repos, request.path_parts,
+ file_data, view_tag, get_dirs)
+
+ has_tags = view_tag or request.repos.branch_tags or request.repos.plain_tags
+
+ # prepare the data that will be passed to the template
+ data.update({
+ 'view_tag' : view_tag,
+ 'attic_showing' : ezt.boolean(not hideattic),
+ 'show_attic_href' : request.get_url(params={'hideattic': 0}),
+ 'hide_attic_href' : request.get_url(params={'hideattic': 1}),
+ 'has_tags' : ezt.boolean(has_tags),
+ ### one day, if EZT has "or" capability, we can lose this
+ 'selection_form' : ezt.boolean(has_tags or cfg.options.use_re_search),
+ 'branch_tags': request.repos.branch_tags,
+ 'plain_tags': request.repos.plain_tags,
+ })
+
+ if search_re:
+ data['search_re'] = htmlify(search_re)
+
+ # sort with directories first, and using the "sortby" criteria
+ sort_file_data(file_data, sortdir, sortby)
+
+ num_files = 0
+ num_displayed = 0
+ unreadable = 0
+ have_logs = 0
+ rows = data['rows'] = [ ]
+
+ where_prefix = where and where + '/'
+
+ ### display a row for ".." ?
+ for file in file_data:
+ row = _item(href=None, graph_href=None,
+ author=None, log=None, log_file=None, log_rev=None,
+ show_log=None, state=None)
+
+ if file.rev == bincvs._FILE_HAD_ERROR:
+ row.cvs = 'error'
+ unreadable = 1
+ elif file.rev is None:
+ row.cvs = 'none'
+ else:
+ row.cvs = 'data'
+ row.rev = file.rev
+ row.author = file.author
+ row.state = file.state
+ row.time = html_time(request, file.date)
+ if cfg.options.show_logs and file.log:
+ row.show_log = 'yes'
+ row.log = format_log(file.log)
+ have_logs = 1
+
+ row.anchor = file.name
+ row.name = file.name
+
+ row.type = (file.kind == vclib.FILE and 'file') or \
+ (file.kind == vclib.DIR and 'dir')
+
+ if file.verboten or not row.type:
+ row.type = 'unreadable'
+ unreadable = 1
+
+ if file.kind == vclib.DIR:
+ if not hideattic and file.name == 'Attic':
+ continue
+ if where == '' and ((file.name == 'CVSROOT' and cfg.options.hide_cvsroot)
+ or cfg.is_forbidden(file.name)):
+ continue
+ if file.name == 'CVS': # CVS directory in repository is for fileattr.
+ continue
+
+ row.href = request.get_url(view_func=view_directory,
+ where=where_prefix+file.name,
+ pathtype=vclib.DIR,
+ params={})
+
+ if row.cvs == 'data':
+ if cfg.options.use_cvsgraph:
+ row.graph_href = '&nbsp;'
+ if cfg.options.show_logs:
+ row.log_file = file.newest_file
+ row.log_rev = file.rev
+
+ elif file.kind == vclib.FILE:
+ num_files = num_files + 1
+
+ if file.rev is None and not file.verboten:
+ continue
+ elif hideattic and view_tag and file.state == 'dead':
+ continue
+ num_displayed = num_displayed + 1
+
+ file_where = where_prefix + (file.in_attic and 'Attic/' or '') + file.name
+
+ row.href = request.get_url(view_func=view_log,
+ where=file_where,
+ pathtype=vclib.FILE,
+ params={})
+
+ row.rev_href = request.get_url(view_func=view_auto,
+ where=file_where,
+ pathtype=vclib.FILE,
+ params={'rev': str(file.rev)})
+
+ if cfg.options.use_cvsgraph:
+ row.graph_href = request.get_url(view_func=view_cvsgraph,
+ where=file_where,
+ pathtype=vclib.FILE,
+ params={})
+
+ rows.append(row)
+
+ data.update({
+ 'num_files' : num_files,
+ 'files_shown' : num_displayed,
+ 'no_match' : num_files and not num_displayed and 'yes' or '',
+ 'unreadable' : unreadable and 'yes' or '',
+
+ # have_logs will be 0 if we only have dirs and !show_subdir_lastmod.
+ # in that case, we don't need the extra columns
+ 'have_logs' : have_logs and 'yes' or '',
+ })
+
+ if data['selection_form']:
+ url, params = request.get_link(params={'only_with_tag': None,
+ 'search': None})
+ data['search_tag_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['search_tag_hidden_values'] = prepare_hidden_values(params)
+
+
+def view_directory_svn(request, data, sortby, sortdir):
+ query_dict = request.query_dict
+ where = request.where
+
+ file_data = request.repos.listdir(request.path_parts)
+ vclib.svn.get_logs(request.repos, where, file_data)
+
+ data.update({
+ 'view_tag' : None,
+ 'tree_rev' : str(request.repos.rev),
+ 'has_tags' : ezt.boolean(0),
+ 'selection_form' : ezt.boolean(0)
+ })
+
+ if request.query_dict.has_key('rev'):
+ data['jump_rev'] = request.query_dict['rev']
+ else:
+ data['jump_rev'] = str(request.repos.rev)
+
+ url, params = request.get_link(params={'rev': None})
+ data['jump_rev_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['jump_rev_hidden_values'] = prepare_hidden_values(params)
+
+ # sort with directories first, and using the "sortby" criteria
+ sort_file_data(file_data, sortdir, sortby)
+
+ num_files = 0
+ num_displayed = 0
+ unreadable = 0
+ rows = data['rows'] = [ ]
+
+ where_prefix = where and where + '/'
+ dir_params = {'rev': query_dict.get('rev')}
+
+ for file in file_data:
+ row = _item(href=None, graph_href=None,
+ author=None, log=None, log_file=None, log_rev=None,
+ show_log=None, state=None)
+
+ row.rev = file.rev
+ row.author = file.author or "&nbsp;"
+ row.state = ''
+ row.time = html_time(request, file.date)
+ row.anchor = file.name
+
+ if file.kind == vclib.DIR:
+ row.type = 'dir'
+ row.name = file.name
+ row.cvs = 'none' # What the heck is this?
+ row.href = request.get_url(view_func=view_directory,
+ where=where_prefix + file.name,
+ pathtype=vclib.DIR,
+ params=dir_params)
+ else:
+ row.type = 'file'
+ row.name = file.name
+ row.cvs = 'data' # What the heck is this?
+
+ row.href = request.get_url(view_func=view_log,
+ where=where_prefix + file.name,
+ pathtype=vclib.FILE,
+ params={})
+
+ row.rev_href = request.get_url(view_func=view_auto,
+ where=where_prefix + file.name,
+ pathtype=vclib.FILE,
+ params={'rev': str(row.rev)})
+
+ num_files = num_files + 1
+ num_displayed = num_displayed + 1
+ if cfg.options.show_logs:
+ row.show_log = 'yes'
+ row.log = format_log(file.log or "")
+
+ rows.append(row)
+
+ ### we need to fix the template w.r.t num_files. it usually is not a
+ ### correct (original) count of the files available for selecting
+ data['num_files'] = num_files
+
+ # the number actually displayed
+ data['files_shown'] = num_displayed
+
+def paging(data, key, pagestart, local_name):
+ # Implement paging
+ # Create the picklist
+ picklist = data['picklist'] = []
+ for i in range(0, len(data[key]), cfg.options.use_pagesize):
+ pick = _item(start=None, end=None, count=None)
+ pick.start = getattr(data[key][i], local_name)
+ pick.count = i
+ pick.page = (i / cfg.options.use_pagesize) + 1
+ try:
+ pick.end = getattr(data[key][i+cfg.options.use_pagesize-1], local_name)
+ except IndexError:
+ pick.end = getattr(data[key][-1], local_name)
+ picklist.append(pick)
+ data['picklist_len'] = len(picklist)
+ # Need to fix
+ # pagestart can be greater than the length of data[key] if you
+ # select a tag or search while on a page other than the first.
+ # Should reset to the first page, this test won't do that every
+ # time that it is needed.
+ # Problem might go away if we don't hide non-matching files when
+ # selecting for tags or searching.
+ if pagestart > len(data[key]):
+ pagestart = 0
+ pageend = pagestart + cfg.options.use_pagesize
+ # Slice
+ return data[key][pagestart:pageend]
+
+def logsort_date_cmp(rev1, rev2):
+ # sort on date; secondary on revision number
+ return -cmp(rev1.date, rev2.date) or -revcmp(rev1.rev, rev2.rev)
+
+def logsort_rev_cmp(rev1, rev2):
+ # sort highest revision first
+ return -revcmp(rev1.rev, rev2.rev)
+
+def find_first_rev(taginfo, revs):
+ "Find first revision that matches a normal tag or is on a branch tag"
+ for rev in revs:
+ if taginfo.matches_rev(rev) or taginfo.holds_rev(rev):
+ return rev
+ return None
+
+def read_log(full_name, which_rev=None, view_tag=None, logsort='cvs'):
+ head, cur_branch, taginfo, revs = bincvs.fetch_log(cfg.general,
+ full_name, which_rev)
+
+ rev_order = map(lambda entry: entry.rev, revs)
+ rev_order.sort(revcmp)
+ rev_order.reverse()
+
+ # HEAD is an artificial tag which is simply the highest tag number on the
+ # main branch, unless there is a branch tag in the RCS file in which case
+ # it's the highest revision on that branch. Find it by looking through
+ # rev_order; it is the first commit listed on the appropriate branch.
+ # This is not neccesary the same revision as marked as head in the RCS file.
+ ### Why are we defining our own HEAD instead of just using the revision
+ ### marked as head? What's wrong with: taginfo['HEAD'] = bincvs.TagInfo(head)
+ taginfo['MAIN'] = bincvs.TagInfo(cur_branch)
+ taginfo['HEAD'] = find_first_rev(taginfo['MAIN'], rev_order)
+
+ # map revision numbers to tag names
+ rev2tag = { }
+
+ # names of symbols at each branch point
+ branch_points = { }
+
+ branch_names = [ ]
+
+ # Now that we know all of the revision numbers, we can associate
+ # absolute revision numbers with all of the symbolic names, and
+ # pass them to the form so that the same association doesn't have
+ # to be built then.
+
+ items = taginfo.items()
+ items.sort()
+ items.reverse()
+ for name, tag in items:
+ if not isinstance(tag, bincvs.TagInfo):
+ taginfo[name] = tag = bincvs.TagInfo(tag)
+
+ number = tag.number()
+
+ if tag.is_branch():
+ branch_names.append(name)
+
+ if number == cur_branch:
+ default_branch = name
+
+ if not tag.is_trunk():
+ rev = tag.branches_at()
+ if branch_points.has_key(rev):
+ branch_points[rev].append(name)
+ else:
+ branch_points[rev] = [ name ]
+
+ # revision number you'd get if you checked out this branch
+ tag.co_rev = find_first_rev(tag, rev_order)
+ else:
+ tag.co_rev = number
+
+ if rev2tag.has_key(number):
+ rev2tag[number].append(name)
+ else:
+ rev2tag[number] = [ name ]
+
+ if view_tag:
+ tag = taginfo.get(view_tag)
+ if not tag:
+ raise debug.ViewcvsException('Tag %s not defined.' % view_tag,
+ '404 Tag Not Found')
+
+ show_revs = [ ]
+ for entry in revs:
+ rev = entry.rev
+ if tag.matches_rev(rev) or tag.branches_at()==rev or tag.holds_rev(rev):
+ show_revs.append(entry)
+ else:
+ show_revs = revs
+
+ if logsort == 'date':
+ show_revs.sort(logsort_date_cmp)
+ elif logsort == 'rev':
+ show_revs.sort(logsort_rev_cmp)
+ else:
+ # no sorting
+ pass
+
+ # build a map of revision number to entry information
+ rev_map = { }
+ for entry in revs:
+ rev_map[entry.rev] = entry
+
+ ### some of this return stuff doesn't make a lot of sense...
+ return show_revs, rev_map, rev_order, taginfo, rev2tag, \
+ default_branch, branch_points, branch_names
+
+_re_is_vendor_branch = re.compile(r'^1\.1\.1\.\d+$')
+
+def augment_entry(entry, request, rev_map, rev2tag, branch_points,
+ rev_order, extended, name_printed):
+ "Augment the entry with additional, computed data from the log output."
+
+ query_dict = request.query_dict
+
+ rev = entry.rev
+ idx = string.rfind(rev, '.')
+ branch = rev[:idx]
+
+ entry.vendor_branch = ezt.boolean(_re_is_vendor_branch.match(rev))
+
+ entry.date_str = make_time_string(entry.date)
+
+ entry.ago = html_time(request, entry.date, 1)
+
+ entry.branches = prep_tags(request, rev2tag.get(branch, [ ]))
+ entry.tags = prep_tags(request, rev2tag.get(rev, [ ]))
+ entry.branch_points = prep_tags(request, branch_points.get(rev, [ ]))
+
+ prev_rev = string.split(rev, '.')
+ while 1:
+ if prev_rev[-1] == '0': # .0 can be caused by 'commit -r X.Y.Z.0'
+ prev_rev = prev_rev[:-2] # X.Y.Z.0 becomes X.Y.Z
+ else:
+ prev_rev[-1] = str(int(prev_rev[-1]) - 1)
+ prev = string.join(prev_rev, '.')
+ if rev_map.has_key(prev) or prev == '':
+ break
+ entry.prev = prev
+
+ ### maybe just overwrite entry.log?
+ entry.html_log = htmlify(entry.log)
+
+ if extended:
+ entry.tag_names = rev2tag.get(rev, [ ])
+ if rev2tag.has_key(branch) and not name_printed.has_key(branch):
+ entry.branch_names = rev2tag.get(branch)
+ name_printed[branch] = 1
+ else:
+ entry.branch_names = [ ]
+
+ entry.href = request.get_url(view_func=view_checkout, params={'rev': rev})
+ entry.view_href = request.get_url(view_func=view_markup,
+ params={'rev': rev})
+ entry.text_href = request.get_url(view_func=view_checkout,
+ params={'content-type': 'text/plain',
+ 'rev': rev})
+
+ entry.annotate_href = request.get_url(view_func=view_annotate,
+ params={'annotate': rev})
+
+ # figure out some target revisions for performing diffs
+ entry.branch_point = None
+ entry.next_main = None
+
+ idx = string.rfind(branch, '.')
+ if idx != -1:
+ branch_point = branch[:idx]
+
+ if not entry.vendor_branch \
+ and branch_point != rev and branch_point != prev:
+ entry.branch_point = branch_point
+
+ # if it's on a branch (and not a vendor branch), then diff against the
+ # next revision of the higher branch (e.g. change is committed and
+ # brought over to -stable)
+ if string.count(rev, '.') > 1 and not entry.vendor_branch:
+ # locate this rev in the ordered list of revisions
+ i = rev_order.index(rev)
+
+ # create a rev that can be compared component-wise
+ c_rev = string.split(rev, '.')
+
+ while i:
+ next = rev_order[i - 1]
+ c_work = string.split(next, '.')
+ if len(c_work) < len(c_rev):
+ # found something not on the branch
+ entry.next_main = next
+ break
+
+ # this is a higher version on the same branch; the lower one (rev)
+ # shouldn't have a diff against the "next main branch"
+ if c_work[:-1] == c_rev[:len(c_work) - 1]:
+ break
+
+ i = i - 1
+
+ # the template could do all these comparisons itself, but let's help
+ # it out.
+ r1 = query_dict.get('r1')
+ if r1 and r1 != rev and r1 != prev and r1 != entry.branch_point \
+ and r1 != entry.next_main:
+ entry.to_selected = 'yes'
+ else:
+ entry.to_selected = None
+
+def view_log(request):
+ diff_format = request.query_dict.get('diff_format', cfg.options.diff_format)
+ logsort = request.query_dict.get('logsort', cfg.options.log_sort)
+
+ data = common_template_data(request)
+ data.update({
+ 'roottype' : request.roottype,
+ 'current_root' : request.repos.name,
+ 'where' : request.where,
+ 'nav_path' : clickable_path(request, 1, 0),
+ 'branch' : None,
+ 'mime_type' : request.mime_type,
+ 'rev_selected' : request.query_dict.get('r1'),
+ 'path_selected' : request.query_dict.get('p1'),
+ 'diff_format' : diff_format,
+ 'logsort' : logsort,
+ 'viewable' : ezt.boolean(request.default_viewable),
+ 'is_text' : ezt.boolean(is_text(request.mime_type)),
+ 'human_readable' : ezt.boolean(diff_format in ('h', 'l')),
+ 'log_pagestart' : None,
+ 'graph_href' : None,
+ })
+
+ url, params = request.get_link(view_func=view_diff,
+ params={'r1': None, 'r2': None,
+ 'diff_format': None})
+ params = compat.urlencode(params)
+ data['diff_url'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['diff_params'] = params and '&' + params
+
+ if cfg.options.use_pagesize:
+ url, params = request.get_link(params={'log_pagestart': None})
+ data['log_paging_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['log_paging_hidden_values'] = prepare_hidden_values(params)
+
+ url, params = request.get_link(params={'r1': None, 'r2': None, 'tr1': None,
+ 'tr2': None, 'diff_format': None})
+ data['diff_select_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['diff_select_hidden_values'] = prepare_hidden_values(params)
+
+ url, params = request.get_link(params={'logsort': None})
+ data['logsort_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['logsort_hidden_values'] = prepare_hidden_values(params)
+
+ if request.roottype == 'svn':
+ view_log_svn(request, data, logsort)
+ else:
+ view_log_cvs(request, data, logsort)
+
+def view_log_svn(request, data, logsort):
+ query_dict = request.query_dict
+
+ alltags, logs = vclib.svn.fetch_log(request.repos, request.where)
+ up_where, filename = os.path.split(request.where)
+
+ entries = []
+ prev_rev = None
+ prev_path = None
+ show_revs = logs.keys()
+ show_revs.sort()
+ for rev in show_revs:
+ entry = logs[rev]
+ entry.prev = prev_rev
+ entry.prev_path = prev_path
+ entry.href = request.get_url(view_func=view_checkout, where=entry.filename,
+ pathtype=vclib.FILE, params={'rev': rev})
+ entry.view_href = request.get_url(view_func=view_markup,
+ where=entry.filename,
+ pathtype=vclib.FILE,
+ params={'rev': rev})
+ entry.text_href = request.get_url(view_func=view_checkout,
+ where=entry.filename,
+ pathtype=vclib.FILE,
+ params={'content-type': 'text/plain',
+ 'rev': rev})
+ entry.revision_href = request.get_url(view_func=view_revision,
+ where=None,
+ pathtype=None,
+ params={'rev': rev})
+
+ if entry.copy_path:
+ entry.copy_href = request.get_url(view_func=view_log,
+ where=entry.copy_path,
+ pathtype=vclib.FILE, params={})
+
+ entry.tags = [ ]
+ entry.branches = [ ]
+ entry.branch_point = None
+ entry.branch_points = [ ]
+ entry.next_main = None
+ entry.to_selected = None
+ entry.vendor_branch = None
+ entry.ago = html_time(request, entry.date, 1)
+ entry.date_str = make_time_string(entry.date)
+ entry.tag_names = [ ]
+ entry.branch_names = [ ]
+ if not entry.log:
+ entry.log = ""
+ entry.html_log = htmlify(entry.log)
+
+ # the template could do all these comparisons itself, but let's help
+ # it out.
+ r1 = query_dict.get('r1')
+ if r1 and r1 != str(rev) and r1 != str(prev_rev):
+ entry.to_selected = 'yes'
+ else:
+ entry.to_selected = None
+
+ entries.append(entry)
+ prev_rev = rev
+ prev_path = entry.filename
+ show_revs.reverse()
+ entries.reverse()
+
+ data.update({
+ 'back_url' : request.get_url(view_func=view_directory, pathtype=vclib.DIR,
+ where=up_where, params={}),
+ 'filename' : filename,
+ 'view_tag' : None,
+ 'entries' : entries,
+ 'tags' : [ ],
+ 'branch_names' : [ ],
+ })
+
+ if len(show_revs):
+ data['tr1'] = show_revs[-1]
+ data['tr2'] = show_revs[0]
+ else:
+ data['tr1'] = None
+ data['tr2'] = None
+
+ if cfg.options.use_pagesize:
+ data['log_pagestart'] = int(query_dict.get('log_pagestart',0))
+ data['entries'] = paging(data, 'entries', data['log_pagestart'], 'rev')
+
+ request.server.header()
+ generate_page(request, cfg.templates.log, data)
+
+def view_log_cvs(request, data, logsort):
+ full_name = request.full_name
+ where = request.where
+ query_dict = request.query_dict
+
+ view_tag = query_dict.get('only_with_tag')
+
+ show_revs, rev_map, rev_order, taginfo, rev2tag, \
+ cur_branch, branch_points, branch_names = \
+ read_log(full_name, None, view_tag, logsort)
+
+ up_where = get_up_path(request, where, int(query_dict.get('hideattic',
+ cfg.options.hide_attic)))
+
+ filename = os.path.basename(where)
+
+ data.update({
+ 'back_url' : request.get_url(view_func=view_directory, pathtype=vclib.DIR,
+ where=up_where, params={}),
+ 'filename' : filename,
+ 'view_tag' : view_tag,
+ 'entries' : show_revs, ### rename the show_rev local to entries?
+
+ })
+
+ if cfg.options.use_cvsgraph:
+ data['graph_href'] = request.get_url(view_func=view_cvsgraph, params={})
+
+ if cur_branch:
+ data['branch'] = cur_branch
+
+ ### I don't like this URL construction stuff. the value
+ ### for head_abs_href vs head_href is a bit bogus: why decide to
+ ### include/exclude the mime type from the URL? should just always be
+ ### the same, right?
+ if request.default_viewable:
+ data['head_href'] = request.get_url(view_func=view_markup, params={})
+ data['head_abs_href'] = request.get_url(view_func=view_checkout,
+ params={})
+ else:
+ data['head_href'] = request.get_url(view_func=view_checkout, params={})
+
+ name_printed = { }
+ for entry in show_revs:
+ # augment the entry with (extended=1) info.
+ augment_entry(entry, request, rev_map, rev2tag, branch_points,
+ rev_order, 1, name_printed)
+
+ tagitems = taginfo.items()
+ tagitems.sort()
+ tagitems.reverse()
+
+ data['tags'] = tags = [ ]
+ for tag, rev in tagitems:
+ if rev.co_rev:
+ tags.append(_item(rev=rev.co_rev, name=tag))
+
+ if query_dict.has_key('r1'):
+ diff_rev = query_dict['r1']
+ else:
+ diff_rev = show_revs[-1].rev
+ data['tr1'] = diff_rev
+
+ if query_dict.has_key('r2'):
+ diff_rev = query_dict['r2']
+ else:
+ diff_rev = show_revs[0].rev
+ data['tr2'] = diff_rev
+
+ branch_names.sort()
+ branch_names.reverse()
+ data['branch_names'] = branch_names
+
+ if branch_names:
+ url, params = request.get_link(params={'only_with_tag': None})
+ data['branch_select_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['branch_select_hidden_values'] = prepare_hidden_values(params)
+
+ if cfg.options.use_pagesize:
+ data['log_pagestart'] = int(query_dict.get('log_pagestart',0))
+ data['entries'] = paging(data, 'entries', data['log_pagestart'], 'rev')
+
+ request.server.header()
+ generate_page(request, cfg.templates.log, data)
+
+def view_checkout(request):
+ rev = request.query_dict.get('rev')
+ fp = request.repos.openfile(request.path_parts, rev)[0]
+ mime_type = request.query_dict.get('content-type', request.mime_type)
+ request.server.header(mime_type)
+ copy_stream(fp)
+
+def view_annotate(request):
+ if not cfg.options.allow_annotate:
+ raise "annotate no allows"
+
+ rev = request.query_dict.get('annotate')
+ data = nav_header_data(request, rev)
+
+ request.server.header()
+ generate_page(request, cfg.templates.annotate, data)
+
+ ### be nice to hook this into the template...
+ import blame
+ blame.make_html(request.repos.rootpath, request.where + ',v', rev,
+ compat.urlencode(request.get_options()))
+
+ html_footer(request)
+
+
+def view_cvsgraph_image(request):
+ "output the image rendered by cvsgraph"
+ # this function is derived from cgi/cvsgraphmkimg.cgi
+
+ if not cfg.options.use_cvsgraph:
+ raise "cvsgraph no allows"
+
+ request.server.header('image/png')
+ fp = popen.popen(os.path.normpath(os.path.join(cfg.options.cvsgraph_path,'cvsgraph')),
+ ("-c", cfg.options.cvsgraph_conf,
+ "-r", request.repos.rootpath,
+ request.where + ',v'), 'rb', 0)
+ copy_stream(fp)
+ fp.close()
+
+def view_cvsgraph(request):
+ "output a page containing an image rendered by cvsgraph"
+ # this function is derived from cgi/cvsgraphwrapper.cgi
+
+ if not cfg.options.use_cvsgraph:
+ raise "cvsgraph no allows"
+
+ where = request.where
+
+ pathname, filename = os.path.split(where)
+ if pathname[-6:] == '/Attic':
+ pathname = pathname[:-6]
+
+ data = nav_header_data(request, None)
+
+ # Required only if cvsgraph needs to find it's supporting libraries.
+ # Uncomment and set accordingly if required.
+ #os.environ['LD_LIBRARY_PATH'] = '/usr/lib:/usr/local/lib'
+
+ query = compat.urlencode(request.get_options({}))
+ amp_query = query and '&' + query
+ qmark_query = query and '?' + query
+
+ imagesrc = request.get_url(view_func=view_cvsgraph_image)
+
+ # Create an image map
+ fp = popen.popen(os.path.join(cfg.options.cvsgraph_path, 'cvsgraph'),
+ ("-i",
+ "-c", cfg.options.cvsgraph_conf,
+ "-r", request.repos.rootpath,
+ "-6", amp_query,
+ "-7", qmark_query,
+ request.where + ',v'), 'rb', 0)
+
+ data.update({
+ 'imagemap' : fp,
+ 'imagesrc' : imagesrc,
+ })
+
+ request.server.header()
+ generate_page(request, cfg.templates.graph, data)
+
+def search_files(repos, path_parts, files, search_re):
+ """ Search files in a directory for a regular expression.
+
+ Does a check-out of each file in the directory. Only checks for
+ the first match.
+ """
+
+ # Pass in search regular expression. We check out
+ # each file and look for the regular expression. We then return the data
+ # for all files that match the regex.
+
+ # Compile to make sure we do this as fast as possible.
+ searchstr = re.compile(search_re)
+
+ # Will become list of files that have at least one match.
+ # new_file_list also includes directories.
+ new_file_list = [ ]
+
+ # Loop on every file (and directory)
+ for file in files:
+ # Is this a directory? If so, append name to new_file_list
+ # and move to next file.
+ if file.kind != vclib.FILE:
+ new_file_list.append(file)
+ continue
+
+ # Only files at this point
+
+ # figure out where we are and its mime type
+ mime_type, encoding = mimetypes.guess_type(file.name)
+ if not mime_type:
+ mime_type = 'text/plain'
+
+ # Shouldn't search binary files, or should we?
+ # Should allow all text mime types to pass.
+ if mime_type[:4] != 'text':
+ continue
+
+ # Only text files at this point
+
+ # process_checkout will checkout the head version out of the repository
+ # Assign contents of checked out file to fp.
+ fp = repos.openfile(path_parts + [file.name])[0]
+
+ # Read in each line, use re.search to search line.
+ # If successful, add file to new_file_list and break.
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+ if searchstr.search(line):
+ new_file_list.append(file)
+ # close down the pipe (and wait for the child to terminate)
+ fp.close()
+ break
+
+ return new_file_list
+
+
+def view_doc(request):
+ """Serve ViewCVS help pages locally.
+
+ Using this avoids the need for modifying the setup of the web server.
+ """
+ help_page = request.where
+ if CONF_PATHNAME:
+ doc_directory = os.path.join(g_install_dir, "doc")
+ else:
+ # aid testing from CVS working copy:
+ doc_directory = os.path.join(g_install_dir, "website")
+ try:
+ fp = open(os.path.join(doc_directory, help_page), "rb")
+ except IOError, v:
+ raise debug.ViewcvsException('help file "%s" not available\n(%s)'
+ % (help_page, str(v)), '404 Not Found')
+ if help_page[-3:] == 'png':
+ request.server.header('image/png')
+ elif help_page[-3:] == 'jpg':
+ request.server.header('image/jpeg')
+ elif help_page[-3:] == 'gif':
+ request.server.header('image/gif')
+ else: # assume HTML:
+ request.server.header()
+ copy_stream(fp)
+ fp.close()
+
+def rcsdiff_date_reformat(date_str):
+ try:
+ date = compat.cvs_strptime(date_str)
+ except ValueError:
+ return date_str
+ return make_time_string(compat.timegm(date))
+
+_re_extract_rev = re.compile(r'^[-+]+ [^\t]+\t([^\t]+)\t((\d+\.)*\d+)$')
+_re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)')
+def human_readable_diff(request, fp, rev1, rev2, sym1, sym2):
+ # do this now, in case we need to print an error
+ request.server.header()
+
+ query_dict = request.query_dict
+
+ where = request.where
+
+ data = nav_header_data(request, rev2)
+
+ log_rev1 = log_rev2 = None
+ date1 = date2 = ''
+ rcsdiff_eflag = 0
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+
+ # Use regex matching to extract the data and to ensure that we are
+ # extracting it from a properly formatted line. There are rcsdiff
+ # programs out there that don't supply the correct format; we'll be
+ # flexible in case we run into one of those.
+ if line[:4] == '--- ':
+ match = _re_extract_rev.match(line)
+ if match:
+ date1 = match.group(1)
+ log_rev1 = match.group(2)
+ elif line[:4] == '+++ ':
+ match = _re_extract_rev.match(line)
+ if match:
+ date2 = match.group(1)
+ log_rev2 = match.group(2)
+ break
+
+ # Didn't want to put this here, but had to. The DiffSource class
+ # picks up fp after this loop has processed the header. Previously
+ # error messages and the 'Binary rev ? and ? differ' where thrown out
+ # and DiffSource then showed no differences.
+ # Need to process the entire header before DiffSource is used.
+ if line[:3] == 'Bin':
+ rcsdiff_eflag = _RCSDIFF_IS_BINARY
+ break
+
+ if (string.find(line, 'not found') != -1 or
+ string.find(line, 'illegal option') != -1):
+ rcsdiff_eflag = _RCSDIFF_ERROR
+ break
+
+ if (log_rev1 and log_rev1 != rev1) or (log_rev2 and log_rev2 != rev2):
+ ### it would be nice to have an error.ezt for things like this
+ print '<strong>ERROR:</strong> rcsdiff did not return the correct'
+ print 'version number in its output.'
+ print '(got "%s" / "%s", expected "%s" / "%s")' % \
+ (log_rev1, log_rev2, rev1, rev2)
+ print '<p>Aborting operation.'
+ sys.exit(0)
+
+ # Process any special lines in the header, or continue to
+ # get the differences from DiffSource.
+ if rcsdiff_eflag == _RCSDIFF_IS_BINARY:
+ rcs_diff = [ (_item(type='binary-diff')) ]
+ elif rcsdiff_eflag == _RCSDIFF_ERROR:
+ rcs_diff = [ (_item(type='error')) ]
+ else:
+ rcs_diff = DiffSource(fp)
+
+ data.update({
+ 'where' : where,
+ 'rev1' : rev1,
+ 'rev2' : rev2,
+ 'tag1' : sym1,
+ 'tag2' : sym2,
+ 'date1' : ', ' + rcsdiff_date_reformat(date1),
+ 'date2' : ', ' + rcsdiff_date_reformat(date2),
+ 'changes' : rcs_diff,
+ 'diff_format' : query_dict.get('diff_format', cfg.options.diff_format),
+ })
+
+ params = request.query_dict.copy()
+ params['diff_format'] = None
+
+ url, params = request.get_link(params=params)
+ data['diff_format_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['diff_format_hidden_values'] = prepare_hidden_values(params)
+
+ generate_page(request, cfg.templates.diff, data)
+
+def spaced_html_text(text):
+ text = string.expandtabs(string.rstrip(text))
+
+ # in the code below, "\x01" will be our stand-in for "&". We don't want
+ # to insert "&" because it would get escaped by htmlify(). Similarly,
+ # we use "\x02" as a stand-in for "<br>"
+
+ if cfg.options.hr_breakable > 1 and len(text) > cfg.options.hr_breakable:
+ text = re.sub('(' + ('.' * cfg.options.hr_breakable) + ')',
+ '\\1\x02',
+ text)
+ if cfg.options.hr_breakable:
+ # make every other space "breakable"
+ text = string.replace(text, ' ', ' \x01nbsp;')
+ else:
+ text = string.replace(text, ' ', '\x01nbsp;')
+ text = htmlify(text)
+ text = string.replace(text, '\x01', '&')
+ text = string.replace(text, '\x02', '<font color=red>\</font><br>')
+ return text
+
+class DiffSource:
+ def __init__(self, fp):
+ self.fp = fp
+ self.save_line = None
+
+ # keep track of where we are during an iteration
+ self.idx = -1
+ self.last = None
+
+ # these will be set once we start reading
+ self.left = None
+ self.right = None
+ self.state = 'no-changes'
+ self.left_col = [ ]
+ self.right_col = [ ]
+
+ def __getitem__(self, idx):
+ if idx == self.idx:
+ return self.last
+ if idx != self.idx + 1:
+ raise DiffSequencingError()
+
+ # keep calling _get_row until it gives us something. sometimes, it
+ # doesn't return a row immediately because it is accumulating changes
+ # when it is out of data, _get_row will raise IndexError
+ while 1:
+ item = self._get_row()
+ if item:
+ self.idx = idx
+ self.last = item
+ return item
+
+ def _get_row(self):
+ if self.state[:5] == 'flush':
+ item = self._flush_row()
+ if item:
+ return item
+ self.state = 'dump'
+
+ if self.save_line:
+ line = self.save_line
+ self.save_line = None
+ else:
+ line = self.fp.readline()
+
+ if not line:
+ if self.state == 'no-changes':
+ self.state = 'done'
+ return _item(type='no-changes')
+
+ # see if there are lines to flush
+ if self.left_col or self.right_col:
+ # move into the flushing state
+ self.state = 'flush-' + self.state
+ return None
+
+ # nothing more to return
+ raise IndexError
+
+ if line[:2] == '@@':
+ self.state = 'dump'
+ self.left_col = [ ]
+ self.right_col = [ ]
+
+ match = _re_extract_info.match(line)
+ return _item(type='header', line1=match.group(1), line2=match.group(2),
+ extra=match.group(3))
+
+ if line[0] == '\\':
+ # \ No newline at end of file
+
+ # move into the flushing state. note: it doesn't matter if we really
+ # have data to flush or not; that will be figured out later
+ self.state = 'flush-' + self.state
+ return None
+
+ diff_code = line[0]
+ output = spaced_html_text(line[1:])
+
+ if diff_code == '+':
+ if self.state == 'dump':
+ return _item(type='add', right=output)
+
+ self.state = 'pre-change-add'
+ self.right_col.append(output)
+ return None
+
+ if diff_code == '-':
+ self.state = 'pre-change-remove'
+ self.left_col.append(output)
+ return None
+
+ if self.left_col or self.right_col:
+ # save the line for processing again later
+ self.save_line = line
+
+ # move into the flushing state
+ self.state = 'flush-' + self.state
+ return None
+
+ return _item(type='context', left=output, right=output)
+
+ def _flush_row(self):
+ if not self.left_col and not self.right_col:
+ # nothing more to flush
+ return None
+
+ if self.state == 'flush-pre-change-remove':
+ return _item(type='remove', left=self.left_col.pop(0))
+
+ # state == flush-pre-change-add
+ item = _item(type='change', have_left=None, have_right=None)
+ if self.left_col:
+ item.have_left = 'yes'
+ item.left = self.left_col.pop(0)
+ if self.right_col:
+ item.have_right = 'yes'
+ item.right = self.right_col.pop(0)
+ return item
+
+class DiffSequencingError(Exception):
+ pass
+
+def view_diff(request):
+ query_dict = request.query_dict
+
+ rev1 = r1 = query_dict['r1']
+ rev2 = r2 = query_dict['r2']
+ p1 = query_dict.get('p1', request.where)
+ p2 = query_dict.get('p2', request.where)
+ sym1 = sym2 = None
+
+ if r1 == 'text':
+ rev1 = query_dict.get('tr1', None)
+ if not rev1:
+ raise debug.ViewcvsException('Missing revision from the diff '
+ 'form text field', '400 Bad Request')
+ else:
+ idx = string.find(r1, ':')
+ if idx == -1:
+ rev1 = r1
+ else:
+ rev1 = r1[:idx]
+ sym1 = r1[idx+1:]
+
+ if r2 == 'text':
+ rev2 = query_dict.get('tr2', None)
+ if not rev2:
+ raise debug.ViewcvsException('Missing revision from the diff '
+ 'form text field', '400 Bad Request')
+ sym2 = ''
+ else:
+ idx = string.find(r2, ':')
+ if idx == -1:
+ rev2 = r2
+ else:
+ rev2 = r2[:idx]
+ sym2 = r2[idx+1:]
+
+ try:
+ if revcmp(rev1, rev2) > 0:
+ rev1, rev2 = rev2, rev1
+ sym1, sym2 = sym2, sym1
+ p1, p2 = p2, p1
+ except ValueError:
+ raise debug.ViewcvsException('Invalid revision(s) passed to diff',
+ '400 Bad Request')
+
+ human_readable = 0
+ unified = 0
+ args = [ ]
+
+ ### Note: these options only really work out where rcsdiff (used by
+ ### CVS) and regular diff (used by SVN) overlap. If for some reason
+ ### our use of the options for these starts to deviate too much,
+ ### this code may a re-org to just do different things for different
+ ### VC types.
+
+ format = query_dict.get('diff_format', cfg.options.diff_format)
+ if format == 'c':
+ args.append('-c')
+ elif format == 's':
+ args.append('--side-by-side')
+ args.append('--width=164')
+ elif format == 'l':
+ args.append('--unified=15')
+ human_readable = 1
+ unified = 1
+ elif format == 'h':
+ args.append('-u')
+ human_readable = 1
+ unified = 1
+ elif format == 'u':
+ args.append('-u')
+ unified = 1
+ else:
+ raise debug.ViewcvsException('Diff format %s not understood'
+ % format, '400 Bad Request')
+
+ if human_readable:
+ if cfg.options.hr_funout:
+ args.append('-p')
+ if cfg.options.hr_ignore_white:
+ args.append('-w')
+ if cfg.options.hr_ignore_keyword_subst and request.roottype == 'cvs':
+ # -kk isn't a regular diff option. it exists only for rcsdiff
+ # (as in "-ksubst") ,so 'svn' roottypes can't use it.
+ args.append('-kk')
+
+ file1 = None
+ file2 = None
+ if request.roottype == 'cvs':
+ args[len(args):] = ['-r' + rev1, '-r' + rev2, request.full_name]
+ fp = bincvs.rcs_popen(cfg.general, 'rcsdiff', args, 'rt')
+ else:
+ try:
+ date1 = vclib.svn.date_from_rev(request.repos, int(rev1))
+ date2 = vclib.svn.date_from_rev(request.repos, int(rev2))
+ except vclib.InvalidRevision:
+ raise debug.ViewcvsException('Invalid revision(s) passed to diff',
+ '400 Bad Request')
+
+ date1 = time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date1))
+ date2 = time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date2))
+ args.append("-L")
+ args.append(p1 + "\t" + date1 + "\t" + rev1)
+ args.append("-L")
+ args.append(p2 + "\t" + date2 + "\t" + rev2)
+
+ # Need to keep a reference to the FileDiff object around long
+ # enough to use. It destroys its underlying temporary files when
+ # the class is destroyed.
+ diffobj = vclib.svn.do_diff(request.repos, p1, int(rev1),
+ p2, int(rev2), args)
+
+ fp = diffobj.get_pipe()
+
+ if human_readable:
+ human_readable_diff(request, fp, rev1, rev2, sym1, sym2)
+ return
+
+ request.server.header('text/plain')
+
+ rootpath = request.repos.rootpath
+ if unified:
+ f1 = '--- ' + rootpath
+ f2 = '+++ ' + rootpath
+ else:
+ f1 = '*** ' + rootpath
+ f2 = '--- ' + rootpath
+
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+
+ if line[:len(f1)] == f1:
+ line = string.replace(line, rootpath + '/', '')
+ if sym1:
+ line = line[:-1] + ' %s\n' % sym1
+ elif line[:len(f2)] == f2:
+ line = string.replace(line, rootpath + '/', '')
+ if sym2:
+ line = line[:-1] + ' %s\n' % sym2
+
+ print line[:-1]
+
+
+def generate_tarball_header(out, name, size=0, mode=None, mtime=0, uid=0, gid=0, typefrag=None, linkname='', uname='viewcvs', gname='viewcvs', devmajor=1, devminor=0, prefix=None, magic='ustar', version='', chksum=None):
+ if not mode:
+ if name[-1:] == '/':
+ mode = 0755
+ else:
+ mode = 0644
+
+ if not typefrag:
+ if name[-1:] == '/':
+ typefrag = '5' # directory
+ else:
+ typefrag = '0' # regular file
+
+ if not prefix:
+ prefix = ''
+
+ block1 = struct.pack('100s 8s 8s 8s 12s 12s',
+ name,
+ '%07o' % mode,
+ '%07o' % uid,
+ '%07o' % gid,
+ '%011o' % size,
+ '%011o' % mtime)
+
+ block2 = struct.pack('c 100s 6s 2s 32s 32s 8s 8s 155s',
+ typefrag,
+ linkname,
+ magic,
+ version,
+ uname,
+ gname,
+ '%07o' % devmajor,
+ '%07o' % devminor,
+ prefix)
+
+ if not chksum:
+ dummy_chksum = ' '
+ block = block1 + dummy_chksum + block2
+ chksum = 0
+ for i in range(len(block)):
+ chksum = chksum + ord(block[i])
+
+ block = block1 + struct.pack('8s', '%07o' % chksum) + block2
+ block = block + '\0' * (512 - len(block))
+
+ out.write(block)
+
+def generate_tarball_cvs(out, request, tar_top, rep_top, reldir, tag, stack=[]):
+ if (rep_top == '' and 0 < len(reldir) and
+ ((reldir[0] == 'CVSROOT' and cfg.options.hide_cvsroot)
+ or cfg.is_forbidden(reldir[0]))):
+ return
+
+ rep_path = rep_top + reldir
+ tar_dir = string.join(tar_top + reldir, '/') + '/'
+
+ entries = request.repos.listdir(rep_path, tag)
+
+ subdirs = [ ]
+ for file in entries:
+ if not file.verboten and file.kind == vclib.DIR:
+ subdirs.append(file.name)
+
+ stack.append(tar_dir)
+
+ bincvs.get_logs(request.repos, rep_path, entries, tag)
+
+ entries.sort(lambda a, b: cmp(a.name, b.name))
+
+ for file in entries:
+ if file.rev is None or file.state == 'dead':
+ continue
+
+ for dir in stack:
+ generate_tarball_header(out, dir)
+ del stack[0:]
+
+ info = os.stat(file.path)
+ mode = (info[stat.ST_MODE] & 0555) | 0200
+
+ rev_flag = '-p' + file.rev
+ fp = bincvs.rcs_popen(cfg.general, 'co', (rev_flag, file.path), 'rb', 0)
+ contents = fp.read()
+ status = fp.close()
+
+ generate_tarball_header(out, tar_dir + file.name,
+ len(contents), mode, file.date)
+ out.write(contents)
+ out.write('\0' * (511 - ((len(contents) + 511) % 512)))
+
+ subdirs.sort()
+ for subdir in subdirs:
+ if subdir != 'Attic':
+ generate_tarball_cvs(out, request, tar_top, rep_top,
+ reldir + [subdir], tag, stack)
+
+ if len(stack):
+ del stack[-1:]
+
+def generate_tarball_svn(out, request, tar_top, rep_top, reldir, tag, stack=[]):
+ rep_dir = string.join(rep_top + reldir, '/')
+ tar_dir = string.join(tar_top + reldir, '/') + '/'
+
+ entries = request.repos.listdir(rep_top + reldir)
+
+ subdirs = []
+ for entry in entries:
+ if entry.kind == vclib.DIR:
+ subdirs.append(entry.name)
+
+ vclib.svn.get_logs(request.repos, rep_dir, entries)
+
+ stack.append(tar_dir)
+
+ for file in entries:
+ if file.kind != vclib.FILE:
+ continue
+
+ for dir in stack:
+ generate_tarball_header(out, dir)
+ del stack[0:]
+
+ mode = 0644
+
+ fp = request.repos.openfile(rep_top + reldir + [file.name])[0]
+
+ contents = ""
+ while 1:
+ chunk = fp.read(CHUNK_SIZE)
+ if not chunk:
+ break
+ contents = contents + chunk
+
+ status = fp.close()
+
+ generate_tarball_header(out, tar_dir + file.name,
+ len(contents), mode, file.date)
+ out.write(contents)
+ out.write('\0' * (511 - ((len(contents) + 511) % 512)))
+
+ for subdir in subdirs:
+ generate_tarball_svn(out, request, tar_top, rep_top,
+ reldir + [subdir], tag, stack)
+
+ if len(stack):
+ del stack[-1:]
+
+def download_tarball(request):
+ if not cfg.options.allow_tar:
+ raise "tarball no allows"
+
+ query_dict = request.query_dict
+ rep_top = tar_top = request.path_parts
+ tag = query_dict.get('only_with_tag')
+
+ ### look for GZIP binary
+
+ request.server.header('application/octet-stream')
+ sys.stdout.flush()
+ fp = popen.pipe_cmds([('gzip', '-c', '-n')])
+
+ # Switch based on the repository root type.
+ if request.roottype == 'cvs':
+ generate_tarball_cvs(fp, request, tar_top, rep_top, [], tag)
+ elif request.roottype == 'svn':
+ generate_tarball_svn(fp, request, tar_top, rep_top, [], tag)
+
+ fp.write('\0' * 1024)
+ fp.close()
+
+def view_revision(request):
+ data = common_template_data(request)
+ data.update({
+ 'roottype' : request.roottype,
+ })
+
+ if request.roottype == "cvs":
+ raise ViewcvsException("Revision view not supported for CVS repositories "
+ "at this time.", "400 Bad Request")
+ else:
+ view_revision_svn(request, data)
+
+def view_revision_svn(request, data):
+ query_dict = request.query_dict
+ date, author, msg, changes = vclib.svn.get_revision_info(request.repos)
+ date_str = make_time_string(date)
+ rev = request.repos.rev
+
+ # add the hrefs, types, and prev info
+ for change in changes:
+ change.view_href = change.diff_href = change.type = None
+ change.prev_path = change.prev_rev = None
+ if change.pathtype is vclib.FILE:
+ change.type = 'file'
+ change.view_href = request.get_url(view_func=view_markup,
+ where=change.filename,
+ pathtype=change.pathtype,
+ params={'rev' : str(rev)})
+ if change.action == "copied" or change.action == "modified":
+ change.prev_path = change.base_path
+ change.prev_rev = change.base_rev
+ change.diff_href = request.get_url(view_func=view_diff,
+ where=change.filename,
+ pathtype=change.pathtype,
+ params={})
+ elif change.pathtype is vclib.DIR:
+ change.type = 'dir'
+ change.view_href = request.get_url(view_func=view_directory,
+ where=change.filename,
+ pathtype=change.pathtype,
+ params={'rev' : str(rev)})
+
+ prev_rev_href = next_rev_href = None
+ if rev > 0:
+ prev_rev_href = request.get_url(view_func=view_revision,
+ where=None,
+ pathtype=None,
+ params={'rev': str(rev - 1)})
+ if rev < request.repos.youngest:
+ next_rev_href = request.get_url(view_func=view_revision,
+ where=None,
+ pathtype=None,
+ params={'rev': str(rev + 1)})
+ if msg:
+ msg = htmlify(msg)
+ data.update({
+ 'rev' : str(rev),
+ 'author' : author,
+ 'date_str' : date_str,
+ 'log' : msg,
+ 'ago' : html_time(request, date, 1),
+ 'changes' : changes,
+ 'jump_rev' : str(rev),
+ 'prev_href' : prev_rev_href,
+ 'next_href' : next_rev_href,
+ })
+
+ url, params = request.get_link(view_func=view_revision,
+ where=None,
+ pathtype=None,
+ params={'rev': None})
+ data['jump_rev_action'] = urllib.quote(url, _URL_SAFE_CHARS)
+ data['jump_rev_hidden_values'] = prepare_hidden_values(params)
+
+ request.server.header()
+ generate_page(request, cfg.templates.revision, data)
+
+
+_views = {
+ 'annotate': view_annotate,
+ 'auto': view_auto,
+ 'co': view_checkout,
+ 'diff': view_diff,
+ 'dir': view_directory,
+ 'graph': view_cvsgraph,
+ 'graphimg': view_cvsgraph_image,
+ 'log': view_log,
+ 'markup': view_markup,
+ 'rev': view_revision,
+ 'tar': download_tarball,
+}
+
+_view_codes = {}
+for code, view in _views.items():
+ _view_codes[view] = code
+
+def list_roots(cfg):
+ allroots = { }
+ allroots.update(cfg.general.cvs_roots)
+ allroots.update(cfg.general.svn_roots)
+ return allroots
+
+def handle_config():
+ debug.t_start('load-config')
+ global cfg
+ if cfg is None:
+ cfg = config.Config()
+ cfg.set_defaults()
+
+ # load in configuration information from the config file
+ pathname = CONF_PATHNAME or os.path.join(g_install_dir, 'viewcvs.conf')
+ if sapi.server:
+ cfg.load_config(pathname, sapi.server.getenv('HTTP_HOST'))
+ else:
+ cfg.load_config(pathname, None)
+
+ # special handling for svn_parent_path. any subdirectories
+ # present in the directory specified as the svn_parent_path that
+ # have a child file named "format" will be treated as svn_roots.
+ if cfg.general.svn_parent_path is not None:
+ pp = cfg.general.svn_parent_path
+ try:
+ subpaths = os.listdir(pp)
+ except OSError:
+ raise debug.ViewcvsException(
+ "The setting for 'svn_parent_path' does not refer to "
+ "a valid directory.")
+
+ for subpath in subpaths:
+ if os.path.exists(os.path.join(pp, subpath)) \
+ and os.path.exists(os.path.join(pp, subpath, "format")):
+ cfg.general.svn_roots[subpath] = os.path.join(pp, subpath)
+
+ debug.t_end('load-config')
+
+
+def view_error(server):
+ exc_dict = debug.GetExceptionData()
+ status = exc_dict['status']
+ handled = 0
+
+ # use the configured error template if possible
+ try:
+ if cfg:
+ server.header(status=status)
+ generate_page(None, cfg.templates.error, exc_dict)
+ handled = 1
+ except:
+ # get new exception data, more important than the first
+ exc_dict = debug.GetExceptionData()
+
+ # but fallback to the old exception printer if no configuration is
+ # available, or if something went wrong
+ if not handled:
+ debug.PrintException(server, exc_dict)
+
+def main(server):
+ try:
+ debug.t_start('main')
+ try:
+ # handle the configuration stuff
+ handle_config()
+
+ # build a Request object, which contains info about the HTTP request
+ request = Request(server)
+ request.run_viewcvs()
+ except SystemExit, e:
+ return
+ except:
+ view_error(server)
+
+ finally:
+ debug.t_end('main')
+ debug.dump()
+ debug.DumpChildren(server)
+
+
+class _item:
+ def __init__(self, **kw):
+ vars(self).update(kw)
+