From 983f9c737929abadf940990b412e6f2e2ece6fd3 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Thu, 18 Jul 2013 20:02:34 +0400 Subject: [PATCH] Post-merge debug. Also return some lost files back, document 4intra.net modifications in CHANGES and README files, remove 'union' authorizer and rewritehtml as the similar functionality is already provided by the core. Also fix diffs for non-bash (sh/ash/dash) shells. --- CHANGES | 25 ++ INSTALL | 2 +- README | 2 + lib/config.py | 2 + lib/cvsdb.py | 24 +- lib/sapi.py | 2 + lib/vcauth/__init__.py | 2 +- lib/vcauth/cvsntacl/__init__.py | 2 +- lib/vcauth/union/__init__.py | 67 ---- lib/vclib/__init__.py | 2 +- lib/viewvc.py | 69 ++-- notes/TODO | 53 +++ notes/authz-dev-TODO | 82 +++++ notes/logo/viewvc-logo.odg | Bin 0 -> 12537 bytes notes/logo/viewvc-logo.pdf | Bin 0 -> 3524 bytes notes/logo/viewvc-logo.svg | 2 + notes/vclib-enhancements.txt | 78 ++++ templates/classic/include/header.ezt | 2 +- templates/classic/rss.ezt | 38 +- templates/default/include/header.ezt | 2 +- templates/default/query_form.ezt | 106 ++++-- templates/default/query_results.ezt | 13 +- tests/testparse.py | 51 +++ tests/vclib/co.py | 53 +++ tests/vclib/rlog.py | 5 + viewvc-install | 509 +-------------------------- 26 files changed, 560 insertions(+), 633 deletions(-) delete mode 100644 lib/vcauth/union/__init__.py create mode 100644 notes/TODO create mode 100644 notes/authz-dev-TODO create mode 100644 notes/logo/viewvc-logo.odg create mode 100644 notes/logo/viewvc-logo.pdf create mode 100644 notes/logo/viewvc-logo.svg create mode 100644 notes/vclib-enhancements.txt create mode 100644 tests/testparse.py create mode 100755 tests/vclib/co.py create mode 100644 tests/vclib/rlog.py diff --git a/CHANGES b/CHANGES index d59618c8..7f8c2c6e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,28 @@ +4Intra.net/CUSTIS improvements + + * Support for full-text search over file contents, including binary + documents like *.doc and so on using Sphinx Search and Apache Tika + server. Patched Tika with fixes for #TIKA709 and #TIKA964 is highly + recommended: + http://wiki.4intra.net/public/tika-app-1.2-fix-TIKA709-TIKA964.jar + (SHA1 efef722a5e2322f7c2616d096552a48134dc5faa) + * Access right checks in query results. + * Access right checks for repository root directories. + * New query parameters: repository, repo type, revision number. + * Authorizer for CVSnt ACLs. + * InnoDB, additional database indexes and some search query + optimisations. + * Support for specifying path to MySQL UNIX socket. + * Asynchronous hook examples for updating SVN and CVS repos. + * Slightly more correct charset guessing, especially for Russian. + * Support for diffing added/removed files. + * File lists in RSS feeds for 'classic' template. + * Path configuration via a single 'viewvcinstallpath.py' file, + not via editing multiple bin/* files. + * Link to repository list instead of viewvc.org from the logo + * "rcsfile service" support used to workaround command execution + problems (forks) from Apache mod_python. + Version 1.2.0 (released ??-???-????) * bumped minimum support Python version to 2.4 diff --git a/INSTALL b/INSTALL index d193c8d9..72efcd00 100644 --- a/INSTALL +++ b/INSTALL @@ -140,7 +140,7 @@ installation instructions. default_root root_as_url_component rcs_dir - mime_types_file + mime_types_files There are some other options that are usually nice to change. See viewvc.conf for more information. ViewVC provides a working, diff --git a/README b/README index a506bf1f..b434d779 100644 --- a/README +++ b/README @@ -1,5 +1,7 @@ ViewVC -- Viewing the content of CVS/SVN repositories with a Webbrowser. +This is the 4Intra.net patched version with some extra features. + Please read the file INSTALL for more information. And see windows/README for more information on running ViewVC on diff --git a/lib/config.py b/lib/config.py index e9a93acf..295c71cb 100644 --- a/lib/config.py +++ b/lib/config.py @@ -363,6 +363,7 @@ class Config: if section == root_authz_section: for key, value in self._get_parser_items(self.parser, section): params[key] = value + params['__config'] = self return authorizer, params def get_authorizer_params(self, authorizer=None): @@ -377,6 +378,7 @@ class Config: sub_config = getattr(self, authz_section) for attr in dir(sub_config): params[attr] = getattr(sub_config, attr) + params['__config'] = self return params def guesser(self): diff --git a/lib/cvsdb.py b/lib/cvsdb.py index e350115c..dbc0c749 100644 --- a/lib/cvsdb.py +++ b/lib/cvsdb.py @@ -14,6 +14,7 @@ import sys import time import re import cgi +import string import vclib import dbi @@ -530,7 +531,7 @@ class CheckinDatabase: ' AND dirid=dirs.id AND fileid=files.id' % (commits_table, commits_table, commits_table, ','.join(ids)) ) - def CreateSQLQueryString(self, query): + def CreateSQLQueryString(self, query, detect_leftover=0): commits_table = self.GetCommitsTable() fields = [ commits_table+".*", @@ -692,7 +693,7 @@ class CheckinDatabase: revision = rows[docid]['revision'] fp = None try: - fp, _ = self.request.get_repo(repo).repos.openfile(path, revision) + fp, _ = self.request.get_repo(repo).repos.openfile(path, revision, {}) content = fp.read() fp.close() content = self.guesser.utf8(content) @@ -757,13 +758,14 @@ class CheckinDatabase: rows = self.RunSphinxQuery(query) else: # Use regular queries when document content is not searched - rows = self.selectall(self.db, self.CreateSQLQueryString(query)) + rows = self.selectall(self.db, self.CreateSQLQueryString(query, 1)) # Check rights rows = (r for r in rows if self.check_commit_access( r['repository_name'], r['dir_name'], r['file_name'], r['revision'])) + query.SetExecuted() # Convert rows to commit objects for row in rows: @@ -1211,15 +1213,13 @@ def CreateCommit(): def CreateCheckinQuery(): return CheckinDatabaseQuery() -def ConnectDatabase(cfg, readonly=0): - if readonly: - user = cfg.cvsdb.readonly_user - passwd = cfg.cvsdb.readonly_passwd - else: - user = cfg.cvsdb.user - passwd = cfg.cvsdb.passwd - db = CheckinDatabase(cfg.cvsdb.host, cfg.cvsdb.port, user, passwd, - cfg.cvsdb.database_name) +def ConnectDatabase(cfg, request=None, readonly=0): + db = CheckinDatabase( + readonly = readonly, + request = request, + cfg = cfg.cvsdb, + guesser = cfg.guesser(), + ) db.Connect() return db diff --git a/lib/sapi.py b/lib/sapi.py index 585f3ca0..037cd09b 100644 --- a/lib/sapi.py +++ b/lib/sapi.py @@ -32,6 +32,8 @@ server = None # that character as-is, and sometimes needs to embed escaped values # into HTML attributes. def escape(s): + try: s = s.encode('utf8') + except: pass s = str(s) s = s.replace('&', '&') s = s.replace('>', '>') diff --git a/lib/vcauth/__init__.py b/lib/vcauth/__init__.py index 2f78571f..b80647fb 100644 --- a/lib/vcauth/__init__.py +++ b/lib/vcauth/__init__.py @@ -41,7 +41,7 @@ class GenericViewVCAuthorizer: pass - + ############################################################################## class ViewVCAuthorizer(GenericViewVCAuthorizer): diff --git a/lib/vcauth/cvsntacl/__init__.py b/lib/vcauth/cvsntacl/__init__.py index 42cbf8a9..1a9cf311 100644 --- a/lib/vcauth/cvsntacl/__init__.py +++ b/lib/vcauth/cvsntacl/__init__.py @@ -84,7 +84,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): def check_path_access(self, rootname, path_parts, pathtype, rev=None): if not path_parts: - return 1 + return self.check(rootname, [], '') if pathtype == vclib.DIR: return self.check(rootname, path_parts, '') f = path_parts[-1] diff --git a/lib/vcauth/union/__init__.py b/lib/vcauth/union/__init__.py deleted file mode 100644 index 61b6cae8..00000000 --- a/lib/vcauth/union/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*-python-*- -# -# Copyright (C) 2009 Vitaliy Filippov. -# -# 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 ViewVC -# distribution or at http://viewvc.org/license-1.html. -# -# For more information, visit http://viewvc.org/ -# -# ----------------------------------------------------------------------- - -import vcauth -import vclib -import string -import debug - -class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): - """A 'union' authorizer: it makes possible to use different authorizers - for different roots.""" - - def __init__(self, username, params={}): - self.username = username - self.params = params - self.cfg = params['__config'] - self.default = params.get('default', '') - self.byroot = {} - self.authz = {} - union = params.get('union', '') - for i in union.split(','): - if i.find(':') < 0: - continue - (root, auth) = i.split(':', 2) - self.byroot[root.strip()] = auth.strip() - - def create_authz(self, rootname): - aname = self.byroot.get(rootname, '') or self.default - if not aname: - return None - if self.authz.get(aname, None): - return self.authz[aname] - import imp - fp = None - try: - try: - fp, path, desc = imp.find_module(aname, vcauth.__path__) - my_auth = imp.load_module('viewvc', fp, path, desc) - except ImportError: - raise - finally: - if fp: - fp.close() - params = self.cfg.get_authorizer_params(aname, rootname) - self.authz[aname] = my_auth.ViewVCAuthorizer(self.username, params) - return self.authz[aname] - - def check_root_access(self, rootname): - a = self.create_authz(rootname) - if a: - return a.check_root_access(rootname) - return None - - def check_path_access(self, rootname, path_parts, pathtype, rev=None): - a = self.create_authz(rootname) - if a: - return a.check_path_access(rootname, path_parts, pathtype, rev) - return None diff --git a/lib/vclib/__init__.py b/lib/vclib/__init__.py index bf1151a3..ffad38e9 100644 --- a/lib/vclib/__init__.py +++ b/lib/vclib/__init__.py @@ -392,7 +392,7 @@ class _diff_fp: args.extend(["-L", self._label(info1), "-L", self._label(info2)]) args.extend([temp1, temp2]) args.insert(0, diff_cmd) - os.system("'"+"' '".join(args)+"' &> "+self.temp3) + os.system("'"+"' '".join(args)+"' > '"+self.temp3+"' 2> '"+self.temp3+"'") self.fp = open(self.temp3, 'rb') self.fp.seek(0) diff --git a/lib/viewvc.py b/lib/viewvc.py index 763966b0..78cc3842 100644 --- a/lib/viewvc.py +++ b/lib/viewvc.py @@ -14,7 +14,7 @@ # # ----------------------------------------------------------------------- -__version__ = '1.2-dev-2243' +__version__ = '1.2.svn2905+4intranet-1' # this comes from our library; measure the startup time import debug @@ -39,6 +39,7 @@ import types import urllib import datetime import locale +import string # These modules come from our library (the stub has set up the path) from common import _item, _RCSDIFF_NO_CHANGES, _RCSDIFF_IS_BINARY, _RCSDIFF_ERROR, TemplateData @@ -152,7 +153,7 @@ class Request: roottype, rootpath, rootname = locate_root(self.cfg, rootname) if roottype: # Setup an Authorizer for this rootname and username - authorizer = setup_authorizer(self.cfg, self.username, self.rootname) + authorizer = setup_authorizer(self.cfg, self.username, rootname) # Create the repository object if roottype == 'cvs': @@ -298,7 +299,7 @@ class Request: debug.t_start('select-repos') try: if self.repos.roottype() == 'cvs': - self.rootpath = vclib.ccvs.canonicalize_rootpath(rootpath) + self.rootpath = vclib.ccvs.canonicalize_rootpath(self.rootpath) self.repos = vclib.ccvs.CVSRepository(self.rootname, self.rootpath, self.auth, @@ -308,7 +309,7 @@ class Request: # $CVSHeader$ os.environ['CVSROOT'] = self.rootpath elif self.repos.roottype() == 'svn': - self.rootpath = vclib.svn.canonicalize_rootpath(rootpath) + self.rootpath = vclib.svn.canonicalize_rootpath(self.rootpath) self.repos = vclib.svn.SubversionRepository(self.rootname, self.rootpath, self.auth, @@ -898,6 +899,7 @@ def setup_authorizer(cfg, username, rootname=None): return None # First, try to load a module with the configured name. + # FIXME FIXME FIXME This hack leads to ALL authorizers having 'viewvc.ViewVCAuthorizer' as their class name import imp fp = None try: @@ -1212,7 +1214,6 @@ _re_rewrite_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)' # Matches email addresses _re_rewrite_email = re.compile('([-a-zA-Z0-9_.\+]+)@' '(([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})') -_re_rewrites_html = [ [ _re_rewrite_url, r'\1' ] ] # Matches revision references _re_rewrite_svnrevref = re.compile(r'\b(r|rev #?|revision #?)([0-9]+)\b') @@ -1306,9 +1307,6 @@ class ViewVCHtmlFormatter: trunc_s = mobj.group(1)[:maxlen] return self._entity_encode(trunc_s), len(trunc_s) - def format_utf8(self, mobj, userdata, maxlen=0): - return userdata(mobj.group(0)) - def format_svnrevref(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as an @@ -1450,11 +1448,15 @@ class LogFormatter: def get(self, maxlen=0, htmlize=1): cfg = self.request.cfg - + # Prefer the cache. if self.cache.has_key((maxlen, htmlize)): return self.cache[(maxlen, htmlize)] - + + # UTF-8 in CVS messages. + if self.request.roottype == 'cvs': + self.log = self.request.utf8(self.log) + # If we are HTML-izing... if htmlize: # ...and we don't yet have ViewVCHtmlFormatter() object tokens... @@ -1462,10 +1464,6 @@ class LogFormatter: # ... then get them. lf = ViewVCHtmlFormatter() - # UTF-8 in CVS messages. - if self.request.roottype == 'cvs': - lf.add_formatter('.*', lf.format_utf8, self.request.utf8) - # Rewrite URLs. lf.add_formatter(_re_rewrite_url, lf.format_url) @@ -1611,6 +1609,7 @@ def common_template_data(request, revision=None, mime_type=None): 'tarball_href' : None, 'up_href' : None, 'username' : request.username, + 'env_user_url' : os.environ.get('user_url', ''), 'view' : _view_codes[request.view_func], 'view_href' : None, 'vsn' : __version__, @@ -1866,6 +1865,7 @@ def markup_stream(request, cfg, blame_data, file_lines, filename, c, encoding = cfg.guesser().guess_charset(content) if encoding: file_lines = c.rstrip('\n').split('\n') + file_lines = [ i+'\n' for i in file_lines ] else: encoding = 'unknown' @@ -2042,7 +2042,7 @@ def markup_or_annotate(request, is_annotate): if not mime_type or mime_type == default_mime_type: try: - fp, revision = request.repos.openfile(path, rev) + fp, revision = request.repos.openfile(path, rev, {}) mime_type = request.cfg.guesser().guess_mime(None, None, fp) fp.close() except: @@ -2292,6 +2292,7 @@ def view_roots(request): # add in the roots for the selection roots = [] + expand_root_parents(request.cfg) allroots = list_roots(request) if len(allroots): rootnames = allroots.keys() @@ -2795,7 +2796,7 @@ def view_log(request): for rev in show_revs: entry = _item() entry.rev = rev.string - entry.state = (cvs and rev.dead and 'dead') + entry.state = (request.roottype == 'cvs' and rev.dead and 'dead') entry.author = rev.author entry.changed = rev.changed entry.date = make_time_string(rev.date, cfg) @@ -4392,7 +4393,8 @@ def validate_query_args(request): # First, make sure the the XXX_match args have valid values: arg_match = arg_base + '_match' arg_match_value = request.query_dict.get(arg_match, 'exact') - if not arg_match_value in ('exact', 'like', 'glob', 'regex', 'notregex'): + if not arg_match_value in ('exact', 'like', 'glob', 'regex', 'notregex') and \ + (arg_base != 'comment' or arg_match_value != 'fulltext'): raise debug.ViewVCException( 'An illegal value was provided for the "%s" parameter.' % (arg_match), @@ -4442,8 +4444,8 @@ def view_queryform(request): data = common_template_data(request) data.merge(TemplateData({ - 'repos' : request.server.escape(repos), - 'repos_match' : request.server.escape(repos_match), + 'repos' : request.server.escape(repos or ''), + 'repos_match' : request.server.escape(repos_match or ''), 'repos_type' : escaped_query_dict_get('repos_type', ''), 'query_revision' : escaped_query_dict_get('query_revision', ''), 'search_content' : escaped_query_dict_get('search_content', ''), @@ -4855,8 +4857,8 @@ def query_patch(request, commits): '400 Bad Request') server_fp.write('Index: %s\n===================================================================\n' % (file)) try: - rdate1, _, _, _ = repos.revinfo(rev1) - rdate2, _, _, _ = repos.revinfo(rev2) + rdate1, _, _, _, _ = repos.revinfo(rev1) + rdate2, _, _, _, _ = repos.revinfo(rev2) rdate1 = datetime.date.fromtimestamp(rdate1).strftime(' %Y/%m/%d %H:%M:%S') rdate2 = datetime.date.fromtimestamp(rdate2).strftime(' %Y/%m/%d %H:%M:%S') except vclib.UnsupportedFeature: @@ -4868,7 +4870,7 @@ def query_patch(request, commits): p2 = _path_parts(repos.get_location(file, rev2, rev2)) else: p2 = _path_parts(file) - fd, fr = repos.openfile(p2, rev2) + fd, fr = repos.openfile(p2, rev2, {}) if not fd or rev2 != fr: raise vclib.ItemNotFound(p2) if fd: @@ -4886,7 +4888,7 @@ def query_patch(request, commits): p1 = _path_parts(repos.get_location(p1, rev1, rev1)) else: p1 = _path_parts(file) - fd, fr = repos.openfile(p1, rev1) + fd, fr = repos.openfile(p1, rev1, {}) if fd: fd.close() rev1 = fr @@ -5290,16 +5292,19 @@ def find_root_in_parents(cfg, rootname, roottype): if repo_type != roottype: continue pp = os.path.normpath(pp[:pos].strip()) - - rootpath = None - if roottype == 'cvs': - rootpath = vclib.ccvs.find_root_in_parent(pp, rootname) - elif roottype == 'svn': - rootpath = vclib.svn.find_root_in_parent(pp, rootname) - if rootpath is not None: - return rootpath - return None + if roottype == 'cvs': + roots = vclib.ccvs.expand_root_parent(pp) + elif roottype == 'svn': + roots = vclib.svn.expand_root_parent(pp) + else: + roots = {} + if roots.has_key(rootname): + return roots[rootname], rootname + for (k, v) in roots.iteritems(): + if v == rootname: + return rootname, k + return None, None def locate_root(cfg, rootname): """Return a 3-tuple ROOTTYPE, ROOTPATH, ROOTNAME for configured ROOTNAME. diff --git a/notes/TODO b/notes/TODO new file mode 100644 index 00000000..c9c36e25 --- /dev/null +++ b/notes/TODO @@ -0,0 +1,53 @@ +PREFACE +------- +This file will go away soon after release 0.8. Please use the SourceForge +tracker to resubmit any of the items listed below, if you think, it is +still an issue: + http://sourceforge.net/tracker/?group_id=18760 +Before reporting please check, whether someone else has already done this. +Working patches increase the chance to be included into the next release. + -- PeFu / October 2001 + +TODO ITEMS +---------- +*) add Tamminen Eero's comments on how to make Linux directly execute + the Python script. From email on Feb 19. + [ add other examples, such as my /bin/sh hack or the teeny CGI stub + importing the bulk hack ] + +*) insert rcs_path into PATH before calling "rcsdiff". rcsdiff might + use "co" and needs to find it on the path. + +*) show the "locked" flag (attach it to the LogEntry objects). + Idea from Russell Gordon + +*) committing with a specific revision number: + http://mailman.lyra.org/pipermail/viewcvs/2000q1/000008.html + +*) add capability similar to cvs2cl.pl: + http://mailman.lyra.org/pipermail/viewcvs/2000q2/000050.html + suggestion from Chris Meyer . + +*) add a tree view of the directory structure (and files?) + +*) include a ConfigParser.py to help older Python installations + +*) add a check for the rcs programs/paths to viewvc-install. clarify the + dependency on RCS in the docs. + +*) have a "check" mode that verifies binaries are available on rcs_path + + -> alternately (probably?): use rcsparse rather than external tools + +KNOWN BUGS +---------- +*) time.timezone seems to not be available on some 1.5.2 installs. + I was unable to verify this. On RedHat and SuSE Linux this bug + is non existant. + +*) With old repositories containing many branches, tags or thousands + or revisions, the cvsgraph feature becomes unusable (see INSTALL). + ViewVC can't do much about this, but it might be possible to + investigate the number of branches, tags and revision in advance + and disable the cvsgraph links, if the numbers exceed a certain + treshold. diff --git a/notes/authz-dev-TODO b/notes/authz-dev-TODO new file mode 100644 index 00000000..4d885794 --- /dev/null +++ b/notes/authz-dev-TODO @@ -0,0 +1,82 @@ +Here lie TODO items for the pluggable authz system: + + * Subversion uses path privelege to determine visibility of revision + metadata. That logic is pretty Subversion-specific, so it feels like it + belongs outside the vcauth library as just a helper function in viewvc.py + or something. The algorithm is something like this (culled from the + CollabNet implementation, and not expected to work as edited): + + # Subversion revision access levels + REVISION_ACCESS_NONE = 0 + REVISION_ACCESS_PARTIAL = 1 + REVISION_ACCESS_FULL = 2 + + def check_svn_revision_access(request, rev): + # Check our revision access cache first. + if request.rev_access_cache.has_key(rev): + return request.rev_access_cache[rev] + + # Check our cached answer to the question "Does the user have + # an all-access or a not-at-all-access pass?" + if request.full_access is not None: + return request.full_access \ + and REVISION_ACCESS_FULL or REVISION_ACCESS_NONE + + # Get a list of paths changed in REV. + ### FIXME: There outta be a vclib-complaint way to do this, + ### as this won't work for vclib.svn_ra. + import svn.fs + rev_root = svn.fs.revision_root(self.repos.fs_ptr, rev) + changes = svn.fs.paths_changed(rev_root) + + # Loop over the list of changed paths, asking the access question + # for each one. We'll track whether we've found any readable paths + # as well as any un-readable (non-authorized) paths, and quit + # checking as soon as we know our revision access level. + found_readable = 0 + found_unreadable = 0 + for path in changes.keys(): + parts = _path_parts(path) + kind = request.repos.itemtype(parts, rev) + if kind == vclib.DIR: + access = request.auth.check_dir_access(parts, rev) + elif: + access = request.auth.check_file_access(parts, rev) + if access: + found_readable = 1 + else: + found_unreadable = 1 + # Optimization: if we've found at least one readable, and one + # unreadable, we needn't ask about any more paths. + if found_readable and found_unreadable: + break + + # If there are paths but we can't read any of them, no access is + # granted. + if len(changes) and not found_readable: + request.rev_access_cache[rev] = REVISION_ACCESS_NONE + # If we found at least one unreadable path, partial access is + # granted. + elif found_unreadable: + request.rev_access_cache[rev] = REVISION_ACCESS_PARTIAL + # Finally, if there were no paths at all, or none of the existing + # ones were unreadable, grant full access. + else: + request.rev_access_cache[rev] = REVISION_ACCESS_FULL + return request.rev_access_cache[rev] + + The problems are: where does one hang the revision access cache + so that it doesn't survive a given request? On the request, as + shown in the edited code above? + + Can we actually get a good interface into the vcauth layer for + asking the all-access / no-access question? Obviously each vcauth + provider can cache that value for itself and use it as it deems + necessary, but ideally the revision access level question will + want to know if said auth provider was able to answer the question + and, if so, what the answer was. + + Another off-the-wall idea -- let's just teach Subversion to do + this calculation as part of a libsvn_repos API. + + diff --git a/notes/logo/viewvc-logo.odg b/notes/logo/viewvc-logo.odg new file mode 100644 index 0000000000000000000000000000000000000000..8516ec97fe8e26aff800d9d9dde0567a24801f8f GIT binary patch literal 12537 zcmbt)1z1$u7w@4{8l*u=q+uwLk`6&oI;94X?uH?xrE6#@ML-ZF6eUyyVL%!I5s;7+ zBqisa!RyuQ^Zvj4zPIN)=j>SVTWfDN=j^q#)G;tA08|OT(;{Ag!S|XQtfMYm@N%+u zvhnnDwXtw@b+os#@U(Yv=J$5C=5w*|u=n6|akX)_cCqquvT^q0vvs#{wX?VK(E5i! zP{!Yd7Ocs;I797iz1+`4c?j`)cv^URd01Mw^MB{V1=XT}{LKVbF8+bf!ph3V(FPRb z;?8g7Q1@%Y93KXLvECra5*r~OwhPZt-*|INkp2bA_s7PdAX{Bri5 zP8O~nznS>17`9=I0RX6r7i8A5xAOD?ZQ>Uc__YZA)j&NV0+*Ep<@sElZDX`FmCxbR z;)8tWR80020l*K`!YCIG;|9{_`)Zf6g$+Sb+v07&o$Ad#rs z+2gDVKmY&=0B`_6p!xx@4S@hq2mprw2nc{Y8-_puC=`H00R$94o>4>K02B_u;Q#^- zAkUZ~2mp!z;0OSL0FY<0AV>g;1mH*jfzpQ(4a$ZBAS*}<4x%)H1qy+{ArJ%vf;>|U zwxLi6911}|A;>cv;4l;pfx{sPI0ShH3#5i3AaDc(fq)>-P=d@*Bm|CxAdnE`8EDW5 z5H$!B#03HYHG`@^*`P3x8zcwE!49y%p-==AiafIrR1CJ^a3}%}MV=W9;sA%?2q*#p zMV{FY!UCz`NGJjcMV`3>^cLtI&>x@!K#M^WL7PA$K-3^i5Elpp)C`ISWrM;%Zjc-t z2Rp!mfWwhzt^_Rv6@zUA0**ZMENC!@0~|&m;m9*bgVO@e1L%0r)1YfXUxLm9y#=}l z^atnw&|=U;5Hu(YL=D0Oae+WU&7f#dHYg0_2Fby3umdc}vpEA@30ep$2HVK9DFo*q zoN#c4!6`+p9@G?pa{^8R=ylM|s0jvr3AzJB2{MEJ0385Y44Mdn24#V$L6{&e5D2Il z6b;G-g@N24IXDh>pw?j%YJL3wm!`q%s9k_cRpF|RZ?4tMMSxm~GMh0A4qViCDGI1V;nW%e3L3ZJyB zH!!&jKY2#l>-~{;Vtr+I*m*4x1CJlDfjWdoJmEo)@k9%@LZosj9@HHm;FYY z{kh(t))(t5LCvjGqdCx!fUIfnX6TjNXAC!|7n{4gPdA^m9QtxF7Dx-56OA!)tdEZ$ zZtsfOe)_tsXf9(Gko5%c3Je@K4fxg*%$v!eHNCbXeOaoP|o$T_`j zw<3Si{jj-ZJREanP_WucSlVOJoVcRm#j3?FHe$Q^S?k0*p|4LXvO0DjSyIi_1 zM+VKUU36TWz0Xu|@^IQD;Jp32!tDL^FSkVqcE1IVd0o`@E5r<-($`PVOaDx|JJ&p) z7!+W4NP2K=y5;vGzkp;zG*cEMFFRCDl-Tp&CJAZu2_4Q(rDH+#ew>&ga{oRPQl!;E zbQj`wq1vaMn+vauWfry-j4LWx*@rskR{G{CW{|xkURJ0qa|a7?$a~5 zP+jrzFfkZy`-G>?KG|*h;& zdNsD43w?K;bK@HlpAS%rw0iO~uQng8f0D4`-7c8ZY^?aaj+XS`;o&3`qrOoqgMRhC zrrW~D1a7gHYmdz4jn2(xua6f=M$P&(Wt`mXut-M+PQ`CSa{^K-Dt$$0Wm>ru`dj)Y z@;s%g;_L4$aK0v3Hk-NMAD`x zil@hn`=V{ii_;JVtkzPs(XC)V7X&kep2%E1#9|) z2!gAh@2%atH?c$=WR|VIkV2V0AFCs=EYyRU?rFqX&c)5MlpC$YX;r1Q#B`@Ymtw+C zAKLveqUQ-gx=wew&>SG$?TaJ*QO$|2NW^$p(DDkovV!m8%YZj#Vu7G&nF=uzX)){ zhQAHPJK!2DaNUHCyvAG5XiF58)*#^=!D{4h3uNFp($Hj*3tZpPRbkCN9@evuAP;-l z5huP(lG5**cV3)bSEGq=WZ8&D*d_(eIWK-sEUwPRhCk6pvlVag(RzsiwO zhhl1wZ*KA*bC|wneUmlfuQ{yPi=&FQQFGGv)`c#ne0X9Rr(`KfZ1X5s=$P?v)n4cP z&X`eR>Map3&4SAjF{ici3Rsv=MBm}QRxm*@#6}pZ^H*g&V4;NiX{lnD7scPWBlO@l z_2ameJ1?zDYCl^UT6d(r0aWdUxKo?#D1E9H-00e-IhKJZI)hjg3bvSx>2266X6(G5 z$agrc8R=(|2QjbaW0#Wp;CazHFATJU1+j$8Tv<(P!s)U=e2a)l0lVA-;&%grk8}Z@zmW5&AYM0VKkn6k6h@KzSpK$UP_PqJc2H z?W(1{#rycCzQ8BlZQ<#q_9mhWx>EI=4zim$AW8-ytc$8 zK$}Bn&`;Gpd^Nm{%z-IcP1APKei*h-H!@ zv!9nH7GIbMYfs`7r}9XOrWZ()&@w&_w|!RA-J(uM?fwEzLOH7Mt6b(# zJikdCeOu2gv9xmPyJX7cbCi)g>1p4B9+tqB99s0))8Vug)m*9AP9vp4%jqF}se+4^ zF_9KUgeEu|)IG(cT$`Q7i7Par##>JhfOj9lIgNeo95ZOP?iR4Ab9z00^yD;jDiZ&u zuwuwd@404+6h6uiZp9A|1i4!{n`Y0cq*%5GjhDMKtkfmknVHeP#@CKP7RNc-UUaTL zzZv^G!b@EZkvFC|d$$iF^SZ}JLI?H8)`93MAyjN7dqCMcMG21{rvmqLR zXF^I<7YSjrbt3C4Yg3{on=uWmiw*8Es;F?JMLfien^TApFGjiczAC*OH5Z!uyuG9` zEHo2`1jD(_KI`D1Cm)SUVGFC?5-qEwOHxX;pR8nnE!;zYUl(A^3IJ=7V;tr#HGoToJyUWtfK`_Q7 zCUD%6x9oLgo+n1?x3B2N8TTdA8pEe0f|oXLExi}_Ey2j7CYp(Ny7mOeRpBL>ie_%! zd>4MZTw9vCcOp57GV!_M8>uQb=t3Hm;%I`WmM`ac(&Hb=iY=Pnv%HISE#G^GDARw@ zkAdUktoXhsPw7-sfXmZ0<96$`TW7`n78n9kY^n89k! zVPd$+g_N&k+B;bYbCT>?aLHjROIq4@XByZ^$YH9qtud{x+4y>@@jP#=%+NTpn0J}= z+#<JTtq;XKk+I44?f0N+S8p_Tq)%{SndF(3en zP?@66C*6BPfJP_p6hlL&u?1Os!E%!#cQl#INeaPR|!! zM`ECkJj(4$pS=QK?d92i9C`fdBn9=Q?cwR?Xybu8ELk@(_m~tV3u(RHKC5wr{Wa0L zQ*X|55b7VK;wXslacizu)c8vyXglYPlaZ zh2@R4ypXtWxblJ2>_*0PQ#-|pWD$+y|_o(renR+#w5{WVx?;|+Ce*v>zo zXiiHb!u`B(gWTmq>}T~zbt68H++_DlMeN)v&Jh`+`!_U7m4|!Y#BxK8jgkTc^$fPW zt-szrM#PS1BwYTOK$-CzR?gc3wYsy>Ls3N&YoK@Q>i(0a>-MJyZ!{t}Gx8g!oNXr$ z;=kTKZ(?p95Iez1FA}KOakGO`nNc++w{j=UaE9h%Jg%gk%v*e(*n`vk zFHWoal5|%x=htN^-HhTzZ+c8_#XnZky;K-(T26O(`f}(U)&3smQO3=yO&yF3Hi@pZ zBGGk~#KJQKk~6aNn$H5Qe3XU;^e{GHsWZ6=k!D*)kBsgsa#P}b;ZF7OdKR>M1Dd=L zIhXY^fPRVdUom5Ln$co!#2lDicoVs^v; zxY#o-L@!2VofX3IVf0kwv*W5n(td_dnq!FN{;kJzSMF^ff~1IIxCw3%V!89+o$&V_ zskM*WhFO|;>#dxVRF`~dCdoE}*Sz{#m5mTWZUeMf$`qx|)4{Q>IIyW&WJ2g?KA2kz z$huq>G@RqAA{I_yG`5O%b=Ajx!g4R!!IuE>m{uMu3bq*bl2j#iz8!s5t){lkotBoo z%+SqsfEo5lh^UyNF57h_r=Pn zgpYO=ev{%eICzDF5AQz^8<%K(O!(Qq#wv7CnJ$G~fZjxI%S#*|pEP*xqlM;zc4gkf z5_2HURU20%L(aXNS?vO+y9mi+uJiPs2fmYH>?OAeLr)_EU$*Kuit{Hf*<|&YUQ+RX zkbkdH&4~WS^;d=URhcjJ-uJhBn5Mf?_3Yj}&&8mLx9gnjB+X2&=uHfeLJJ1rd$ZJ{ zMHb5<#rO882m8}5Rh{=nA80CINSE;I5>F6JoWilJ4{it=>o(YA%ou#{wO40w(dX^F z{!oLko~k00!lB7Wc|etFIcNa4Z&5>NrJoho;OW<8&UJyVUJnD4^-igm^-v+4K+7j? za45!V_M--2tBzeS3X#cYMOnqzw9+=5()Ieqp-#`HM75_A@V*+(7*OTnZlydC!R0Th zr?TjXNc#L#+DL6w?gkNVt}|I&(SWLJWnZpvf{l9(Hfe6X8cToB`7eBKW9q_y*};~~ z=2czWvYO>WRz}CVQdfEn5r~nly;?+ZjJt`G+lqE-5)Z%g2jt1V%onZvkJn!|&NwzU zJ9gaIPosZCzWPZnulu>xO8%{D-;xAJ)p=6W;wQ`oVr8d_M7PzSQpBbV(3kpR327iq zCg@J4@$Marli)*V>lEzVlp-GGv&)^gmFLXm1uL1h@^+^@U;W&q1M}_kTO{b@;%u|; zSHFwy-$o3DhKNS^kx6kDs6~cX=WpGn%Fy0)R`&{6!-yE^#U8`I0kjx~U zaIdU2&rAL@+lipAnpjP_iqo>c&olU;=eC^^DBV0csa7y&@ zV?}aChXHyLVq@XCIHRc@+GhIMAjA(mvYCF>TMF6OFN%&}?H2$dqmN z?Zt9v{$IEI(eHF1*G0C^Zjf@gSH$|XT{aemPd2UG^{1W_9i@Dt+o{rA8n_7yK?R-s z!%VOiB@a@fb{gzCL5Zq9VQMM01*9*tmrDw~!knb$>vDfkDc4E4+~@rd?ZxPpl7|&A zC}t)ll4|yLW3J{4w2=n>2|CRe?s+-atJ3O&%Lve$<ig%=mP2WqN(~@`~APhh=~3p6dGk5|uoiro8s{m~7oTraZgDBY+=(5(@F*=QBm$oD8 z@J-~Z^H0x(kv8T$Z>Oxpqv5-=HE4QNaqdPrY$$(%~N3JJ!Gx>SC=HlYqtQ8Ay@);!(&m;ivf&LUwpo))N=CP^9a#_Vn5?g7Ta zq*w&`1X!4DoUL4}?VW9A#9W(VkiQUBF*6{mYUNWRdNuP2jhdcZLB=`Ma2c21clh-yzCvVc`plzi%RnUCh@xlduMx3dkfG+cNoSVxVE#;kUQ@%484bX43BIv!<2 zlLKnr&0;Y>rV7PW$AATCUI6eEfWm?yTv^l;QI2`52aAma4d$fDem0UfgeQwm3fRr1 zp(n{=0(+OM05&xi0N`K4Z_C`+=tW;zy>I>r;V@2Q0<;n6qSX*eoYS5V%#MnRD%%KZ zohF5l2zufrp&QPjp9bq0BavfG0E_`mrTv+;dCyFbVfBDAHaiQTe}5dU1Q?BNYjyw9 zIC=By$?jWe?jv(DW*B!JH}I68Kk0R`7_Nt44N1Z4^}U@kP@75mBPoFU{O0lTkp^?d z={xjVnKn@kbb%o(o`Sj=?*O4owiIh$!o{Za(UhJ7wMg;NOghs1EJstS(FEN(0O5|N zH!f4(`uS1YsKaJV#?_>>&u&bz>hP2p=*Z}e%X&c{CL1%n0@ChNx(oyXANiQ&_O3IG zLvRP`pzkoNf?9T@jE~eO^hbl}y#il!+G}J2@!n_|t~1gPN1`!#;F_W}srt1mf;{i4DZJ+7Vt-YXni=tV8pDfF564igr$j2n&v8NyBc# z0#6UXExhi>4!jt9Rb?tWzqSF#E7suT9WV)BaPKTSRSB}%Q($%ij3uJs<1&_!(b2jx zy@X{dp*)JbpfK5<==zWi|BCu+rWFYqRRy|=d~ zoVR}|ND3S0lg&8^*po7N7PVQop<0n(tqFk;8bk;iF4`+O8Uq!8F)U9}izf z?>g-)5{d4s8O`{#UMb1|Q^15a153qTmJIE^xp{*Ih99;}P0;Of+f>dA%)McXtDL74 zNV%(4T?Y&X*RIgnUuJ#N*hs{Gen$xqZoMxygmnwKpg>vo{Fs{`gU9vqIRmQ*g7-bA z`RG|X#(O7SicZV&O>wD-^(YC{}03wBjyi#amuNAjGI%8;YN@iM^Rq_W{YLA2|< zzK}2L#l;($otShCw;;6(55DdmI`cYRl7J~<7Bz!QR62uvjT63WXAtMLDlW2)rqIly z)pJ@LJ`G{!=^3S08EBa6Y6y-TdW+m^AvC6h1zr+ZEtxT6IGA4h)+o;*gZ*)C*o#tj z1(U~AcJ*ESSX1mS&U-6;V3aPbXElSl+Z482W}1KU_81V#rf(b74ds+68=yNToggH^ z8o4XW%QAD|N)*N@N%#%C;1ZlvHhJ+lo z6*2?B`9)&mu9B{m{hN5Gt@me89|spP5zbJ61pwLL_3xWJA;{%n;1YqToAvOrF!9s( zgZp_oeU~NX54Rl@63k!(2)m&Qrm^R&h0+89=W;$;f2c6=FEExdts5si>c`D0P>{7z zgr(h%5x+56ZdYk0_99{kTOK_QTah$6a)*uw`x*F!X)+~XU2(A*bLexKSf-G(PDNbD zCto*zgs(xBxv8mc@8s@aR=Ady3Tfdl3kj-*&5F7+q;XwZ{Fwh`@d2XWc{XE z<*jFW^1_UlGK zB=#XprHLZuLpL9anF;WGWNGi2*q_UHF|f0lpzqJ>_;^@q@@9p-Y|ueh^WAm6>WXGr zCUmB@Ym0>i8Q4e$9QXV?!S3GfZrZe`M6TlR(prN3^|<5BW3)`#g<<-*4Nq?+uk+L? zcMzFVB~w$K+)}@G;b|;pb?ICYnCmnxPISmp-$`|xCWs#GUq!=$-aWG~UOXu(jp=V5 z&l%f!txi0GsfgqYDX}1>A5Y3fcGaDIw6Y|1H$ai`en=>PfQgi6pOEzm#pTCiNo=2p z24FC|R4gM2LUV>^E17f&FOwy1vrS*w!=ECfw}@pUq%oweI5&(jKI&=cxBOhMP8XWl zt@xz$3w2@X!+^tLzltHi_ZD(mK3D;5wcwgC`J87Fwh$9ZI2#;bPf8n;=auni!;b7$Jo!4aM zVqK<@O|Zb_cf72}OlyGy>Y@8O`nlZRCnXW8!dr|1nW3XqHKd1$QEcc5Y;#1Honb;l z;|n$Pu(_kw!cHGC|D8MbWg@-2BfW)jJw-|~f$vT)S`D!FE=l4KkH$ykutq%}Tc%(Q z2n7rS&FcM=TyH*+5o)^KDQKvPxk<-1Fq;!gl%Y28I3eQg5VP%rt>w=Oc$?~KT9o!D z(o~MOJ0JQ}C+IM}ZRQ!oxFJPG&)pr~kXGi_IErXweR0*&xtGSk{Uss00@p+`!kJx1 zjmz=%@mJHtua}6>3thf&%OL0;KtHAEQz@eg?-TX zBC=N6GelnUp4R3(J!Q6;GY2j?r@)?ra?h+-tdJGSzhW z5&@Nnmm+tC(0^oGVn&X?6>tx?+Q@1aj(K>;St%F$MbttIU$JC+-rJ(=SG`&W9(Fr8 z`mdZ`3B1AlFkv5`*HsO9pjI-v{5m7!-VB~r8|OIRwW$6T#v~Q-YfF7=mz;`uT-Z61 zQta?eMv%BRX2iTMPvl3GBIrA|waFA8rBuX}RO7K2IOn`TSa-FdZ&Y-b%N*&6xWO`- zS&O`cNgEzKP|8eJs4$Ebn$tP&!h9p1w9C5r=JC?|pa`>>$(6ZdeZ`b@Jrs3 z^?&V}U@jG)p{R3}S5;Gq-wFK8gW7nUrIpsFUv+uOOaa?CqCB@x<&|0WI2-YLo9GJ; zb5Io<1@4B$&U9k-gDGf+*QFN#Uf}fHRi>WZ@}Y53NiGL2QGO^72CGxmHRH#LQ=%aj zt0P0TeKIG$S;f?*ti%;+4#5?3(vb1 z$=LeP46Dt1dTR;J=IY(hice+IFf}ph(9(Wl))b!7^(BJCF7L|Ks93i*%6LpSMMs4n zo;xMOVyg*d-S;%+V1p*a%${kZUphv)JNRX zhB&CAKdlCn8;OpmKMA7HzQld;agQ>B!%;{~AX7kyDIHNd8%3!0PXE{v|#6e}O}Nm;Ed= zVD)#QKa-RH>6o8fzne$}y7G4^za}gHyBt)y^3S6ACprH}UjBDU|FkRm50ZW+Gyhj9 zEq{>mGr9TSr7-<839Rk>LCXJ??EFt*zo$F@lqU!-j{m3R=O1g~#{&B`f{V)0{8`4p zS3Rqr8JoW|{(6Rq3h@3cL!`fkdw=ku#{YD}`I8Ty4$S-q53W$>ojbDrPx{I>gk&bjVUG$WCBBh@fa#oEl;d$pyt zX;2h^0N6*|q1xK;19X-v*9|~}77CoqU~=gkIGIW1(n)k0+nKJb3-#u5=u}Ure`=$R z(XAbK)_1onmr1@}US;r6RQO(e#d{&S9jof1g*-CGw6-feW{rs6cRGyDt#?MDKh`!B z#D#>;CvoQNKV7vAjX|ujrTTpF{<3>=BGZ{ z$(k(mMu@ETHE@*Pt0FdD68!X;T){i|j7OGG_t}M<);kWiRgR`Pok{Bw%!B(n)NE?w zis0vu+N)(fKU3A(W|UX;B*Opwv}!m+43;0&$|$3~rr6B{pDgTIzzuAO##{AW5NluI3`39D;1fJhLXWL9X41s-;Iq1L@6T__d5ESbbke_eOzL|@LS+t8g@xvDkw-7$4y zZyA2OCu9iEOS)?y9imUWi{c4(5!*@)zMYHYREGzsxIyd&nq60rn~qk0OMWhEW=))e zX1_LcRt?iY% z)pt{ztyC=N9~-tuJcfPKvwuov%EY(a=*f({zf0aQ~8%SNv3bD{5n^>GXAAx zlB~a!9mPdK)3dH{r%YFoA{Pb?fb$8LHZhaPahe zD?U`b_0!f1S@>K-RbY*bZq*>c8&AbG#e;=`)fIuFc_ zymaDQCP30X%UW#~@pqvp@oCMe>^eiK^G%$v=}Z0KE~Y;0+SW&|P3k`NHohOm_c{3Z(bXI8Cj+IYqe=_XMjXE?DVh*RA>;g9 zN?5@TSu@uxk*gD0%|U0!UKroo+t9D7BWoUb_*sLEHM;0{j$^VN_5JBI;_XI%y;IH6 ztz&Cms@vAlnGGvmxnAAPIf?xm9-i`I*i6|iR##J7&340qj#wHQ z;jH%lw0Me9bZ(%ygg&NujovNv>SqVN2Ak8#@1;L8Ke%X?jc{MAa8Hw%wa%w$FY6of z8oZD^F}ci4A#eK)Jzb`UR%A7)Tp?)Rnd>ns@e=`+ZR?^mbvl&|ZI3=-sOm`eIGa=K zLP!XiI~S1DB>R5VV=Yc*XH%a$eTjz$Mh&lxqx~Hpc=v3@i%TkA?Jn4c59L~Z@g`_P zb?ff_!?#Aaa?y8n_MSJawG1+8x>b|Km97t#tkJEWp;3GR-#OWx<+V-r-PaO5f>KRP zhi>(}9Uq+6U*^421T)>$L!F@R$U>U)OrN{`b&b} z*z8qzr9DuO@PvuGD27|O`Q$zY={1kHOZT?y$x@CR05WP_d9@MmJDQf?56x}FNC)JG zBXNO6lq_CLieU+4+@{;cqc#-64IP%zB!4Y#`fPTc$5Qd!d0b8phUo0qYl(F`eV_mJ z813OY>AVQ9b=Zr18IOO`lk-2^$gFTQsqS}EzRBC4Cv4}yISzsC*=40Yn#;HT$9L8F z#9Z-kdly;lxfOh4wo@f9Cpxa)$x6$lgSY2W67PaUhr#Mk9xVpZr;by0^`(zhGDMAa z5+E@=J;@mP%a1Z0pBqI-NsTJXo`jW+6#Mx?lv<3niTPK`qX<_z&r!8Hj~4pt(XB-Y{6IYvqwGNQ3f`G#gg(_xJQk!u8SaQbp3VNk@qh9A(QBRN8lmYEQ_PIBfcBwj0$S`}UC3mFlG>CtcV|H5bDOz&); zYS77>miCQYF;wmwzFBE%&_TSEx>IrkIsgv4=yNlB$=x7iv7TDY%(Z$s2H?wO5T9 z8;LxBEA)w>mC!D^@;y+GT`iNJGgLCRDi&fR`wa%!qxv|`>h8vdFy26u%U21W$AJu# z)t8Mu2R0qGzIZDNwpU|K;ii!4*4-;A71L`!)178)T1&{+Qf^%%n~sgpG+4cdQj=J{ z+uw>B$cihYgfS3g;n*1!+i%AlH{xfrL~jfaoS^sc*PhB#%Q@oJ(k>>ko!D4bq*-kS z+uK-HDa>dpOmbrv1LL-CheyBmz1dsDaB z26ZLU_EU!&g>M|p>ntdZ8Ly$0Gfrio`NQHHitUX8Mv`~k%L{xeS$(acT-BvB1Rb_4 z8J*I!K14cO1bdoZYy@HMv*q2+iG3z_;4>`J{6e1(w7ys=Pce@O!w+v6D8y{~BB4E) zDb3xToxLG8qZxZRt3OCY;Y76pwJlFK&q>k}LtA1WE=f5HNxV6jEJXu)Ju*{?s{8 zP{{nS;(l7;WHyTn2zF7Ra5D~@W&2og}>#&l-}Rgdj&j{v7Q911|HBOKuSIBXv;PuxXPY-SYa5+Ap^nw$?^1uY%fIzTOerW*@&X)unk?jMPfCRN0$D0eN zFQz^~{qgu~3xc)(I3eJc3@($dP1Yxq5eO_Afxr>Lp#csIIIsu=3V}eNk-AW@IB%}K z00Ro##?}t}1)BlDc9swj)bne70z{Tl{Y1+f`a49>e=!30BLJ)h7Es5a00$`Ch~eyQ z4=g4WOqZJc1p$rUG7za;DwFNH$cy)PMxbz0FFK1r<1*N+@3jMq#j|{vOknYb07IMl zaG4Ai$l78Lzv6`y)U?zE!H4U{<}4wF0g*!Ke@6!d={sTq*KmTpf>@x#B0=Cy zU`Jzqqy-x$ff8PcqzNvm3z$WsFc>6mHv+Q@f!L+|3y%IAx(gJf6A1;gFI@l*i$!Ar z7huVTLgPU~1sA~jVM8F0AO-)mp-_16jQwWA;lbD8HycXhPhT_&Wc#;$Smd9+Slpk! z_&@7IspG*Aa|C~QEEs`wuw6KX&E^7vQv|;H1}qmgut>LHLIUgoZ9Gnc zMApEOuvn5l3XLP6F#1>|mPpdq&?8}p7_=_*|8EhTToRi`bfeQeynQ?YI8h&g18LGA s6YyjVfq+9J5oiK|OeUeQ`g#~FL2&xKxl|5!kri+i1yxkkHzh;=2e;#>Z2$lO literal 0 HcmV?d00001 diff --git a/notes/logo/viewvc-logo.svg b/notes/logo/viewvc-logo.svg new file mode 100644 index 00000000..b67df99d --- /dev/null +++ b/notes/logo/viewvc-logo.svg @@ -0,0 +1,2 @@ + +Master slideSlideDrawingDrawingDrawingDrawingDrawingDrawingDrawingDrawingDrawing \ No newline at end of file diff --git a/notes/vclib-enhancements.txt b/notes/vclib-enhancements.txt new file mode 100644 index 00000000..627e23df --- /dev/null +++ b/notes/vclib-enhancements.txt @@ -0,0 +1,78 @@ +The following is an email from a developer who was integrating bzr +into ViewVC in which he shares some thoughts on how to further +abstract the version control system interactions into first-class APIs +in the vclib module. + + Subject: Re: [ViewCVS-dev] difflib module + Date: Wed, 1 Jun 2005 16:59:10 -0800 + From: "Johan Rydberg" + To: "Michael Pilato" + Cc: + + "C. Michael Pilato" writes: + + >> I've tried to minimize the changes to the viewcvs.py, but of course + >> there are a few places where some things has to be altered. + > + > Well, if along the way, you have ideas about how to further abstract + > stuff into the vclib/ modules, please post. + + I came up with a few as off now; + + * Generalize revision counting; svn starts from 0, bzr starts from 1. + Can be done by a constant; request.repos.MIN_REVNO. For CVS I'm not + sure exactly what should be done. Right now this is only used in + view_revision_svn, so it is not a problem in the short term. + + * Generalize view_diff; + + * Have a repo-method diff(file1, rev1, file2, rev2, args) that returns + (date1, date2, fp). Means human_readbale_diff and raw_diff does not + have to parse dates. Good for VCS that does not have the date in + the diff. [### DONE ###] + + * I'm not sure you should require GNU diff. Some VCS may use own + diff mechanisms (bzr uses difflib, _or_ GNU diff when needed. + Monotone uses its own, IIRC.) + + * Generalize view_revision ; + + * Have a method, revision_info(), which returns (date, author, msg, + changes) much like vclib.svn.get_revision_info. The CVS version + can raise a ViewCVSException. [### DONE ###] + + * Establish a convention for renamed/copied files; current should + work good enough (change.base_path, change.base_rev) but action + string must be same for both svn and others. + + * request.repos.rev (or .revision) should give the current revision + number of the repo. No need for this (from view_directory): + + if request.roottype == 'svn': + revision = str(vclib.svn.created_rev(request.repos, request.where)) + + If svn needs the full name of the repo, why not give it when the + repo is created? + + * request.repos.youngest vs vclib.svn.get_youngest_revision(REPO) + + * More object oriented; + + * The subversion backend is not really object oriented. viewcfg.py uses + a lot function from vclib.svn, which could instead be methods of the + Repository class. Example: + + diffobj = vclib.svn.do_diff(request.repos, p1, int(rev1), + p2, int(rev2), args) + + This should be a method of the repository; + + diffobj = request.repos.do_diff(p1, rev1, ...) + + I have identified the following functions; + + - vclib.svn.created_rev + - vclib.svn.get_youngest_revision + - vclib.svn.date_from_rev + - vclib.svn.do_diff + - vclib.svn.get_revision_info diff --git a/templates/classic/include/header.ezt b/templates/classic/include/header.ezt index bfb9019d..d6527956 100644 --- a/templates/classic/include/header.ezt +++ b/templates/classic/include/header.ezt @@ -17,7 +17,7 @@ -
ViewVC logotype
+
ViewVC logotype

[page_title]

diff --git a/templates/classic/rss.ezt b/templates/classic/rss.ezt index c3c9ae00..1415d026 100644 --- a/templates/classic/rss.ezt +++ b/templates/classic/rss.ezt @@ -11,7 +11,43 @@ [if-any commits.rss_url][commits.rss_url][end] [commits.author] [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] - <pre>[format "xml"][format "html"][commits.log][end][end]</pre> + + [format "xml"] + <pre>[commits.log]</pre> + <table> + [for commits.files] + <tr> + <td style="vertical-align: top;"> + [define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] + [if-any commits.files.rev][if-any rev_href]<a href="[rev_href]">[end][commits.files.rev][if-any rev_href]</a>[end][else]&nbsp;[end] + </td> + <td style="vertical-align: top;"> + <a href="[commits.files.dir_href]">[commits.files.dir]/</a> + <a href="[commits.files.log_href]">[commits.files.file]</a> + </td> + <td style="vertical-align: top;"> + [is commits.files.type "Add"]<ins>[end] + [is commits.files.type "Change"]<a href="[commits.files.diff_href]">[end] + [is commits.files.type "Remove"]<del>[end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"]</ins>[end] + [is commits.files.type "Change"]</a>[end] + [is commits.files.type "Remove"]</del>[end] + </td> + </tr> + [end] + [if-any commits.limited_files] + <tr class="vc_row_[if-index commits even]even[else]odd[end]"> + <td>&nbsp;</td> + <td colspan="5"> + <strong><em><small>Only first [commits.num_files] files shown. + <a href="[limit_changes_href]">Show all files</a> or + <a href="[queryform_href]">adjust limit</a>.</small></em></strong> + </tr> + [end] + </table> + [end] + [end] diff --git a/templates/default/include/header.ezt b/templates/default/include/header.ezt index 3b08eaff..d979b22b 100644 --- a/templates/default/include/header.ezt +++ b/templates/default/include/header.ezt @@ -23,7 +23,7 @@
diff --git a/templates/default/query_form.ezt b/templates/default/query_form.ezt index 8060d0ba..b8b0d7d9 100644 --- a/templates/default/query_form.ezt +++ b/templates/default/query_form.ezt @@ -1,39 +1,82 @@ [# setup page definitions] - [define page_title]Query on:[end] + [define page_title]Query on /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] + [include "include/header.ezt" "query"] +

+Directory +Browse Directory

+
[for query_hidden_values][end] + [if-any enable_search_content] + + + + + [end] + + + + [is roottype "cvs"] [# For subversion, the branch field is not used ] @@ -41,85 +84,97 @@ + + + + @@ -128,8 +183,10 @@ @@ -186,7 +243,8 @@ diff --git a/templates/default/query_results.ezt b/templates/default/query_results.ezt index 5f8b85ba..e0cda9c3 100644 --- a/templates/default/query_results.ezt +++ b/templates/default/query_results.ezt @@ -16,8 +16,19 @@ query capabilities, or asking your administrator to raise the database response size threshold.

[end] -

Modify query

+

Modify query[if-any repos_root] [else] + [if-any repos_type] + [is repos_type "cvs"]| Look only in SVN | Look in all repos [end] + [is repos_type "svn"]| Look only in CVS | Look in all repos [end] + [else] + | Look only in SVN | Look only in CVS + [end] +[end]

Show commands which could be used to back out these changes

+

+ Show a patch built from these changes + [if-any patch_unsecure]
CAUTION: selected changes are not contiguous, patch may include differences from other commits.[end] +

+[plus_count]/-[minus_count] lines changed.

diff --git a/tests/testparse.py b/tests/testparse.py new file mode 100644 index 00000000..ceafe96d --- /dev/null +++ b/tests/testparse.py @@ -0,0 +1,51 @@ + +MODULE = '/home/gstein/testing/cvsroot/mod_dav' +OUTPUT = 'rlog-dump' + +import sys +sys.path.insert(0, '../lib') + +import os +import rlog + + +def get_files(root): + all_files = [ ] + os.path.walk(root, _collect_files, all_files) + all_files.sort() + return all_files + +def _collect_files(all_files, dir, files): + for f in files: + if f[-2:] == ',v': + all_files.append(os.path.join(dir, f)) + +def get_config(): + class _blank: + pass + cfg = _blank() + cfg.general = _blank() + cfg.general.rcs_path = '' + return cfg + + +def gen_dump(cfg, out_fname, files, func): + out = open(out_fname, 'w') + for f in files: + data = func(cfg, f) + out.write(data.filename + '\n') + tags = data.symbolic_name_hash.keys() + tags.sort() + for t in tags: + out.write('%s:%s\n' % (t, data.symbolic_name_hash[t])) + for e in data.rlog_entry_list: + names = dir(e) + names.sort() + for n in names: + out.write('%s=%s\n' % (n, getattr(e, n))) + +def _test(): + cfg = get_config() + files = get_files(MODULE) + gen_dump(cfg, OUTPUT + '.old', files, rlog.GetRLogData) + gen_dump(cfg, OUTPUT + '.new', files, rlog.get_data) diff --git a/tests/vclib/co.py b/tests/vclib/co.py new file mode 100755 index 00000000..b5af5946 --- /dev/null +++ b/tests/vclib/co.py @@ -0,0 +1,53 @@ +#!/usr/local/bin/python +import sys, os.path +sys.path.append( os.path.normpath(os.path.join(sys.path[0],"..","..","lib")) ) +import vclib.ccvs +import popen +def usage(): + print """ + co simulation using vclib!!! + python co.py <(relative) Path to file> + """ + sys.exit() +def convertpath(s): + a=(s,'') + res=[] + while (a[0]!=''): + a=os.path.split(a[0]) + res= [a[1]]+res + return res + +def compareco(repo,file,rev): + a=vclib.ccvs.CVSRepository("lucas",repo) + f=a.getfile(convertpath(file)) # example: ["kdelibs","po","Attic","nl.po"] + r=f.tree[rev] + fp1 = r.checkout() + fp2 = popen.popen('co', + ('-p'+rev, os.path.join(repo,file) ), 'r') + l1 = fp1.readlines() + l2 = fp2.readlines() + ok=1 + for i in range(0,len(l1)-1): + if l1[i] != l2[i+2]: + print " Difference in line %d"% i + print " line from CCVS %s" % l1[i] + print " line from RCS %s" % l2[i+2] + ok=0 + return ok + +if len(sys.argv)==4: + compareco(sys.argv[1],sys.argv[2],sys.argv[3]) +elif len(sys.argv)==3: + a=vclib.ccvs.CVSRepository("lucas",sys.argv[1]) + f=a.getfile(convertpath(sys.argv[2])) # example: ["kdelibs","po","Attic","nl.po"] + for rev in f.tree.keys(): + print ("revision: %s" % rev), + if compareco(sys.argv[1],sys.argv[2],rev): + print "ok" + else: + print "fail" + +else: + usage() + + \ No newline at end of file diff --git a/tests/vclib/rlog.py b/tests/vclib/rlog.py new file mode 100644 index 00000000..dc5211b1 --- /dev/null +++ b/tests/vclib/rlog.py @@ -0,0 +1,5 @@ +#!/usr/local/bin/python +import sys, os.path +sys.path.append( os.path.normpath(os.path.join(sys.path[0],"..","..","lib")) ) +import vclib.ccvs +import popen diff --git a/viewvc-install b/viewvc-install index c52b05aa..fd1b5270 100755 --- a/viewvc-install +++ b/viewvc-install @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- Mode: python -*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. @@ -60,6 +60,8 @@ FILE_INFO_LIST = [ ("bin/cvsdbadmin", "bin/cvsdbadmin", 0755, 1, 0, 0), ("bin/svndbadmin", "bin/svndbadmin", 0755, 1, 0, 0), ("bin/make-database", "bin/make-database", 0755, 1, 0, 0), + ("bin/svnupdate-async", "bin/svnupdate-async", 0755, 1, 0, 0), + ("bin/svnupdate-async.sh", "bin/svnupdate-async.sh", 0755, 1, 0, 0), ("conf/viewvc.conf.dist", "viewvc.conf.dist", 0644, 0, 0, 0), ("conf/viewvc.conf.dist", "viewvc.conf", 0644, 0, 1, 0), ("conf/cvsgraph.conf.dist", "cvsgraph.conf.dist", 0644, 0, 0, 0), @@ -235,492 +237,6 @@ LEGEND ### If we get here, we're creating or overwriting the existing file. - # Read the source file's contents. - try: - contents = open(src_path, "rb").read() - except IOError, e: - error(str(e)) - - # Ensure the existence of the containing directories. - dst_parent = os.path.dirname(destdir_path) - if not os.path.exists(dst_parent): - try: - compat.makedirs(dst_parent) - print " created %s%s" % (dst_parent, os.sep) - except os.error, e: - if e.errno == 17: # EEXIST: file exists - return - if e.errno == 13: # EACCES: permission denied - error("You do not have permission to create directory %s" \ - % (dst_parent)) - error("Unknown error creating directory %s" \ - % (dst_parent, OSError, e)) - - # Now, write the file contents to their destination. - try: - exists = os.path.exists(destdir_path) - open(destdir_path, "wb").write(contents) - print " %s %s" \ - % (exists and 'replaced ' or 'installed', dst_path) - except IOError, e: - if e.errno == 13: - # EACCES: permission denied - error("You do not have permission to write file %s" % (dst_path)) - error("Unknown error writing file %s" % (dst_path, IOError, e)) - - # Set the files's mode. - os.chmod(destdir_path, mode) - - # (Optionally) compile the file. - if compile_it: - py_compile.compile(destdir_path, destdir_path + "c" , dst_path) - - -def install_tree(src_path, dst_path, prompt_replace): - """Install a tree whose source is at SRC_PATH (which is relative - to the ViewVC source directory) into the location DST_PATH (which - is relative both to the global ROOT_DIR and DESTDIR settings). If - PROMPT_REPLACE is set (and is not overridden by global setting - CLEAN_MODE), prompt the user for how to deal with already existing - files that differ from the to-be-installed version.""" - - orig_src_path = src_path - orig_dst_path = dst_path - src_path = _actual_src_path(src_path) - dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) - destdir_path = os.path.join(DESTDIR + dst_path) - - # Get a list of items in the directory. - files = os.listdir(src_path) - files.sort() - for fname in files: - # Ignore some stuff found in development directories, but not - # intended for installation. - if fname == 'CVS' or fname == '.svn' or fname == '_svn' \ - or fname[-4:] == '.pyc' or fname[-5:] == '.orig' \ - or fname[-4:] == '.rej' or fname[0] == '.' \ - or fname[-1] == '~': - continue - - orig_src_child = orig_src_path + '/' + fname - orig_dst_child = orig_dst_path + '/' + fname - - # If the item is a subdirectory, recurse. Otherwise, install the file. - if os.path.isdir(os.path.join(src_path, fname)): - install_tree(orig_src_child, orig_dst_child, prompt_replace) - else: - set_paths = 0 - compile_it = fname[-3:] == '.py' - install_file(orig_src_child, orig_dst_child, 0644, - set_paths, prompt_replace, compile_it) - - # Check for .py and .pyc files that don't belong in installation. - for fname in os.listdir(destdir_path): - if not os.path.isfile(os.path.join(destdir_path, fname)) or \ - not ((fname[-3:] == '.py' and fname not in files) or - (fname[-4:] == '.pyc' and fname[:-1] not in files)): - continue - - # If we get here, there's cruft. - delete = None - if CLEAN_MODE == 'true': - delete = 1 - elif CLEAN_MODE == 'false': - delete = 0 - else: - print "File %s does not belong in ViewVC %s." \ - % (dst_path, version) - while 1: - temp = raw_input("Do you want to [D]elete it, or [L]eave " - "it as is? ") - temp = string.lower(temp[0]) - if temp == "l": - delete = 0 - elif temp == "d": - delete = 1 - - if delete is not None: - break - - assert delete is not None - if delete: - print " deleted %s" % (os.path.join(dst_path, fname)) - os.unlink(os.path.join(destdir_path, fname)) - else: - print " preserved %s" % (os.path.join(dst_path, fname)) - - - -def usage_and_exit(errstr=None): - stream = errstr and sys.stderr or sys.stdout - stream.write("""Usage: %s [OPTIONS] - -Installs the ViewVC web-based version control repository browser. - -Options: - - --help, -h, -? Show this usage message and exit. - - --prefix=DIR Install ViewVC into the directory DIR. If not provided, - the script will prompt for this information. - - --destdir=DIR Use DIR as the DESTDIR. This is generally only used - by package maintainers. If not provided, the script will - prompt for this information. - - --clean-mode= If 'true', overwrite existing ViewVC configuration files - found in the target directory, and purge Python modules - from the target directory that aren't part of the ViewVC - distribution. If 'false', do not overwrite configuration - files, and do not purge any files from the target - directory. If not specified, the script will prompt - for the appropriate action on a per-file basis. - -""" % (os.path.basename(sys.argv[0]))) - if errstr: - stream.write("ERROR: %s\n\n" % (errstr)) - sys.exit(1) - else: - sys.exit(0) - - -if __name__ == "__main__": - # Option parsing. - try: - optlist, args = getopt.getopt(sys.argv[1:], "h?", - ['prefix=', - 'destdir=', - 'clean-mode=', - 'help']) - except getopt.GetoptError, e: - usage_and_exit(str(e)) - for opt, arg in optlist: - if opt == '--help' or opt == '-h' or opt == '-?': - usage_and_exit() - if opt == '--prefix': - ROOT_DIR = arg - if opt == '--destdir': - DESTDIR = arg - if opt == '--clean-mode': - arg = arg.lower() - if arg not in ('true', 'false'): - usage_and_exit("Invalid value for --overwrite parameter.") - CLEAN_MODE = arg - - # Print the header greeting. - print """This is the ViewVC %s installer. - -It will allow you to choose the install path for ViewVC. You will now -be asked some installation questions. Defaults are given in square brackets. -Just hit [Enter] if a default is okay. -""" % version - - # Prompt for ROOT_DIR if none provided. - if ROOT_DIR is None: - if sys.platform == "win32": - pf = os.getenv("ProgramFiles", "C:\\Program Files") - default = os.path.join(pf, "viewvc-" + version) - else: - default = "/usr/local/viewvc-" + version - temp = string.strip(raw_input("Installation path [%s]: " \ - % default)) - print - if len(temp): - ROOT_DIR = temp - else: - ROOT_DIR = default - - # Prompt for DESTDIR if none provided. - if DESTDIR is None: - default = '' - temp = string.strip(raw_input( - "DESTDIR path (generally only used by package " - "maintainers) [%s]: " \ - % default)) - print - if len(temp): - DESTDIR = temp - else: - DESTDIR = default - - # Install the files. - print "Installing ViewVC to %s%s:" \ - % (ROOT_DIR, DESTDIR and " (DESTDIR = %s)" % (DESTDIR) or "") - for args in FILE_INFO_LIST: - apply(install_file, args) - for args in TREE_LIST: - apply(install_tree, args) - - # Write LIBRARY_DIR and CONF_PATHNAME into viewvcinstall.py config file - viewvcinstallpath = """#!/usr/bin/python -LIBRARY_DIR = "%s" -CONF_PATHNAME = "%s" -""" % (os.path.join(ROOT_DIR, 'lib'), os.path.join(ROOT_DIR, 'viewvc.conf')) - open(os.path.join(ROOT_DIR, 'bin', 'viewvcinstallpath.py'),'wb').write(viewvcinstallpath) - if sys.platform != 'win32': - for i in ['cgi', 'mod_python']: - os.symlink(os.path.join(ROOT_DIR, 'bin', 'viewvcinstallpath.py'), os.path.join(ROOT_DIR, 'bin', i, 'viewvcinstallpath.py')) - else: - for i in ['asp', 'cgi', 'mod_python']: - open(os.path.join(ROOT_DIR, 'bin', i, 'viewvcinstallpath.py'),'wb').write(viewvcinstallpath) - - # Print some final thoughts. - print """ - -ViewVC file installation complete. - -Consult the INSTALL document for detailed information on completing the -installation and configuration of ViewVC on your system. Here's a brief -overview of the remaining steps: - - 1) Edit the %s file. - - 2) Either configure an existing web server to run - %s. - - Or, copy %s to an - already-configured cgi-bin directory. - - Or, use the standalone server provided by this distribution at - %s. -""" % (os.path.join(ROOT_DIR, 'viewvc.conf'), - os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), - os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), - os.path.join(ROOT_DIR, 'bin', 'standalone.py')) -#!/usr/bin/env python -# -*- Mode: python -*- -# -# Copyright (C) 1999-2007 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 ViewVC -# distribution or at http://viewvc.org/license-1.html. -# -# For more information, visit http://viewvc.org/ -# -# ----------------------------------------------------------------------- -# -# Install script for ViewVC -# -# ----------------------------------------------------------------------- - -import os -import sys -import string -import re -import traceback -import py_compile -import getopt -import StringIO - -# Get access to our library modules. -sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), 'lib')) - -import compat -import viewvc -import compat_ndiff -version = viewvc.__version__ - - -## Installer defaults. -DESTDIR = None -ROOT_DIR = None -CLEAN_MODE = None - - -## List of files for installation. -## tuple (source path, -## destination path, -## mode, -## boolean -- search-and-replace? -## boolean -- prompt before replacing? -## boolean -- compile?) -FILE_INFO_LIST = [ - ("bin/cgi/viewvc.cgi", "bin/cgi/viewvc.cgi", 0755, 1, 0, 0), - ("bin/cgi/query.cgi", "bin/cgi/query.cgi", 0755, 1, 0, 0), - ("bin/mod_python/viewvc.py", "bin/mod_python/viewvc.py", 0755, 1, 0, 0), - ("bin/mod_python/query.py", "bin/mod_python/query.py", 0755, 1, 0, 0), - ("bin/mod_python/handler.py", "bin/mod_python/handler.py", 0755, 1, 0, 0), - ("bin/mod_python/.htaccess", "bin/mod_python/.htaccess", 0755, 0, 0, 0), - ("bin/standalone.py", "bin/standalone.py", 0755, 1, 0, 0), - ("bin/loginfo-handler", "bin/loginfo-handler", 0755, 1, 0, 0), - ("bin/cvsdbadmin", "bin/cvsdbadmin", 0755, 1, 0, 0), - ("bin/svndbadmin", "bin/svndbadmin", 0755, 1, 0, 0), - ("bin/make-database", "bin/make-database", 0755, 1, 0, 0), - ("bin/svnupdate-async", "bin/svnupdate-async", 0755, 1, 0, 0), - ("bin/svnupdate-async.sh", "bin/svnupdate-async.sh", 0755, 1, 0, 0), - ("conf/viewvc.conf.dist", "viewvc.conf.dist", 0644, 0, 0, 0), - ("conf/viewvc.conf.dist", "viewvc.conf", 0644, 0, 1, 0), - ("conf/cvsgraph.conf.dist", "cvsgraph.conf.dist", 0644, 0, 0, 0), - ("conf/cvsgraph.conf.dist", "cvsgraph.conf", 0644, 0, 1, 0), - ("conf/mimetypes.conf.dist", "mimetypes.conf.dist", 0644, 0, 0, 0), - ("conf/mimetypes.conf.dist", "mimetypes.conf", 0644, 0, 1, 0), - ] -if sys.platform == "win32": - FILE_INFO_LIST.extend([ - ("bin/asp/viewvc.asp", "bin/asp/viewvc.asp", 0755, 1, 0, 0), - ("bin/asp/query.asp", "bin/asp/query.asp", 0755, 1, 0, 0), - ]) - - -## List of directories for installation. -## type (source path, -## destination path, -## boolean -- prompt before replacing?) -TREE_LIST = [ - ("lib", "lib", 0), - ("templates", "templates", 1), - ("templates-contrib", "templates-contrib", 1), - ] - - -## List of file extensions we can't show diffs for. -BINARY_FILE_EXTS = [ - '.png', - '.gif', - '.jpg', - ] - - -def _escape(str): - """Callback function for re.sub(). - - re.escape() is no good because it blindly puts backslashes in - front of anything that is not a number or letter regardless of - whether the resulting sequence will be interpreted.""" - return string.replace(str, "\\", "\\\\") - - -def _actual_src_path(path): - """Return the real on-disk location of PATH, which is relative to - the ViewVC source directory.""" - return os.path.join(os.path.dirname(sys.argv[0]), - string.replace(path, '/', os.sep)) - - -def error(text, etype=None, evalue=None): - """Print error TEXT to stderr, pretty printing the optional - exception type and value (ETYPE and EVALUE, respective), and then - exit the program with an errorful code.""" - sys.stderr.write("\n[ERROR] %s\n" % (text)) - if etype: - traceback.print_exception(etype, evalue, None, file=sys.stderr) - sys.exit(1) - - -def replace_var(contents, var, value): - """Replace instances of the variable VAR as found in file CONTENTS - with VALUE.""" - pattern = re.compile('^' + var + r'\s*=\s*.*$', re.MULTILINE) - repl = '%s = r"%s"' % (var, os.path.join(ROOT_DIR, value)) - return re.sub(pattern, _escape(repl), contents) - - -def replace_paths(contents): - """Replace all ViewVC path placeholders found in file CONTENTS.""" - if contents[:2] == '#!': - shbang = '#!' + sys.executable - contents = re.sub('^#![^\n]*', _escape(shbang), contents) - contents = replace_var(contents, 'LIBRARY_DIR', 'lib') - contents = replace_var(contents, 'CONF_PATHNAME', 'viewvc.conf') - return contents - - -def install_file(src_path, dst_path, mode, subst_path_vars, - prompt_replace, compile_it): - """Install a single file whose source is at SRC_PATH (which is - relative to the ViewVC source directory) into the location - DST_PATH (which is relative both to the global ROOT_DIR and - DESTDIR settings), and set the file's MODE. If SUBST_PATH_VARS is - set, substitute path variables in the file's contents. If - PROMPT_REPLACE is set (and is not overridden by global setting - CLEAN_MODE), prompt the user for how to deal with already existing - files that differ from the to-be-installed version. If COMPILE_IT - is set, compile the file as a Python module.""" - - src_path = _actual_src_path(src_path) - dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) - destdir_path = DESTDIR + dst_path - - overwrite = None - if not (prompt_replace and os.path.exists(destdir_path)): - # If the file doesn't already exist, or we've been instructed to - # replace it without prompting, then drop in the new file and get - # outta here. - overwrite = 1 - else: - # If we're here, then the file already exists, and we've possibly - # got to prompt the user for what to do about that. - - # Collect ndiff output from ndiff - sys.stdout = StringIO.StringIO() - compat_ndiff.main([destdir_path, src_path]) - ndiff_output = sys.stdout.getvalue() - - # Return everything to normal - sys.stdout = sys.__stdout__ - - # Collect the '+ ' and '- ' lines. - diff_lines = [] - looking_at_diff_lines = 0 - for line in string.split(ndiff_output, '\n'): - # Print line if it is a difference line - if line[:2] == "+ " or line[:2] == "- " or line[:2] == "? ": - diff_lines.append(line) - looking_at_diff_lines = 1 - else: - # Compress lines that are the same to print one blank line - if looking_at_diff_lines: - diff_lines.append("") - looking_at_diff_lines = 0 - - # If there are no differences, we're done here. - if not diff_lines: - overwrite = 1 - else: - # If we get here, there are differences. - if CLEAN_MODE == 'true': - overwrite = 1 - elif CLEAN_MODE == 'false': - overwrite = 0 - else: - print "File %s exists and is different from source file." \ - % (destdir_path) - while 1: - name, ext = os.path.splitext(src_path) - if ext in BINARY_FILE_EXTS: - temp = raw_input("Do you want to [O]verwrite or " - "[D]o not overwrite? ") - else: - temp = raw_input("Do you want to [O]verwrite, [D]o " - "not overwrite, or [V]iew " - "differences? ") - temp = string.lower(temp[0]) - if temp == "v" and ext not in BINARY_FILE_EXTS: - print """ ----------------------------------------------------------------------------""" - print string.join(diff_lines, '\n') + '\n' - print """ -LEGEND - A leading '- ' indicates line to remove from installed file - A leading '+ ' indicates line to add to installed file - A leading '? ' shows intraline differences. ----------------------------------------------------------------------------""" - elif temp == "d": - overwrite = 0 - elif temp == "o": - overwrite = 1 - - if overwrite is not None: - break - - assert overwrite is not None - if not overwrite: - print " preserved %s" % (dst_path) - return - - ### If we get here, we're creating or overwriting the existing file. - # Read the source file's contents. try: contents = open(src_path, "rb").read() @@ -922,7 +438,7 @@ Just hit [Enter] if a default is okay. ROOT_DIR = temp else: ROOT_DIR = default - + # Prompt for DESTDIR if none provided. if DESTDIR is None: default = '' @@ -933,7 +449,7 @@ Just hit [Enter] if a default is okay. DESTDIR = temp else: DESTDIR = default - + # Install the files. print "Installing ViewVC to %s%s:" \ % (ROOT_DIR, DESTDIR and " (DESTDIR = %s)" % (DESTDIR) or "") @@ -941,7 +457,20 @@ Just hit [Enter] if a default is okay. apply(install_file, args) for args in TREE_LIST: apply(install_tree, args) - + + # Write LIBRARY_DIR and CONF_PATHNAME into viewvcinstall.py config file + viewvcinstallpath = """#!/usr/bin/python +LIBRARY_DIR = "%s" +CONF_PATHNAME = "%s" +""" % (os.path.join(ROOT_DIR, 'lib'), os.path.join(ROOT_DIR, 'viewvc.conf')) + open(os.path.join(ROOT_DIR, 'bin', 'viewvcinstallpath.py'),'wb').write(viewvcinstallpath) + if sys.platform != 'win32': + for i in ['cgi', 'mod_python']: + os.symlink(os.path.join(ROOT_DIR, 'bin', 'viewvcinstallpath.py'), os.path.join(ROOT_DIR, 'bin', i, 'viewvcinstallpath.py')) + else: + for i in ['asp', 'cgi', 'mod_python']: + open(os.path.join(ROOT_DIR, 'bin', i, 'viewvcinstallpath.py'),'wb').write(viewvcinstallpath) + # Print some final thoughts. print """
Search content:
Repository: +   Repository type:   + +
+ + + + +
Branch: - +
Subdirectory: - - (You can list multiple directories separated by commas.) +
+ (you can list multiple directories separated by commas)
File: - +
Revision number: +
+ (you can list multiple revision numbers separated by commas) +
Who: - +
Comment: - +
+
Show at most - changed files per commit. (Use 0 to show all files.) + changed files per commit.
+ (use 0 to show all files)